diff --git a/.gitignore b/.gitignore index 99658f06399c1cd16684d2f9d9392b9536e428aa..76be0df09bbbbc8c1a39f40653b0fde95f99dbc8 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,9 @@ bin/ share/ build/ include/ +!snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/ +_sass/ +.sass-cache/ *.pt.py *.installed.cfg *.sqlite @@ -20,7 +23,7 @@ selenium-server-standalone-2.0b2.jar .settings/ settings.d/*-local.conf *.egg-info -dist +#dist _build .coverage @@ -31,6 +34,7 @@ pithos/data */synnefo/versions/*.py */pithos/*/version.py !*/synnefo/versions/__init__.py +snf-admin-app/synnefo_admin/version.py snf-astakos-app/astakos/version.py snf-tools/synnefo_tools/version.py snf-quotaholder-app/quotaholder_django/version.py @@ -41,5 +45,8 @@ astakosclient/astakosclient/version.py snf-django-lib/snf_django/version.py snf-branding/synnefo_branding/version.py snf-deploy/snfdeploy/version.py +# Temp dirs generated by snf-deploy keygen +snf-deploy/files/root/.ssh +snf-deploy/files/root/ddns *.egg *.tar.gz diff --git a/COPYRIGHT b/COPYRIGHT index 31b631fea2591d9f5f20c492a920c4be01b22077..94a9ed024d3859793618152ea559a168bbcbb5e2 100644 --- a/COPYRIGHT +++ b/COPYRIGHT @@ -1,32 +1,674 @@ -Copyright (C) 2010-2014 GRNET S.A. All rights reserved. - -Redistribution and use in source and binary forms, with or -without modification, are permitted provided that the following -conditions are met: - - 1. Redistributions of source code must retain the above - copyright notice, this list of conditions and the following - disclaimer. - - 2. Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials - provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A. OR -CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. - -The views and conclusions contained in the software and -documentation are those of the authors and should not be -interpreted as representing official policies, either expressed -or implied, of GRNET S.A. + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 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 General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is 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. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + 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. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + 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 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. Use with the GNU Affero General Public License. + + 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 Affero 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 special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU 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 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 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 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 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 General Public License for more details. + + You should have received a copy of the GNU 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 the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + 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 GPL, see +<http://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<http://www.gnu.org/philosophy/why-not-lgpl.html>. diff --git a/Changelog b/Changelog index dc5d36969bcdb9450a4935776860a3721c807afc..2e7bd85d29547178197ddb7fe06e66bec9f03654 100644 --- a/Changelog +++ b/Changelog @@ -6,6 +6,257 @@ Unified Changelog file for Synnefo versions >= 0.13 Since v0.13 most of the Synnefo components have been merged into a single repository and have aligned versions. +.. _Changelog-0.16: + +v0.16 +===== + +Released: Tue Nov 11 10:44:57 EET 2014 + +Synnefo-wide +------------ + +* Replace accumulative projects with pool projects: + + * Projects are now viewed as a source of finite resources. A member can + reserve a part of these resources up to a specified limit. + + * Base quota are now offered through a special purpose user-specific system + project, identified with the same UUID as the user. + + * Each actual resource (Cyclades VM, network, floating IP and Pithos + container) is now also associated with a project besides the owner. + + * In resource creation, project defaults to the user-specific system + project, if not specified otherwise. It is also possible to change the + project assignment of an existing resource. + + * All existing resources have been assigned to the respective + user-specific system projects. + +* Logging mechanism for Synnefo management commands + + * Log all stdout and stderr output of every invocation of snf-manage, + on unique filenames under a given directory. + + * Add a new setting in the snf-common package, namely 'LOG_DIR', which + specifies the directory to be used by Synnefo components to write + their log files. + +* Rename argument names of Synnefo management commands + + * All Synnefo management commands now use 'user/flavor/image/network' + as argument names, instead of 'userid/user-id/owner/flavor-id/image + -id/network-id', for consistency across commands. + +Astakos +------- + +* Decouple projects from applications: + + * Support project creation (by the system) and modification (by a + privileged user) without the need to submit/approve an application. + + * View applications as modifications. When a project is uninitialized + (e.g. an application for a new project is pending), no further + modification is allowed. + + * Applications are removed from the API. A project's last application is + only accessible as part of the project details. + + * Decouple project state from application state; they can be combined by + an API client, if needed. + +* Changes concerning quota and pool projects: + + * A project must provide limits for all registered resources. On project + activation, resources missing are automatically completed using a + skeleton. + + * Field `uplimit' of registed resources is exposed as `system_default' and + provide the skeleton for user-specific system projects. A new field + `project_default' is introduce to act as a skeleton for conventional + projects. + + * The quotaholder now also records project quota besides user quota. The + two types of holders are distinguished with a prefix: `user:' and + `project:'. + + * The quota API is extended to make project quota available. + + * Removed setting `ASTAKOS_PROJECTS_VISIBLE'; we now always display + projects in Astakos menu. + +* Projects can be set `private', making it accessible only to its owner and + members. + +* Admin users API + + * Extend astakos identity API to support user management functionality. + * Admin API related settings + * ``ADMIN_API_ENABLED`` + Whether or not the admin api endpoints will be enabled. + * ``ADMIN_API_PERMITTED_GROUPS`` + A list of group names for which users which belong to any of + them be granted full access to the admin API endpoints. + + +* Updated projects integration in UI + + * Display both max per member/total in group quota in project views. + * Enhanced usage view to display individual resource usage details for + each of the projects the user is associated with. + * Display project usage statistics in project details view. + * Display system projects in admin project list view. + * Handle display of infinite resource quota values. + +* Explicitly specify the lists in which every notification will be sent. + + +Cyclades +-------- + +* Introduce Volumes and Snapshots: + + * Implement 'cyclades_volumes' service, containing the /volumes, /types, + and /snapshots API endpoints under '/volumes/v2.0'. + * Implement `snf-manage volume-{create, list, show, modify, inspect, remove}' + management commands for handling of volumes. + * Implement `snf-manage snapshot-{create, list, show, modify}` management + commands for handling of snapshots. + * Implement 'volume-type-{create, list, modify, show, remove}` management + commands for handling of volume types. + * Implement reconciliation for stale snapshots. + * Add new settings: + + * GANETI_MAX_DISKS_PER_INSTANCE: Maximum number of disks that each Ganeti + instance can have. + * CYCLADES_VOLUME_MAX_SIZE: The maximum allowed size (in GB) for Cyclades + volumes. + * CYCLADES_SNAPSHOTS_ENABLED: Enable/Disable the snapshots feature + altogether at the API level. + + +* Integrate Cyclades resources with Astakos projects. + + * Extend API calls that create resources with the 'project' attribute + in order to assign resources to the specified project. + * Implement API calls for reassigning resources to a new project. + * Export the project that a resource belongs in the 'tenant_id' API + attribute. + +* Add support for snf-vncauthproxy 1.6 (configurable VNC console types). +* Change the ``CYCLADES_VNCAUTHPROXY_OPTS`` setting to a list of dictionaries + and support configurable vncauthproxy proxy addresses (added in + snf-vncauthproxy-1.6). +* Add support for more types of VM console: + + * Modify the current 'console' action to support VNC WebSockets (requires + snf-vncauthproxy=1.6). + * Add support for the three OpenStack Compute console actions + (VNC, RDP, Spice). RDP and Spice are currently not implemented. + +* Update Cyclades to work with Pithos with integrated Archipelago v2 + mapfiles. +* Include functionality for checking the status of the Cyclades update path, + which includes Ganeti, AMQP, snf-ganeti-eventd and snf-dispatcher. This + functionality is implemented as part of the snf-dispatcher and can be used + by passing the '--status-check' option. +* Add setting `CYCLADES_VM_MAX_METADATA` to limit the maximum number of + metadata per vm. +* Add setting `CYCLADES_VOLUME_MAX_METADATA` to limit the maximum number of + metadata per volume. +* Use common -u/--user option to specify the UUID or email of the user, for + all management commands. +* Store basic information about images that have been used to create servers, + in order to preserve this information even if images are deleted from + Pithos. +* Make 'snf-ganeti-eventd' tolerate failures when processing Ganeti jobs. The + daemon will not crash but continue to run in order to process jobs that can + be processed. +* Update 'backend-list' command to not count the free IPs from networks that + are drained. +* Fix the'network-inspect' command to not contain externally reserved IPs + in th number of available IPs. +* Add `GANETI_DISKS_WAIT_FOR_SYNC` setting to decide whether Ganeti will + wait for the disk mirror to sync (`--no-wait-for-sync` Ganeti option). +* Fix mishandling of `MAX_CIDR_BLOCK` setting. Allowed CIDR sizes + changed from (MAX_CIDR_BLOCK, 29) to [MAX_CIDR_BLOCK, 29]. +* Fix various minor bugs. +* Remove stale '--public' and '--user' options from 'image-show' and + 'snapshot-show' management commands. + +Cyclades UI +----------- + +* In sync with the updated astakos projects API + + * Include a project select widget within all resource create views, to let user + decide the project that the created resources will be assigned to. + * Display resource assigned project in resource list views. + * Let user reassign resource project. + +* Volumes API integration. + + * Introduce the Disks list/create views. + * Display machine attached disks in icon/single machine views. + * Use ``CYCLADES_VOLUME_MAX_SIZE`` setting to determine the + maximum allowed disk size. + +* Integrate volume snapshots + + * Include available snapshots in disk/vm create wizards. + * Disk snaphot create actions. + * The ui snapshoting functionality (snapshot actions and listing) + can be enabled/disabled using the introduced ``UI_SNAPSHOTS_ENABLED`` + setting. + +* Replace TigerVNC Java client with an HTML5 Websocket-based client (noVNC) + +* Other UI fixes + + * Disabled suspended vm actions + * Custom error message for 413 api error response codes + * Enable resize actions from both info/actions subviews for active + machines + * Update images collection each time machine create wizard opens + * Fixed a couple of title truncate in several views + * Handle display of infinite resource quota values. + * Improve network create wizard. Support for custom gateway. + * Common font styling for all resource titles + +Pithos +------ + +* Backend modifications to enable/support snapshot creation. +* Backend and API modifications to reflect the modifications in the resource + allocation mechanism via the accumulative projects with pool projects. +* Change default disposition type: If no disposition-type is + specifically requested or an invalid value is passed, the disposition type + is set to 'inline'. + +Admin +----- + +* Introduce Administrator Dashboard, which provides the following: + + * Graphic access to the details of various Synnefo entities (users, VMs, + Projects). + * Intuitive filtering. + * Multiple actions and notifications. + * Charts and statistics generation. + +* Define the Admin node URL and path with the ``ADMIN_BASE_URL`` setting + + * The ``ADMIN_BASE_URL`` setting should be adjusted in every node that Admin + is installed. + +Tools +----- + +* Extend snf-burnin to include testing of snapshots. + + .. _Changelog-0.15.2: v0.15.2 @@ -29,6 +280,7 @@ Cyclades UI .. _Changelog-0.15.1: + v0.15.1 ======= @@ -49,7 +301,6 @@ Astakos * Fix Authenticate Identity API call with trailing slash, which used to fail with 405 (Method not allowed) - Cyclades -------- diff --git a/NEWS b/NEWS index a3a803ef2824650b905145e95a455f16b8f5183e..a11d3c45754eb36ff13ab095b7fa37ed6590d929 100644 --- a/NEWS +++ b/NEWS @@ -5,6 +5,88 @@ Unified NEWS file for Synnefo versions >= 0.13 Since v0.13 all Synnefo components have been merged into a single repository. +.. _NEWS-0.16: + +v0.16 +===== + +Released: Tue Nov 11 10:44:57 EET 2014 + +The Synnefo 0.16 release brings major new features and significant bug fixes +across Synnefo. All users are strongly encouraged to upgrade. Please see +the Synnefo documentation for upgrade instructions to v0.16. + +The most notables changes are: + +Synnefo-wide +------------- + +* Administrator Dashboard: Synnefo 0.16 features a new, integrated Web-based UI + for managing Synnefo users and resources. It allows the administrator to + view, filter, modify, and produce stats for all Synnefo resources. This new + interface will eventually replace the old read-only helpdesk interface, which + has been obsoleted and will be removed in Synnefo 0.17. +* Pool projects: Projects have been upgraded to a new unified `pool project` + type in Synnefo 0.16. Resources get assigned to projects, and project members + may reserve resources from individual projects for their own use. Users may + choose the project where a new resource be charged (e.g., a new Cyclades VM or + a new IP address), and re-assign them freely. Pre-0.16 projects are a subset + of pool projects, with per-user constraints on resource consumption. +* Management command logging: All Synnefo management commands and their outputs + are individually logged, which may prove very useful for debugging and + auditing purposes. +* Archipelago has been adopted as the single backend for Pithos. It is a + unified access layer supporting NFS or Ceph/RADOS-based storage. Current + Pithos installations over NFS can be migrated seamlessly to Pithos over + Archipelago over NFS, please see the Synnefo upgrade notes (FIXME) + and the Archipelago Administrator's Guide for more details. + Cyclades VMs continue to run over all Ganeti-supported disk templates, + including DRBD, LVM, Ceph/RBD, and Archipelago. +* Various bug fixes and performance improvements. Please see the Synnefo + `Changelog` for a complete list. + + +Cyclades +-------- + +* Storage: Major improvements in VM storage handling. + This release introduces a complete implementation of the OpenStack Block Storage + (Cinder) API v2.0, with distinct /volumes, /snapshots/, /types API endpoints. +* Storage: You can now add and remove virtual disks to and from VMs, even when + they are powered on, with hot-(un)plugging. +* Storage: You can now snapshot running VMs to files on Pithos, provided + their storage is backed by Archipelago. New snapshots appear in Pithos and + are manage-able as Pithos files. +* Storage: You can now spawn new VMs from pre-existing snapshots, regardless + of their disk template, e.g., you can use an existing snapshot to spawn a + DRBD VM. If the new VM is to be based on Archipelago, its creation is a thin + clone and completes in seconds. + PLEASE NOTE: Snapshot support is a work-in-progress, and Archipelago does not + yet perform garbage collection on snapshot deletion. This will be fixed in + Synnefo 0.17. +* Console: Support noVNC-based console for an HTML5-based UI to server consoles + over VNC, with HTTPS encryption. This removes the need for a working Java Runtime + Environment in the client's browser. +* Console: Support multiple console types, including VNC over raw TCP sockets + and VNC over Websockets, with optional encryption. +* Projects: Supports arbitrary (re-)assignment of Cyclades resources to + individual projects. +* Admin: Support end-to-end checking of the Ganeti-to-Synnefo update path, + including all intermediate daemons and the Message Queue. +* Numerous bug fixes, performance improvements and improved usability in + management commands. + + +Pithos +------ + +* Pithos has been refactored to use Archipelago as its single unified storage + backend. Individual storage types (e.g., NFS, or RADOS) are handled by + Archipelago itself. +* Snapshots: Snapshots created from Cyclades VMs on Archipelago are now + presented as files on Pithos and may be downloaded via the Pithos UI. + + .. _NEWS-0.15.2: v0.15.2 @@ -145,7 +227,7 @@ Synnefo-wide .. _NEWS-0.14.10: v0.14.10 -======= +======== Released: Tue Nov 26 11:03:37 EET 2013 @@ -247,7 +329,7 @@ Synnefo-wide * Branding customization support across synnefo frontend components: - * ability to adapt the Astakos, Pithos and Cyclades Web UI to a company’s + * ability to adapt the Astakos, Pithos and Cyclades Web UI to a company's visual identity. This is possible using the snf-branding component, which is automatically installed on the nodes running the API servers for Astakos, Pithos and Cyclades. diff --git a/README.md b/README.md index 200fc99bd12d3d98e00f1ff90cf3ca7d293945b4..8687d628a5f174b15f0340ceae4cbd698643b8ad 100644 --- a/README.md +++ b/README.md @@ -47,36 +47,18 @@ for more information on the Synnefo users and developers lists. Copyright and license ===================== -Copyright (C) 2010-2014 GRNET S.A. All rights reserved. - -Redistribution and use in source and binary forms, with or -without modification, are permitted provided that the following -conditions are met: - - 1. Redistributions of source code must retain the above - copyright notice, this list of conditions and the following - disclaimer. - - 2. Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials - provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A. OR -CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. - -The views and conclusions contained in the software and -documentation are those of the authors and should not be -interpreted as representing official policies, either expressed -or implied, of GRNET S.A. +Copyright (C) 2010-2014 GRNET S.A. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. diff --git a/astakosclient/MANIFEST.in b/astakosclient/MANIFEST.in index 6106c5d89680afa76e234fbcf1feb50c52b9a5f6..ce7f614828c2d727bb93e8692c231749d45fdd5a 100644 --- a/astakosclient/MANIFEST.in +++ b/astakosclient/MANIFEST.in @@ -1 +1 @@ -include distribute_setup.py +include distribute_setup.py README.md diff --git a/astakosclient/README.md b/astakosclient/README.md new file mode 100644 index 0000000000000000000000000000000000000000..e08280b238b80f09ecfb032dd7cb541b45137c18 --- /dev/null +++ b/astakosclient/README.md @@ -0,0 +1,27 @@ +astakosclient +============= + +Overview +-------- + +This is Synnefo's astakosclient component. Please see the [official Synnefo +site](http://www.synnefo.org) for more information. + + +Copyright and license +===================== + +Copyright (C) 2010-2014 GRNET S.A. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. diff --git a/astakosclient/astakosclient/__init__.py b/astakosclient/astakosclient/__init__.py index 759e91afdeb218263b978d8cfd79ec41153aa062..161be1103d56561fa7b0e29baba11430345e3d57 100644 --- a/astakosclient/astakosclient/__init__.py +++ b/astakosclient/astakosclient/__init__.py @@ -1,35 +1,17 @@ -# Copyright (C) 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. """ Simple and minimal client for the Astakos authentication service @@ -42,12 +24,18 @@ import hashlib from base64 import b64encode from copy import copy -import simplejson +try: + import simplejson as json +except ImportError: + import json + from astakosclient.utils import \ - retry_dec, scheme_to_class, parse_request, check_input, join_urls + retry_dec, scheme_to_class, parse_request, check_input, join_urls, \ + render_overlimit_exception from astakosclient.errors import \ AstakosClientException, Unauthorized, BadRequest, NotFound, Forbidden, \ - NoUserName, NoUUID, BadValue, QuotaLimit, InvalidResponse, NoEndpoints + NoUserName, NoUUID, BadValue, QuotaLimit, InvalidResponse, NoEndpoints, \ + ConnectionError # -------------------------------------------------------------------- @@ -213,6 +201,10 @@ class AstakosClient(object): def api_service_quotas(self): return join_urls(self.account_prefix, "service_quotas") + @property + def api_service_project_quotas(self): + return join_urls(self.account_prefix, "service_project_quotas") + @property def api_commissions(self): return join_urls(self.account_prefix, "commissions") @@ -229,10 +221,6 @@ class AstakosClient(object): def api_projects(self): return join_urls(self.account_prefix, "projects") - @property - def api_applications(self): - return join_urls(self.api_projects, "apps") - @property def api_memberships(self): return join_urls(self.api_projects, "memberships") @@ -258,7 +246,7 @@ class AstakosClient(object): hashed_token.update(self.token) self.logger.debug( "Make a %s request to %s, using token with hash %s, " - "with headers %s and body %s", + "with headers %r and body %r", method, request_path, hashed_token.hexdigest(), headers, body if log_body else "(not logged)") @@ -300,11 +288,11 @@ class AstakosClient(object): self.log_response = dict( status=status, message=message, data=data) except Exception as err: - self.logger.error("Failed to send request: %s" % repr(err)) - raise AstakosClientException(str(err)) + self.logger.error("Failed to send request: %r", err) + raise ConnectionError(err) # Return - self.logger.debug("Request returned with status %s" % status) + self.logger.debug("Request returned with status %s", status) if status == 400: raise BadRequest(message, data) elif status == 401: @@ -314,17 +302,18 @@ class AstakosClient(object): elif status == 404: raise NotFound(message, data) elif status < 200 or status >= 300: - raise AstakosClientException(message, data, status) + raise AstakosClientException( + message=message, status=status, response=data) try: if data: - return simplejson.loads(unicode(data)) + return json.loads(unicode(data)) else: return None except Exception as err: - msg = "Cannot parse response \"%s\" with simplejson: %s" + msg = "Cannot parse response \"%r\" with simplejson: %s" self.logger.error(msg % (data, str(err))) - raise InvalidResponse(str(err), data) + raise InvalidResponse(message=str(err), response=data) # ---------------------------------- # do a POST to ``API_USERCATALOGS`` (or ``API_SERVICE_USERCATALOGS``) @@ -338,10 +327,10 @@ class AstakosClient(object): if "uuid_catalog" in data: return data.get("uuid_catalog") else: - msg = "_uuid_catalog request returned %s. No uuid_catalog found" \ + msg = "_uuid_catalog request returned %r. No uuid_catalog found" \ % data self.logger.error(msg) - raise AstakosClientException(msg) + raise AstakosClientException(message=msg, response=data) def get_usernames(self, uuids): """Return a uuid_catalog dictionary for the given uuids @@ -389,10 +378,10 @@ class AstakosClient(object): if "displayname_catalog" in data: return data.get("displayname_catalog") else: - msg = "_displayname_catalog request returned %s. " \ + msg = "_displayname_catalog request returned %r. " \ "No displayname_catalog found" % data self.logger.error(msg) - raise AstakosClientException(msg) + raise AstakosClientException(message=msg, response=data) def get_uuids(self, display_names): """Return a displayname_catalog for the given names @@ -540,27 +529,62 @@ class AstakosClient(object): """ return self._call_astakos(self.api_quotas) + def _join_if_list(self, val): + return ','.join(map(str, val)) if isinstance(val, list) else val + # ---------------------------------- # do a GET to ``API_SERVICE_QUOTAS`` - def service_get_quotas(self, user=None): + def service_get_quotas(self, user=None, project_id=None, project=None): """Get all quotas for resources associated with the service Keyword arguments: - user -- optionally, the uuid of a specific user + user -- optionally, the uuid of a specific user, or a list thereof + project_id -- optionally, the uuid of a specific project, or a list + thereof + project -- backwards compatibility (replaced by "project_id") In case of success return a dict of dicts of dicts with current quotas for all users, or of a specified user, if user argument is set. Otherwise raise an AstakosClientException """ + project_id = project if project_id is None else project_id query = self.api_service_quotas + filters = {} if user is not None: - query += "?user=" + user + filters['user'] = self._join_if_list(user) + if project_id is not None: + filters['project'] = self._join_if_list(project_id) + if filters: + query += "?" + urllib.urlencode(filters) + return self._call_astakos(query) + + # ---------------------------------- + # do a GET to ``API_SERVICE_PROJECT_QUOTAS`` + def service_get_project_quotas(self, project_id=None, project=None): + """Get all project quotas for resources associated with the service + + Keyword arguments: + project -- optionally, the uuid of a specific project, or a list + thereof + + In case of success return a dict of dicts with current quotas + for all projects, or of a specified project, if project argument is + set. Otherwise raise an AstakosClientException + + """ + project_id = project if project_id is None else project_id + query = self.api_service_project_quotas + filters = {} + if project_id is not None: + filters['project'] = self._join_if_list(project_id) + if filters: + query += "?" + urllib.urlencode(filters) return self._call_astakos(query) # ---------------------------------- # do a POST to ``API_COMMISSIONS`` - def issue_commission(self, request): + def _issue_commission(self, request): """Issue a commission Keyword arguments: @@ -579,54 +603,135 @@ class AstakosClient(object): method="POST") except AstakosClientException as err: if err.status == 413: - raise QuotaLimit(err.message, err.details) + try: + msg, details = render_overlimit_exception( + err.response, self.logger) + except Exception as perr: + self.logger.error("issue_commission request returned '413'" + " but response '%r' could not be parsed:" + " %s", err.response, str(perr)) + msg, details = err.message, "" + raise QuotaLimit(message=msg, + details=details, + response=err.response) else: raise if "serial" in response: return response['serial'] else: - msg = "issue_commission_core request returned %s. " + \ + msg = "issue_commission_core request returned %r. " + \ "No serial found" % response self.logger.error(msg) - raise AstakosClientException(msg) + raise AstakosClientException(message=msg, response=response) + + def _mk_user_provision(self, holder, source, resource, quantity): + holder = "user:" + holder + source = "project:" + source + return {"holder": holder, "source": source, + "resource": resource, "quantity": quantity} + + def _mk_project_provision(self, holder, resource, quantity): + holder = "project:" + holder + return {"holder": holder, "source": None, + "resource": resource, "quantity": quantity} - def issue_one_commission(self, holder, source, provisions, + def mk_provisions(self, holder, source, resource, quantity): + return [self._mk_user_provision(holder, source, resource, quantity), + self._mk_project_provision(source, resource, quantity)] + + def issue_commission_generic(self, user_provisions, project_provisions, + name="", force=False, auto_accept=False): + """Issue commission (for multiple holder/source pairs) + + keyword arguments: + user_provisions -- dict mapping user holdings + (user, project, resource) to integer quantities + project_provisions -- dict mapping project holdings + (project, resource) to integer quantities + name -- description of the commission (string) + force -- force this commission (boolean) + auto_accept -- auto accept this commission (boolean) + + In case of success return commission's id (int). + Otherwise raise an AstakosClientException. + + """ + request = {} + request["force"] = force + request["auto_accept"] = auto_accept + request["name"] = name + try: + request["provisions"] = [] + for (holder, source, resource), quantity in \ + user_provisions.iteritems(): + p = self._mk_user_provision(holder, source, resource, quantity) + request["provisions"].append(p) + for (holder, resource), quantity in project_provisions.iteritems(): + p = self._mk_project_provision(holder, resource, quantity) + request["provisions"].append(p) + except Exception as err: + self.logger.error(str(err)) + raise BadValue(str(err)) + + return self._issue_commission(request) + + def issue_one_commission(self, holder, provisions, name="", force=False, auto_accept=False): """Issue one commission (with specific holder and source) keyword arguments: holder -- user's id (string) - source -- commission's source (ex system) (string) - provisions -- resources with their quantity (dict from string to int) + provisions -- (source, resource) mapping to quantity name -- description of the commission (string) force -- force this commission (boolean) auto_accept -- auto accept this commission (boolean) In case of success return commission's id (int). Otherwise raise an AstakosClientException. - (See also issue_commission) """ check_input("issue_one_commission", self.logger, - holder=holder, source=source, - provisions=provisions) + holder=holder, provisions=provisions) + + request = {} + request["force"] = force + request["auto_accept"] = auto_accept + request["name"] = name + try: + request["provisions"] = [] + for (source, resource), quantity in provisions.iteritems(): + ps = self.mk_provisions(holder, source, resource, quantity) + request["provisions"].extend(ps) + except Exception as err: + self.logger.error(str(err)) + raise BadValue(str(err)) + + return self._issue_commission(request) + + def issue_resource_reassignment(self, holder, provisions, name="", + force=False, auto_accept=False): + """Change resource assignment to another project + """ request = {} request["force"] = force request["auto_accept"] = auto_accept request["name"] = name + try: request["provisions"] = [] - for resource, quantity in provisions.iteritems(): - prov = {"holder": holder, "source": source, - "resource": resource, "quantity": quantity} - request["provisions"].append(prov) + for key, quantity in provisions.iteritems(): + (from_source, to_source, resource) = key + ps = self.mk_provisions( + holder, from_source, resource, -quantity) + ps += self.mk_provisions(holder, to_source, resource, quantity) + request["provisions"].extend(ps) except Exception as err: self.logger.error(str(err)) raise BadValue(str(err)) - return self.issue_commission(request) + return self._issue_commission(request) # ---------------------------------- # do a GET to ``API_COMMISSIONS`` @@ -713,13 +818,15 @@ class AstakosClient(object): # ---------------------------- # do a GET to ``API_PROJECTS`` - def get_projects(self, name=None, state=None, owner=None): + def get_projects(self, name=None, state=None, owner=None, mode=None): """Retrieve all accessible projects Arguments: name -- filter by name (optional) state -- filter by state (optional) owner -- filter by owner (optional) + mode -- if value is 'member', return only active projects in which + the request user is an active member In case of success, return a list of project descriptions. """ @@ -730,11 +837,13 @@ class AstakosClient(object): filters["state"] = state if owner is not None: filters["owner"] = owner + if mode is not None: + filters["mode"] = mode + path = self.api_projects + if filters: + path += "?" + urllib.urlencode(filters) req_headers = {'content-type': 'application/json'} - req_body = (parse_request({"filter": filters}, self.logger) - if filters else None) - return self._call_astakos(self.api_projects, - headers=req_headers, body=req_body) + return self._call_astakos(path, headers=req_headers) # ----------------------------------------- # do a GET to ``API_PROJECTS``/<project_id> @@ -766,7 +875,7 @@ class AstakosClient(object): method="POST") # ------------------------------------------ - # do a POST to ``API_PROJECTS``/<project_id> + # do a PUT to ``API_PROJECTS``/<project_id> def modify_project(self, project_id, specs): """Submit application to modify an existing project @@ -780,7 +889,7 @@ class AstakosClient(object): req_headers = {'content-type': 'application/json'} req_body = parse_request(specs, self.logger) return self._call_astakos(path, headers=req_headers, - body=req_body, method="POST") + body=req_body, method="PUT") # ------------------------------------------------- # do a POST to ``API_PROJECTS``/<project_id>/action @@ -798,74 +907,53 @@ class AstakosClient(object): path = join_urls(self.api_projects, str(project_id)) path = join_urls(path, "action") req_headers = {'content-type': 'application/json'} - req_body = parse_request({action: reason}, self.logger) + req_body = parse_request({action: {"reason": reason}}, self.logger) return self._call_astakos(path, headers=req_headers, body=req_body, method="POST") - # -------------------------------- - # do a GET to ``API_APPLICATIONS`` - def get_applications(self, project=None): - """Retrieve all accessible applications - - Arguments: - project -- filter by project (optional) - - In case of success, return a list of application descriptions. - """ - req_headers = {'content-type': 'application/json'} - body = {"project": project} if project is not None else None - req_body = parse_request(body, self.logger) if body else None - return self._call_astakos(self.api_applications, - headers=req_headers, body=req_body) - - # ----------------------------------------- - # do a GET to ``API_APPLICATIONS``/<app_id> - def get_application(self, app_id): - """Retrieve application description, if accessible - - Arguments: - app_id -- application identifier - - In case of success, return application description. - """ - path = join_urls(self.api_applications, str(app_id)) - return self._call_astakos(path) - # ------------------------------------------------- - # do a POST to ``API_APPLICATIONS``/<app_id>/action - def application_action(self, app_id, action, reason=""): - """Perform action on an application + # do a POST to ``API_PROJECTS``/<project_id>/action + def application_action(self, project_id, app_id, action, reason=""): + """Perform action on a project application Arguments: - app_id -- application identifier - action -- action to perform, one of "approve", "deny", - "dismiss", "cancel" - reason -- reason of performing the action + project_id -- project identifier + app_id -- application identifier + action -- action to perform, one of "approve", "deny", + "dismiss", "cancel" + reason -- reason of performing the action In case of success, return nothing. """ - path = join_urls(self.api_applications, str(app_id)) + path = join_urls(self.api_projects, str(project_id)) path = join_urls(path, "action") req_headers = {'content-type': 'application/json'} - req_body = parse_request({action: reason}, self.logger) + req_body = parse_request({action: { + "reasons": reason, + "app_id": app_id}}, self.logger) return self._call_astakos(path, headers=req_headers, body=req_body, method="POST") # ------------------------------- # do a GET to ``API_MEMBERSHIPS`` - def get_memberships(self, project=None): + def get_memberships(self, project_id=None, project=None): """Retrieve all accessible memberships Arguments: - project -- filter by project (optional) + project_id -- filter by project (optional) + project -- backwards compatibility In case of success, return a list of membership descriptions. """ + project_id = project if project_id is None else project_id req_headers = {'content-type': 'application/json'} - body = {"project": project} if project is not None else None - req_body = parse_request(body, self.logger) if body else None - return self._call_astakos(self.api_memberships, - headers=req_headers, body=req_body) + filters = {} + if project_id is not None: + filters["project"] = project_id + path = self.api_memberships + if filters: + path += '?' + urllib.urlencode(filters) + return self._call_astakos(path, headers=req_headers) # ----------------------------------------- # do a GET to ``API_MEMBERSHIPS``/<memb_id> diff --git a/astakosclient/astakosclient/errors.py b/astakosclient/astakosclient/errors.py index a85d38da02da784637ea4316565f10a1e45fe83f..fdfd9fc8b41d3355b5e3a4ccc7f355f0a61d07b3 100644 --- a/astakosclient/astakosclient/errors.py +++ b/astakosclient/astakosclient/errors.py @@ -1,35 +1,17 @@ -# Copyright (C) 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. """ Astakos Client Exceptions @@ -38,15 +20,25 @@ Astakos Client Exceptions class AstakosClientException(Exception): """Base AstakosClientException Class""" - def __init__(self, message='', details='', status=500): + def __init__(self, message='', details='', status=500, + response=None, errobject=None): self.message = message self.details = details + self.errobject = errobject + self.response = response if not hasattr(self, 'status'): self.status = status super(AstakosClientException, self).__init__(self.message, self.details, self.status) +class ConnectionError(AstakosClientException): + """Connection failed""" + def __init__(self, errobject): + super(ConnectionError, self).__init__( + message=str(errobject), errobject=errobject) + + class BadValue(AstakosClientException): """Re-define ValueError Exception under AstakosClientException""" def __init__(self, details): @@ -56,8 +48,6 @@ class BadValue(AstakosClientException): class InvalidResponse(AstakosClientException): """Return simplejson parse Exception as AstakosClient one""" - def __init__(self, message, details): - super(InvalidResponse, self).__init__(message, details) class BadRequest(AstakosClientException): diff --git a/astakosclient/astakosclient/tests.py b/astakosclient/astakosclient/tests.py index c67b6e1df7480ca29243aadb166b0bae46231f17..d327cb098ada0fa5d40f35fb72884f7a2a0dc864 100644 --- a/astakosclient/astakosclient/tests.py +++ b/astakosclient/astakosclient/tests.py @@ -1,37 +1,19 @@ #!/usr/bin/env python # -# Copyright (C) 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. """Unit Tests for the astakos-client module @@ -42,7 +24,11 @@ the astakos client library import re import sys -import simplejson + +try: + import simplejson as json +except ImportError: + import json import astakosclient from astakosclient import AstakosClient @@ -289,10 +275,10 @@ def _req_tokens(conn, method, url, **kwargs): if 'body' in kwargs: # Return endpoints with authenticate info - return ("", simplejson.dumps(endpoints_with_info), 200) + return ("", json.dumps(endpoints_with_info), 200) else: # Return endpoints without authenticate info - return ("", simplejson.dumps(endpoints), 200) + return ("", json.dumps(endpoints), 200) def _req_catalogs(conn, method, url, **kwargs): @@ -309,7 +295,7 @@ def _req_catalogs(conn, method, url, **kwargs): return _request_status_401(conn, method, url, **kwargs) # Return - body = simplejson.loads(kwargs['body']) + body = json.loads(kwargs['body']) if 'uuids' in body: # Return uuid_catalog uuids = body['uuids'] @@ -326,7 +312,7 @@ def _req_catalogs(conn, method, url, **kwargs): return_catalog = {"displayname_catalog": catalogs, "uuid_catalog": {}} else: return_catalog = {"displayname_catalog": {}, "uuid_catalog": {}} - return ("", simplejson.dumps(return_catalog), 200) + return ("", json.dumps(return_catalog), 200) def _req_resources(conn, method, url, **kwargs): @@ -340,7 +326,7 @@ def _req_resources(conn, method, url, **kwargs): return _request_status_400(conn, method, url, **kwargs) # Return - return ("", simplejson.dumps(resources), 200) + return ("", json.dumps(resources), 200) def _req_quotas(conn, method, url, **kwargs): @@ -357,7 +343,7 @@ def _req_quotas(conn, method, url, **kwargs): return _request_status_401(conn, method, url, **kwargs) # Return - return ("", simplejson.dumps(quotas), 200) + return ("", json.dumps(quotas), 200) def _req_commission(conn, method, url, **kwargs): @@ -375,22 +361,22 @@ def _req_commission(conn, method, url, **kwargs): if method == "POST": if 'body' not in kwargs: return _request_status_400(conn, method, url, **kwargs) - body = simplejson.loads(unicode(kwargs['body'])) + body = json.loads(unicode(kwargs['body'])) if re.match('/?'+api_commissions+'$', url) is not None: # Issue Commission # Check if we have enough resources to give if body['provisions'][1]['quantity'] > 420000000: - return ("", simplejson.dumps(commission_failure_response), 413) + return ("", json.dumps(commission_failure_response), 413) else: return \ - ("", simplejson.dumps(commission_successful_response), 200) + ("", json.dumps(commission_successful_response), 200) else: # Issue commission action serial = url.split('/')[3] if serial == "action": # Resolve multiple actions if body == resolve_commissions_req: - return ("", simplejson.dumps(resolve_commissions_rep), 200) + return ("", json.dumps(resolve_commissions_rep), 200) else: return _request_status_400(conn, method, url, **kwargs) else: @@ -406,12 +392,12 @@ def _req_commission(conn, method, url, **kwargs): elif method == "GET": if re.match('/?'+api_commissions+'$', url) is not None: # Return pending commission - return ("", simplejson.dumps(pending_commissions), 200) + return ("", json.dumps(pending_commissions), 200) else: # Return commissions's description serial = re.sub('/?' + api_commissions, '', url)[1:] if serial == str(57): - return ("", simplejson.dumps(commission_description), 200) + return ("", json.dumps(commission_description), 200) else: return _request_status_404(conn, method, url, **kwargs) else: @@ -781,7 +767,7 @@ class TestCommissions(unittest.TestCase): global auth_url try: client = AstakosClient(token['id'], auth_url) - response = client.issue_commission(commission_request) + response = client._issue_commission(commission_request) except Exception as err: self.fail("Shouldn't raise Exception %s" % err) self.assertEqual(response, commission_successful_response['serial']) @@ -795,7 +781,7 @@ class TestCommissions(unittest.TestCase): new_request['provisions'][1]['quantity'] = 520000000 try: client = AstakosClient(token['id'], auth_url) - client.issue_commission(new_request) + client._issue_commission(new_request) except QuotaLimit: pass except Exception as err: @@ -811,7 +797,8 @@ class TestCommissions(unittest.TestCase): client = AstakosClient(token['id'], auth_url) response = client.issue_one_commission( "c02f315b-7d84-45bc-a383-552a3f97d2ad", - "system", {"cyclades.vm": 1, "cyclades.ram": 30000}) + {("system", "cyclades.vm"): 1, + ("system", "cyclades.ram"): 30000}) except Exception as err: self.fail("Shouldn't have raised Exception %s" % err) self.assertEqual(response, commission_successful_response['serial']) diff --git a/astakosclient/astakosclient/utils.py b/astakosclient/astakosclient/utils.py index 70cd87e82a81e405b58f917a651ec6c92874fed3..f2eb4e58385c0fb5aea052386e18024a47199f91 100644 --- a/astakosclient/astakosclient/utils.py +++ b/astakosclient/astakosclient/utils.py @@ -1,35 +1,17 @@ -# Copyright (C) 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. """ Astakos Client utility module @@ -38,10 +20,14 @@ Astakos Client utility module from httplib import HTTPConnection, HTTPSConnection from contextlib import closing -import simplejson from objpool.http import PooledHTTPConnection from astakosclient.errors import AstakosClientException, BadValue +try: + import simplejson as json +except ImportError: + import json + def retry_dec(func): """Class Method Decorator""" @@ -99,7 +85,7 @@ def scheme_to_class(scheme, use_pool, pool_size): def parse_request(request, logger): """Parse request with simplejson to convert it to string""" try: - return simplejson.dumps(request) + return json.dumps(request) except Exception as err: msg = "Cannot parse request \"%s\" with simplejson: %s" \ % (request, str(err)) @@ -120,3 +106,35 @@ def check_input(function_name, logger, **kwargs): def join_urls(url_a, url_b): """Join_urls from synnefo.lib""" return url_a.rstrip("/") + "/" + url_b.lstrip("/") + + +def render_overlimit_exception(response, logger): + """Render a human readable message for QuotaLimit Exception""" + resource_name = { + "cyclades.disk": "Disk", + "cyclades.vm": "Virtual Machine", + "cyclades.cpu": "CPU", + "cyclades.ram": "RAM", + "cyclades.floating_ip": "Floating IP address", + "cyclades.network.private": "Private Network", + "pithos.diskspace": "Storage space", + "astakos.pending_app": "Pending Applications" + } + response = json.loads(response) + data = response['overLimit']['data'] + usage = data["usage"] + limit = data["limit"] + available = limit - usage + provision = data['provision'] + requested = provision['quantity'] + resource = provision['resource'] + try: + resource = resource_name[resource] + except KeyError: + logger.error("Unknown resource name '%s'", resource) + + msg = "Resource Limit Exceeded for your account." + details = "Limit for resource '%s' exceeded for your account."\ + " Available: %s, Requested: %s"\ + % (resource, available, requested) + return msg, details diff --git a/astakosclient/docs/index.rst b/astakosclient/docs/index.rst index cdf9aea732629ba4fe43bfcc5dd0f22d04d22d9f..1b7b5d81a2844f1aad78739d10a718af553d351e 100644 --- a/astakosclient/docs/index.rst +++ b/astakosclient/docs/index.rst @@ -133,15 +133,26 @@ retry=0, use_pool=False, pool_size=8, logger=None\ **)** It returns user's current quotas (as dict of dicts). In case of error it raises an AstakosClientException exception. - **service_get_quotas(**\ user=None\ **)** + **service_get_quotas(**\ user=None, project_id=None\ **)** It returns all users' current quotas for the resources associated with the service (as dict of dicts of dicts). Optionally, one can query the - quotas of a specific user with argument user=UUID. In case of error it - raises an AstakosClientException exception. + quotas of a specific user with argument user=UUID (or a list of UUID). + Likewise one can specify a project (or a list of projects). In case of + error it raises an AstakosClientException exception. + + **service_get_project_quotas(**\ project_id=None\ **)** + It returns all projects' current quotas for the resources + associated with the service (as dict of dicts). + Optionally, one can query the quotas of a specific project with + argument project_id=UUID (or a list of UUID). In case of error it raises an + AstakosClientException exception. - **issue_commission(**\ request\ **)** - Issue a commission. In case of success it returns commission's id - (int). Otherwise it raises an AstakosClientException exception. + **issue_commission_generic(**\ user_provisions, project_provisions, name="", force=False, auto_accept=False\ **)** + Issue a commission. User provisions are specified as a dict from + (user, project, resource) to int; project provisions as a dict from + (project, resource) to int. + In case of success return commission's id (int). + Otherwise raise an AstakosClientException exception. **issue_one_commission(**\ holder, source, provisions, name="", force=False, auto_accept=False\ **)** Issue a commission. We have to specify the holder, the source and the @@ -150,6 +161,9 @@ retry=0, use_pool=False, pool_size=8, logger=None\ **)** commission's id (int). Otherwise it raises an AstakosClientException exception. + **issue_resource_reassignment(**\ holder, provisions, name="", force=False, auto_accept=False\ **)** + Change resource assignment to another project + **get_pending_commissions()** It returns the pending commissions (list of integers). In case of error it raises an AstakosClientException exception. @@ -176,7 +190,7 @@ retry=0, use_pool=False, pool_size=8, logger=None\ **)** rejected and which failed to resolved. Otherwise raise an AstakosClientException exception. - **get_projects(**\ name=None, state=None, owner=None\ **)** + **get_projects(**\ name=None, state=None, owner=None, mode=None\ **)** Retrieve all accessible projects **get_project(**\ project_id\ **)** @@ -191,16 +205,10 @@ retry=0, use_pool=False, pool_size=8, logger=None\ **)** **project_action(**\ project_id, action, reason=""\ **)** Perform action on a project - **get_applications(**\ project=None\ **)** - Retrieve all accessible applications - - **get_application(**\ app_id\ **)** - Retrieve application description, if accessible - - **application_action(**\ app_id, action, reason=""\ **)** - Perform action on an application + **application_action(**\ project_id, app_id, action, reason=""\ **)** + Perform action on a project application - **get_memberships(**\ project=None\ **)** + **get_memberships(**\ project_id=None\ **)** Retrieve all accessible memberships **get_membership(**\ memb_id\ **)** diff --git a/astakosclient/setup.py b/astakosclient/setup.py index ab2425038adc8831ba8d21568cf268504fed4bb3..d1fd8752b8d43a1686b12048e5cfe867f6df450a 100644 --- a/astakosclient/setup.py +++ b/astakosclient/setup.py @@ -1,35 +1,17 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # import distribute_setup distribute_setup.use_setuptools() @@ -60,9 +42,12 @@ CLASSIFIERS = [] # Package requirements INSTALL_REQUIRES = [ "objpool>=0.3", - "simplejson" ] +EXTRAS_REQUIRES = { + 'SimpleJSON': ['simplejson'], +} + # Provided as an attribute, so you can append to these instead # of replicating them: standard_exclude = ["*.py", "*.pyc", "*$py.class", "*~", ".*", "*.bak"] @@ -159,7 +144,7 @@ def find_package_data( setup( name='astakosclient', version=VERSION, - license='BSD', + license='GNU GPLv3', url='http://www.synnefo.org/', description=SHORT_DESCRIPTION, classifiers=CLASSIFIERS, @@ -176,6 +161,7 @@ setup( zip_safe=False, install_requires=INSTALL_REQUIRES, + extras_require=EXTRAS_REQUIRES, tests_require=['mock'], entry_points={}, diff --git a/ci/ci_squeeze.conf b/ci/ci_squeeze.conf deleted file mode 100644 index 72f0a4d32c5702c11b937eed9f6006cfdb7975c4..0000000000000000000000000000000000000000 --- a/ci/ci_squeeze.conf +++ /dev/null @@ -1,83 +0,0 @@ -[Global] -# Timeouts in seconds -build_timeout = 240 -# Apt repository to use -apt_repo = - deb http://apt.dev.grnet.gr squeeze/ - deb http://packages.x2go.org/debian squeeze main - -# Synnefo git repo. -# If not set, snf-ci will copy and use the local repo. -synnefo_repo = https://code.grnet.gr/git/synnefo -# Git branch to test (specify sha1 or branch name). If not set, the -# branch/sha will result from the current repository. -synnefo_branch = - -# pithos-web-client git repo -pithos_webclient_repo = https://code.grnet.gr/git/pithos-web-client -# Git branch to use for pithos-web-client -# If not set, snf-ci will decide which one to use -pithos_webclient_branch = - -# Defines the schema that snf-deploy will use -schema = one_node_squeeze -# Local dir to save builded packages -pkgs_dir = /tmp/synnefo_pkgs -# If True patch the pydist.py module (see Debian bug #657665) -patch_pydist = True - -# Configuration of git (on remote server) -git_config_name = Buildbot -git_config_mail = synnefo@builder.dev.grnet.gr - -# Network address from which we allow access to server. -# If not set, access to server is not restricted. -accept_ssh_from = -# Config file to save temporary options (eg IPs, passwords etc) -temporary_config = /tmp/ci_temp_conf -# File to save the x2goplugin html file -x2go_plugin_file = /tmp/x2go.html - - -[Deployment] -# Choose the 'cloud' to use from .kamakirc -kamaki_cloud = -# Server name to use for our machine -server_name = Synnefo_CI -# A list of flavors (comma seperated) to choose from -# The user can specify a flavor name (reg expression) -# with "name:" or a flavor id with "id:". -flavors = name:C2R2...D20ext_.*, name:C2R2...D20drbd, id:1 -# A list of images (comma seperated) to choose from -# The user can specify an image name (reg expression) -# with "name:" or an image id with "id:". -images = name:SynnefoCISqueeze.*, name:Debian Base \(OldStable\), id:72d9844f-1024-4a07-a3c3-60d650b8f5cd -# File containing the ssh keys to upload/install to server -# If not set, no ssh keys will be installed -ssh_keys = ~/.ssh/id_rsa.pub - - -[Burnin] -# Maybe add some burnin options -# (e.g. tests to run/ignore, timeouts etc) -cmd_options = --images "name:.*" --flavors "name:C1R512D2file" --no-ipv6 - - -[Unit Tests] -component = astakos cyclades pithos astakosclient - - -[Repository] -# Projects reside on this repo -projects = - snf-common - astakosclient - snf-django-lib - snf-webproject - snf-branding - snf-astakos-app - snf-pithos-backend - snf-cyclades-gtools - snf-cyclades-app - snf-pithos-app - snf-tools diff --git a/ci/ci_wheezy.conf b/ci/ci_wheezy.conf index c3b8fb0275c0d82543fe44dd215eb1ff5da7c54d..9d3a27b0fc320450e0e53f57cb99216f220b64b9 100644 --- a/ci/ci_wheezy.conf +++ b/ci/ci_wheezy.conf @@ -8,11 +8,12 @@ apt_repo = # Synnefo git repo. # If not set, snf-ci will copy and use the local repo. -synnefo_repo = https://code.grnet.gr/git/synnefo +synnefo_repo = https://github.com/grnet/synnefo # Git branch to test (specify sha1 or branch name). If not set, the # branch/sha will result from the current repository. synnefo_branch = +build_pithos_webclient = True # pithos-web-client git repo pithos_webclient_repo = https://code.grnet.gr/git/pithos-web-client # Git branch to use for pithos-web-client @@ -35,6 +36,8 @@ git_config_mail = synnefo@builder.dev.grnet.gr accept_ssh_from = # Config file to save temporary options (eg IPs, passwords etc) temporary_config = /tmp/ci_temp_conf +# Install x2go and firefox +setup_x2go = True # File to save the x2goplugin html file x2go_plugin_file = /tmp/x2go.html @@ -47,7 +50,7 @@ server_name = Synnefo_CI # A list of flavors (comma seperated) to choose from # The user can specify a flavor name (reg expression) # with "name:" or a flavor id with "id:". -flavors = name:C2R2...D20ext_.*, name:C2R2...D20drbd, id:1 +flavors = name:C2R4...D20ext_.*, name:C2R4...D20drbd, id:1 # A list of images (comma seperated) to choose from # The user can specify an image name (reg expression) # with "name:" or an image id with "id:". @@ -59,16 +62,19 @@ ssh_keys = ~/.ssh/id_rsa.pub allocate_floating_ip = True # List of networks IDs (comma seperated) to connect server private_networks = +# Connect to a specific ssh port. If not set, the ssh port is calculated +# automatically. +ssh_port = [Burnin] # Maybe add some burnin options # (e.g. tests to run/ignore, timeouts etc) -cmd_options = --images "name:.*" --flavors "name:C1R512D2file" --no-ipv6 +cmd_options = --images "name:.*" --flavors "name:C1R512D2\D.*" --no-ipv6 [Unit Tests] -component = astakos cyclades pithos astakosclient +component = astakos cyclades pithos astakosclient admin [Repository] @@ -83,5 +89,6 @@ projects = snf-pithos-backend snf-cyclades-gtools snf-cyclades-app + snf-admin-app snf-pithos-app snf-tools diff --git a/ci/config b/ci/config index 4987150f584c2ddcd8562895c63929e9f76fc823..240ccd36f646d64fba6f07fb5a0443b2d2e4e851 100644 --- a/ci/config +++ b/ci/config @@ -9,4 +9,5 @@ PROJECTS="\ snf-cyclades-gtools\ snf-cyclades-app\ snf-pithos-app\ + snf-admin-app\ snf-tools" diff --git a/ci/develop-common.sh b/ci/develop-common.sh new file mode 100755 index 0000000000000000000000000000000000000000..31e4801e3f4f037630016569b279fb472d592fc3 --- /dev/null +++ b/ci/develop-common.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +if [ -n "$VIRTUAL_ENV" ]; then + OPTIONS=--script-dir=$VIRTUAL_ENV/bin/ + echo $OPTIONS +else + OPTIONS= +fi + +. ./ci/config diff --git a/ci/install.sh b/ci/install.sh index e949a541e6485f089e51585375fb528cedf9e1e1..ccf0be7ec2479f72548fea3ff51cbfecbee58823 100755 --- a/ci/install.sh +++ b/ci/install.sh @@ -1,17 +1,12 @@ #!/bin/sh -if [ -n "$VIRTUAL_ENV" ]; then - OPTIONS=--script-dir=$VIRTUAL_ENV/bin/ - echo $OPTIONS -else - OPTIONS= -fi - +# `cd` to the top dir of synnefo repository set -e cwd=`dirname $0` cd "$cwd"/.. -. ./ci/config +# Do common tasks for install/uninstall purposes +. ./ci/develop-common.sh # Update version devflow-update-version diff --git a/ci/schemas/one_node_squeeze/deploy.conf b/ci/schemas/one_node_squeeze/deploy.conf deleted file mode 100644 index c0fb53ed1f00871db3abb172fdb81c4602d013eb..0000000000000000000000000000000000000000 --- a/ci/schemas/one_node_squeeze/deploy.conf +++ /dev/null @@ -1,41 +0,0 @@ -[packages] -# whether to use apt-get or local generated package found in packages dir -use_local_packages = True - -# url to obtain latest synnefo packages. -# To use them change USE_LOCAL_PACKAGES setting to yes -# To get them run: snf-deploy packages -package_url = http://builder.dev.grnet.gr/synnefo/packages/Squeeze/40/ - -[dirs] -# dir to find all template files used to customize setup -# in case you want to add another setting please modify the corresponding file -templates = /var/lib/snf-deploy/files -# dir to store local images (disk0, disk1 of the virtual cluster) -images = /var/lib/snf-deploy/images -# dir to store/find local packages -# dir to locally save packages that will be downloaded from package_url -# put here any locally created packages (useful for development) -packages = /var/lib/snf-deploy/packages -# dir to store pidfiles (dnsmasq, kvm) -run = /var/run/snf-deploy -# dir to store dnsmasq related files -dns = /var/lib/snf-deploy/dnsmasq -# dir to lookup fabfile and ifup script -lib = /usr/lib/snf-deploy -# dir to store executed commands (to enforce sequential execution) -cmd = /var/run/snf-deploy/cmd -# dir to be used by Django for file-based mail backend -mail_dir = /var/tmp/synnefo-mails - -[keys] -# whether to create new keys -keygen = False -# whether to inject ssh keys found in templates/root/.ssh in nodes -key_inject = True - -[options] -# Deploy Synnefo, specially tuned for testing. This option improves the speed -# of some operations, but is not safe for all enviroments. (e.g. disable -# fsync of postgresql) -testing_vm = True diff --git a/ci/schemas/one_node_squeeze/ganeti.conf b/ci/schemas/one_node_squeeze/ganeti.conf deleted file mode 100644 index d3507ed21856d2c2e5f4e9b58904d90ed21aaf6a..0000000000000000000000000000000000000000 --- a/ci/schemas/one_node_squeeze/ganeti.conf +++ /dev/null @@ -1,18 +0,0 @@ -[ganeti1] -cluster_nodes = node1 -master_node = node1 - -cluster_netdev = eth0 -cluster_name = ganeti1 -cluster_ip = 192.168.0.13 - -vg = autovg - -synnefo_public_network_subnet = 10.2.1.0/24 -synnefo_public_network_gateway = 10.2.1.1 -synnefo_public_network_type = CUSTOM - -image_dir = /srv/okeanos - -# To add another cluster repeat the above section -# with different header and nodes diff --git a/ci/schemas/one_node_squeeze/nodes.conf b/ci/schemas/one_node_squeeze/nodes.conf deleted file mode 100644 index c4d1a66e61918b01c37cd0ba5271f2bffd465edf..0000000000000000000000000000000000000000 --- a/ci/schemas/one_node_squeeze/nodes.conf +++ /dev/null @@ -1,42 +0,0 @@ -# please note that currently is only supported deployment -# with nodes (both ganeti and synnefo) residing in the same subnet/domain -[network] -domain = synnefo.live - -[os] -node1 = squeeze -# node2 = wheezy - -[hostnames] -node1 = auto1 -# node2 = auto2 - -[ips] -node1 = 192.168.0.1 -# node2 = 192.168.0.2 - -# This is used only in case of vcluster -# needed to pass the correct dhcp responces to the virtual nodes -[macs] -node1 = 52:54:00:00:00:01 -# node2 = 52:54:00:00:00:02 - -[info] -# Here we define which nodes from the predefined ones to use -nodes = node1 - -# login credentials for the nodes -# please note that in case of vcluster these are preconfigured -# and not editable. -# in case of physical nodes all nodes should have the same login account -user = root -password = 12345 - -public_iface = eth0 -vm_public_iface = eth1 -vm_private_iface = eth2 - -# extra disk name inside the nodes -# if defined, snf-deploy will create a VG for ganeti in order to support lvm storage -# if not then only file disk template will be supported -extra_disk = /dev/vdb diff --git a/ci/schemas/one_node_squeeze/squeeze.conf b/ci/schemas/one_node_squeeze/squeeze.conf deleted file mode 100644 index b29389a1c0c9f3d90b1e428a1d80e099db65a5cf..0000000000000000000000000000000000000000 --- a/ci/schemas/one_node_squeeze/squeeze.conf +++ /dev/null @@ -1,56 +0,0 @@ -[debian] -rabbitmq-server = squeeze-backports -gunicorn = squeeze-backports -qemu-kvm = squeeze-backports -qemu = squeeze-backports -python-gevent = squeeze-backports -apache2 = -postgresql = -python-psycopg2 = -python-argparse = -nfs-kernel-server = squeeze-backports -nfs-common = squeeze-backports -bind9 = -vlan = -vlan = -lvm2 = -curl = -memcached = -python-memcache = -bridge-utils = -python-progress = -ganeti-instance-debootstrap = -python-django-south = squeeze-backports -python-django = squeeze-backports -drbd8-utils = - - -[synnefo] -snf-astakos-app = squeeze -snf-common = squeeze -snf-cyclades-app = squeeze -snf-cyclades-gtools = squeeze -snf-django-lib = squeeze -python-astakosclient = squeeze -snf-branding = squeeze -snf-webproject = squeeze -snf-pithos-app = squeeze -snf-pithos-backend = squeeze -snf-tools = squeeze - - -[ganeti] -snf-ganeti = 2.6.2+ippool11+hotplug5+extstorage3+rbdfix1+kvmfix2+nolvm+netxen-1~squeeze -ganeti-htools = 2.6.2+ippool11+hotplug5+extstorage3+rbdfix1+kvmfix2+nolvm+netxen-1~squeeze - -[other] -snf-cloudcms = squeeze -snf-vncauthproxy = squeeze -snf-pithos-webclient = squeeze -snf-image = squeeze -snf-network = squeeze -python-objpool = squeeze -nfdhcpd = squeeze -kamaki = squeeze -python-bitarray = squeeze-backports -nfqueue-bindings-python = 0.3+physindev-1 diff --git a/ci/schemas/one_node_squeeze/synnefo.conf b/ci/schemas/one_node_squeeze/synnefo.conf deleted file mode 100644 index e0ac23ded0d6203e6b07bbca42e3354892031807..0000000000000000000000000000000000000000 --- a/ci/schemas/one_node_squeeze/synnefo.conf +++ /dev/null @@ -1,37 +0,0 @@ -[cred] -synnefo_user = synnefo -synnefo_db_passwd = example_passw0rd -synnefo_rapi_passwd = example_rapi_passw0rd -synnefo_rabbitmq_passwd = example_rabbitmq_passw0rd -user_email = user@synnefo.org -user_name = John -user_lastname = Doe -user_passwd = 12345 - - -[roles] -accounts = node1 -compute = node1 -object-store = node1 -cyclades = node1 -pithos = node1 -cms = node1 -db = node1 -mq = node1 -ns = node1 -client = node1 -router = node1 - - -[synnefo] -pithos_dir = /srv/pithos -flavor_cpu = 1,2,4,8 -flavor_ram = 128,256,512,1024,2048,4096,8192 -flavor_disk = 2,5,10,20,40,60,80,100 -flavor_storage = file - -vm_public_bridge = br0 -vm_private_bridge = prv0 -common_bridge = br0 - -debian_base_url = http://cdn.synnefo.org/debian_base-7.0-x86_64.diskdump diff --git a/ci/schemas/one_node_squeeze/vcluster.conf b/ci/schemas/one_node_squeeze/vcluster.conf deleted file mode 100644 index 377b2a9c7ec3784813a962d9a7035db0bcf29cf1..0000000000000000000000000000000000000000 --- a/ci/schemas/one_node_squeeze/vcluster.conf +++ /dev/null @@ -1,40 +0,0 @@ -[image] -# url to get the base image. This is a debian base image with preconfigured -# root password and installed rsa/dsa keys. Plus a NetworkManager hook that -# changes the VM's name based on info provided by dhcp response. -# To create it run: snf-deploy image -squeeze_image_url = https://pithos.okeanos.grnet.gr/public/832xv -ubuntu_image_url = - -# in order ganeti nodes to support lvm storage (plain disk template) it will -# be needed an extra disk to eventually be able to create a VG. Ganeti requires -# this VG to be at least of 30GB. To this end in order the virtual nodes to have -# this extra disk an image should be created locally. There are three options: -# 1. not create an extra disk (only file storage template will be supported) -# 2. create an image of 30G in image dir (default /var/lib/snf-deploy/images) -# using dd if=/dev/zero of=squeeze.disk1 -# 3. create this image in a local VG using lvgreate -L30G squeeze.disk1 lvg -# and create a symbolic link in /var/lib/snf-deploy/images - -# Whether to create an extra disk or not -create_extra_disk = False -# lvg is the name of the local VG if any -lvg = - -# OS istalled in the virtual cluster -os = squeeze - - -[cluster] -# the bridge to use for the virtual cluster -# on this bridge we will launch a dnsnmasq and provide -# fqdns needed to the cluster. -# In ordrer cluster nodes to have internet access, host must do NAT. -# iptables -t nat -A POSTROUTING -s 192.0.0.0/28 -j MASQUERADE -# ip addr add 192.0.0.14/28 dev auto_nodes_br -# To create run: snf-deploy cluster -bridge = auto_nodes_br - -[network] -subnet = 192.168.0.0/28 -gateway = 192.168.0.14 diff --git a/ci/schemas/one_node_squeeze/wheezy.conf b/ci/schemas/one_node_squeeze/wheezy.conf deleted file mode 100644 index 940c981ab245738de6b73ddd8a51dcbdfcf2f085..0000000000000000000000000000000000000000 --- a/ci/schemas/one_node_squeeze/wheezy.conf +++ /dev/null @@ -1,55 +0,0 @@ -[debian] -rabbitmq-server = -gunicorn = -qemu-kvm = -qemu = -python-gevent = -apache2 = -postgresql = -python-psycopg2 = -python-argparse = -nfs-kernel-server = -nfs-common = -bind9 = -vlan = -vlan = -lvm2 = -curl = -memcached = -python-memcache = -bridge-utils = -python-progress = -ganeti-instance-debootstrap = -python-django-south = -python-django = -drbd8-utils = - - -[synnefo] -snf-astakos-app = wheezy -snf-common = wheezy -snf-cyclades-app = wheezy -snf-cyclades-gtools = wheezy -snf-django-lib = wheezy -python-astakosclient = wheezy -snf-branding = wheezy -snf-webproject = wheezy -snf-pithos-app = wheezy -snf-pithos-backend = wheezy -snf-tools = wheezy - - -[ganeti] -snf-ganeti = 2.8.0+hotplug+ippoolfix-1 -ganeti-htools = 2.8.0+hotplug+ippoolfix-1 -[other] -snf-cloudcms = wheezy -snf-vncauthproxy = wheezy -snf-pithos-webclient = wheezy -snf-image = wheezy -snf-network = wheezy -python-objpool = wheezy -nfdhcpd = wheezy -kamaki = wheezy -python-bitarray = wheezy -nfqueue-bindings-python = 0.3+physindev-1~wheezy diff --git a/ci/schemas/one_node_wheezy/deploy.conf b/ci/schemas/one_node_wheezy/deploy.conf index c0fb53ed1f00871db3abb172fdb81c4602d013eb..952ef4991601ba6ab4b5d764040fa930ada6e04a 100644 --- a/ci/schemas/one_node_wheezy/deploy.conf +++ b/ci/schemas/one_node_wheezy/deploy.conf @@ -1,40 +1,34 @@ -[packages] +[DEFAULT] # whether to use apt-get or local generated package found in packages dir use_local_packages = True # url to obtain latest synnefo packages. -# To use them change USE_LOCAL_PACKAGES setting to yes # To get them run: snf-deploy packages package_url = http://builder.dev.grnet.gr/synnefo/packages/Squeeze/40/ -[dirs] +# dir to store snf-deploy status +state_dir = /var/lib/snf-deploy # dir to find all template files used to customize setup # in case you want to add another setting please modify the corresponding file -templates = /var/lib/snf-deploy/files -# dir to store local images (disk0, disk1 of the virtual cluster) -images = /var/lib/snf-deploy/images +template_dir = /var/lib/snf-deploy/files +# dir to store disks for the virtual cluster) +vcluster_dir = /var/lib/snf-deploy/vcluster # dir to store/find local packages # dir to locally save packages that will be downloaded from package_url # put here any locally created packages (useful for development) -packages = /var/lib/snf-deploy/packages +package_dir = /var/lib/snf-deploy/packages # dir to store pidfiles (dnsmasq, kvm) -run = /var/run/snf-deploy +run_dir = /var/run/snf-deploy # dir to store dnsmasq related files -dns = /var/lib/snf-deploy/dnsmasq +dns_dir = /var/lib/snf-deploy/dnsmasq # dir to lookup fabfile and ifup script -lib = /usr/lib/snf-deploy -# dir to store executed commands (to enforce sequential execution) -cmd = /var/run/snf-deploy/cmd +lib_dir = /usr/lib/snf-deploy # dir to be used by Django for file-based mail backend mail_dir = /var/tmp/synnefo-mails -[keys] -# whether to create new keys -keygen = False # whether to inject ssh keys found in templates/root/.ssh in nodes key_inject = True -[options] # Deploy Synnefo, specially tuned for testing. This option improves the speed # of some operations, but is not safe for all enviroments. (e.g. disable # fsync of postgresql) diff --git a/ci/schemas/one_node_wheezy/ganeti.conf b/ci/schemas/one_node_wheezy/ganeti.conf index d3507ed21856d2c2e5f4e9b58904d90ed21aaf6a..f3ca8bddc250f676e6902dfd65414d95c04686de 100644 --- a/ci/schemas/one_node_wheezy/ganeti.conf +++ b/ci/schemas/one_node_wheezy/ganeti.conf @@ -1,18 +1,27 @@ -[ganeti1] -cluster_nodes = node1 -master_node = node1 +[DEFAULT] +vg = ganeti +# Ganeti has hard requiremend for VG larger than 20480M +vg_size = 30G +# whether to add synnefo related packages +synnefo = True -cluster_netdev = eth0 -cluster_name = ganeti1 -cluster_ip = 192.168.0.13 +[ganeti] +name = ganeti +domain = synnefo.live +ip = 10.1.2.101 +netdev = eth0 -vg = autovg -synnefo_public_network_subnet = 10.2.1.0/24 -synnefo_public_network_gateway = 10.2.1.1 -synnefo_public_network_type = CUSTOM +[ganeti-qa] +name = ganeti +domain = qa.synnefo.live +ip = 10.1.2.101 +netdev = eth0 +synnefo = -image_dir = /srv/okeanos -# To add another cluster repeat the above section -# with different header and nodes +[ganeti-vc] +name = ganeti +domain = vcluster.synnefo.live +ip = 10.1.2.101 +netdev = eth0 diff --git a/ci/schemas/one_node_wheezy/nodes.conf b/ci/schemas/one_node_wheezy/nodes.conf index 27f60f0d28a643819cafae19624e941e856073ed..6fb814e10e5e5d34810ae6fd1d2a6948cf161c99 100644 --- a/ci/schemas/one_node_wheezy/nodes.conf +++ b/ci/schemas/one_node_wheezy/nodes.conf @@ -1,42 +1,104 @@ -# please note that currently is only supported deployment -# with nodes (both ganeti and synnefo) residing in the same subnet/domain -[network] +# In this section we define configuration setting common to all nodes +[DEFAULT] +# Currently both ganeti and synnefo must reside in the same domain +# Instances will reside in the .vm.<domain> subdomain domain = synnefo.live -[os] -node1 = wheezy -# node2 = wheezy +# Each node should define: -[hostnames] -node1 = auto1 -# node2 = auto2 - -[ips] -node1 = 192.168.0.1 -# node2 = 192.168.0.2 +# The node's desired hostname. It will be set +hostname = +# The node's primary IP +ip = # This is used only in case of vcluster # needed to pass the correct dhcp responces to the virtual nodes -[macs] -node1 = 52:54:00:00:00:01 -# node2 = 52:54:00:00:00:02 - -[info] -# Here we define which nodes from the predefined ones to use -nodes = node1 - -# login credentials for the nodes -# please note that in case of vcluster these are preconfigured -# and not editable. -# in case of physical nodes all nodes should have the same login account +mac = + +# The node's OS (debian, ubuntu, etc) +# Currently tested only under debian (wheezy) +os = debian + +# The node's administrator account (with root priviledges) user = root -password = 12345 +# The root's password +password = +# The interface with internet access public_iface = eth0 +# The interface for the instances' public traffic vm_public_iface = eth1 +# The interface for the instances' private traffic vm_private_iface = eth2 -# extra disk name inside the nodes -# if defined, snf-deploy will create a VG for ganeti in order to support lvm storage -# if not then only file disk template will be supported +# The extra disk for the Ganeti VG needed for plain and drbd disk templates extra_disk = /dev/vdb + +################### +# synnefo/ci node # +################### + +[node] +name = node +ip = 192.0.2.1 +extra_disk = + +############ +# qa nodes # +############ + +[dev] +name = qa2 +ip = 10.1.2.10 +public_iface = eth1 +domain = qa.synnefo.live + +[qa1] +name = qa1 +ip = 10.1.2.11 +public_iface = eth1 +domain = qa.synnefo.live + +[qa2] +name = qa2 +ip = 10.1.2.12 +public_iface = eth1 +domain = qa.synnefo.live + +############ +# vc nodes # +############ + +[vc1] +mac = 52:54:00:00:00:01 +name = vc1 +ip = 10.1.2.1 +public_iface = eth0 +domain = vcluster.synnefo.live + +[vc2] +mac = 52:54:00:00:00:02 +name = vc2 +ip = 10.1.2.2 +public_iface = eth0 +domain = vcluster.synnefo.live + +[vc3] +mac = 52:54:00:00:00:03 +name = vc3 +ip = 10.1.2.3 +public_iface = eth0 +domain = vcluster.synnefo.live + +[vc4] +mac = 52:54:00:00:00:04 +name = vc4 +ip = 10.1.2.4 +public_iface = eth0 +domain = vcluster.synnefo.live + +[dummy] +name = dummy +ip = 1.2.3.4 +public_iface = eth0 +domain = synnefo.live diff --git a/ci/schemas/one_node_wheezy/packages.conf b/ci/schemas/one_node_wheezy/packages.conf new file mode 100644 index 0000000000000000000000000000000000000000..41b2b22f1d0252f5d5a399adb25841907b492bdb --- /dev/null +++ b/ci/schemas/one_node_wheezy/packages.conf @@ -0,0 +1,9 @@ +[debian] +python-nfqueue = 0.4+physindev-1~wheezy +python-scapy = 2.2.0+rfc6355-1 +snf-ganeti = unstable +ganeti2 = unstable +python-django-eztables = 0.3.3-1~snf~0.2 +qemu-kvm = wheezy-backports + +[ubuntu] diff --git a/ci/schemas/one_node_wheezy/setups.conf b/ci/schemas/one_node_wheezy/setups.conf new file mode 100644 index 0000000000000000000000000000000000000000..ecf78432c2b8de130586f1d5a1dbbb4fe98f2b2c --- /dev/null +++ b/ci/schemas/one_node_wheezy/setups.conf @@ -0,0 +1,79 @@ +[DEFAULT] + +################################# +# snf-deploy synnefo --autoconf # +################################# + +[auto] +ns = node +ca = node +client = node +router = node +nfs = node +db = node +mq = node +astakos = node +cyclades = node +admin = node +vnc = node +pithos = node +cms = node +stats = node +dev = node +clusters = + ganeti + + +[ganeti] +master = node +vmc = + node + +################################### +# snf-deploy ganeti-qa --setup qa # +################################### + +[qa] +ns = dev +client = dev +router = qa1 +nfs = dev +dev = dev +clusters = + ganeti-qa + + +[ganeti-qa] +master = qa1 +vmc = + qa1 + qa2 + +################################## +# snf-deploy vcluster --setup vc # +################################## + +[vc] +ns = vc1 +client = vc4 +router = vc1 +nfs = vc1 +db = vc2 +mq = vc3 +astakos = vc1 +cyclades = vc2 +pithos = vc3 +cms = vc4 +stats = vc1 +dev = vc1 +clusters = + ganeti-vc + + +[ganeti-vc] +master = vc1 +vmc = + vc1 + vc2 + vc3 + vc4 diff --git a/ci/schemas/one_node_wheezy/squeeze.conf b/ci/schemas/one_node_wheezy/squeeze.conf deleted file mode 100644 index 4b302abab1b375c0f3125efe9765f58d5db75695..0000000000000000000000000000000000000000 --- a/ci/schemas/one_node_wheezy/squeeze.conf +++ /dev/null @@ -1,55 +0,0 @@ -[debian] -rabbitmq-server = squeeze-backports -gunicorn = squeeze-backports -qemu-kvm = squeeze-backports -qemu = squeeze-backports -python-gevent = squeeze-backports -apache2 = -postgresql = -python-psycopg2 = -python-argparse = -nfs-kernel-server = squeeze-backports -nfs-common = squeeze-backports -bind9 = -vlan = -vlan = -lvm2 = -curl = -memcached = -python-memcache = -bridge-utils = -python-progress = -ganeti-instance-debootstrap = -python-django-south = squeeze-backports -drbd8-utils = - - -[synnefo] -snf-astakos-app = squeeze -snf-common = squeeze -snf-cyclades-app = squeeze -snf-cyclades-gtools = squeeze -snf-django-lib = squeeze -python-astakosclient = squeeze -snf-branding = squeeze -snf-webproject = squeeze -snf-pithos-app = squeeze -snf-pithos-backend = squeeze -snf-tools = squeeze - - -[ganeti] -snf-ganeti = 2.6.2+ippool11+hotplug5+extstorage3+rbdfix1+kvmfix2+nolvm+netxen-1~squeeze -ganeti-htools = 2.6.2+ippool11+hotplug5+extstorage3+rbdfix1+kvmfix2+nolvm+netxen-1~squeeze - -[other] -snf-cloudcms = squeeze -snf-vncauthproxy = squeeze -snf-pithos-webclient = squeeze -snf-image = squeeze -snf-network = squeeze -python-objpool = squeeze -nfdhcpd = squeeze -kamaki = squeeze -python-bitarray = squeeze-backports -nfqueue-bindings-python = 0.3+physindev-1 diff --git a/ci/schemas/one_node_wheezy/synnefo.conf b/ci/schemas/one_node_wheezy/synnefo.conf index bbcb7bb80a5b6ec09e98ecdb39a75a3d696d89db..e5cad0755835b39768ef5f827a9982d670814cef 100644 --- a/ci/schemas/one_node_wheezy/synnefo.conf +++ b/ci/schemas/one_node_wheezy/synnefo.conf @@ -1,39 +1,39 @@ -[cred] +[DEFAULT] +# various credentials synnefo_user = synnefo synnefo_db_passwd = example_passw0rd synnefo_rapi_passwd = example_rapi_passw0rd synnefo_rabbitmq_passwd = example_rabbitmq_passw0rd +synnefo_vnc_passwd = example_vnc_passw0rd +cyclades_secret = example_cyclades_secret +oa2_secret = example_oa2_secret +webproject_secret = example_webproject_secret +stats_secret = example_stats_secret +collectd_secret = example_collectd_secret + user_email = user@synnefo.org user_name = John user_lastname = Doe user_passwd = 12345 -oa2_secret = 12345 - -[roles] -accounts = node1 -compute = node1 -object-store = node1 -cyclades = node1 -pithos = node1 -cms = node1 -db = node1 -mq = node1 -ns = node1 -client = node1 -router = node1 -stats = node1 +# one common shared dir for nfs +shared_dir = /srv - -[synnefo] -pithos_dir = /srv/pithos flavor_cpu = 1,2,4,8 flavor_ram = 128,256,512,1024,2048,4096,8192 flavor_disk = 2,5,10,20,40,60,80,100 -flavor_storage = file +flavor_storage = file,ext_archipelago,ext_shared-filer + +# url to download debian wheezy image +debian_base_url = http://cdn.synnefo.org/debian_base-7.0-x86_64.diskdump + +# archipelago segment +segment_size = 512 +# options related to synnefo networks vm_public_bridge = br0 vm_private_bridge = prv0 common_bridge = br0 - -debian_base_url = http://cdn.synnefo.org/debian_base-7.0-x86_64.diskdump +synnefo_public_network_subnet = 10.2.1.0/24 +synnefo_public_network_gateway = 10.2.1.1 +synnefo_public_network_type = CUSTOM diff --git a/ci/schemas/one_node_wheezy/vcluster.conf b/ci/schemas/one_node_wheezy/vcluster.conf index 377b2a9c7ec3784813a962d9a7035db0bcf29cf1..52995cd4cf899e4a10efdc213fd280837af1bd8a 100644 --- a/ci/schemas/one_node_wheezy/vcluster.conf +++ b/ci/schemas/one_node_wheezy/vcluster.conf @@ -1,31 +1,7 @@ -[image] -# url to get the base image. This is a debian base image with preconfigured -# root password and installed rsa/dsa keys. Plus a NetworkManager hook that -# changes the VM's name based on info provided by dhcp response. -# To create it run: snf-deploy image -squeeze_image_url = https://pithos.okeanos.grnet.gr/public/832xv -ubuntu_image_url = +[DEFAULT] +disk0_size = 10G +disk1_size = 30G -# in order ganeti nodes to support lvm storage (plain disk template) it will -# be needed an extra disk to eventually be able to create a VG. Ganeti requires -# this VG to be at least of 30GB. To this end in order the virtual nodes to have -# this extra disk an image should be created locally. There are three options: -# 1. not create an extra disk (only file storage template will be supported) -# 2. create an image of 30G in image dir (default /var/lib/snf-deploy/images) -# using dd if=/dev/zero of=squeeze.disk1 -# 3. create this image in a local VG using lvgreate -L30G squeeze.disk1 lvg -# and create a symbolic link in /var/lib/snf-deploy/images - -# Whether to create an extra disk or not -create_extra_disk = False -# lvg is the name of the local VG if any -lvg = - -# OS istalled in the virtual cluster -os = squeeze - - -[cluster] # the bridge to use for the virtual cluster # on this bridge we will launch a dnsnmasq and provide # fqdns needed to the cluster. @@ -33,8 +9,7 @@ os = squeeze # iptables -t nat -A POSTROUTING -s 192.0.0.0/28 -j MASQUERADE # ip addr add 192.0.0.14/28 dev auto_nodes_br # To create run: snf-deploy cluster -bridge = auto_nodes_br +bridge = vcluster_bridge -[network] -subnet = 192.168.0.0/28 -gateway = 192.168.0.14 +subnet = 10.1.2.0/24 +gateway = 10.1.2.254 diff --git a/ci/schemas/one_node_wheezy/wheezy.conf b/ci/schemas/one_node_wheezy/wheezy.conf deleted file mode 100644 index b80b9e34b2e2ef8b9a79bb3aaa66a97ae06cf308..0000000000000000000000000000000000000000 --- a/ci/schemas/one_node_wheezy/wheezy.conf +++ /dev/null @@ -1,60 +0,0 @@ -[debian] -rabbitmq-server = -gunicorn = -qemu-kvm = -qemu = -python-gevent = -apache2 = -postgresql = -python-psycopg2 = -python-argparse = -nfs-kernel-server = -nfs-common = -bind9 = -vlan = -vlan = -lvm2 = -curl = -memcached = -python-memcache = -bridge-utils = -python-progress = -ganeti-instance-debootstrap = -python-django-south = -python-django = -drbd8-utils = -collectd = - - -[synnefo] -snf-astakos-app = wheezy -snf-common = wheezy -snf-cyclades-app = wheezy -snf-cyclades-gtools = wheezy -snf-django-lib = wheezy -python-astakosclient = wheezy -snf-branding = wheezy -snf-webproject = wheezy -snf-pithos-app = wheezy -snf-pithos-backend = wheezy -snf-tools = wheezy -snf-stats-app = wheezy - - -[ganeti] -snf-ganeti = wheezy -ganeti-htools = wheezy -ganeti-haskell = wheezy - - -[other] -snf-cloudcms = wheezy -snf-vncauthproxy = unstable -snf-pithos-webclient = wheezy -snf-image = wheezy -snf-network = wheezy -python-objpool = wheezy -nfdhcpd = wheezy -kamaki = wheezy -python-bitarray = wheezy -python-nfqueue = 0.4+physindev-1~wheezy diff --git a/ci/snf-ci b/ci/snf-ci index a46b0b1df0b7772db95ef40745b1a151b0083861..b94e294c5bc1b9ff6606f96b4326ab34dd873b75 100755 --- a/ci/snf-ci +++ b/ci/snf-ci @@ -1,4 +1,19 @@ #!/usr/bin/env python +# +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # Invalid name for type module. pylint: disable-msg=C0103 @@ -7,6 +22,7 @@ Continuous Integration script for Synnefo. """ import os +import sys import utils from optparse import OptionParser @@ -18,8 +34,11 @@ DEPLOY_SYNNEFO_CMD = "deploy" TEST_SYNNEFO_CMD = "test" RUN_BURNIN_CMD = "burnin" CREATE_X2GO_FILE = "x2goplugin" +SHELL_CONNECT = "shell" ALL_CMDS = "all" +DEFAULT_CONFIG_FILE = os.path.expanduser("~/.synnefo_ci") + COMMANDS_IN_ALL_MODE = [ CREATE_SERVER_CMD, BUILD_SYNNEFO_CMD, @@ -32,6 +51,7 @@ COMMANDS_IN_ALL_MODE = [ AVAILABLE_COMMANDS = [ CREATE_X2GO_FILE, DELETE_SERVER_CMD, + SHELL_CONNECT, ] + COMMANDS_IN_ALL_MODE USAGE = """usage: %%prog [options] command[,command...] @@ -45,6 +65,7 @@ command: * %s: Run snf-burnin in the deployed Synnefo * %s: Create x2go plugin file * %s: Delete the slave server + * %s: Connect to the server using ssh * %s: Run all the available commands """ % tuple([CREATE_SERVER_CMD, @@ -55,16 +76,25 @@ command: RUN_BURNIN_CMD, CREATE_X2GO_FILE, DELETE_SERVER_CMD, + SHELL_CONNECT, ALL_CMDS]) -def main(): # Too many branches. pylint: disable-msg=R0912 +def main(): # pylint: disable=too-many-statements, too-many-branches """Parse command line options and run the specified actions""" parser = OptionParser(usage=USAGE) - parser.add_option("-c", "--conf", dest="config_file", default=None, - help="Configuration file for SynnefoCI script") + parser.add_option("-c", "--conf", dest="config_file", + default=DEFAULT_CONFIG_FILE, + help="Configuration file for SynnefoCI" + " script (Default: %s)" % DEFAULT_CONFIG_FILE) parser.add_option("--cloud", dest="kamaki_cloud", default=None, help="Use specified cloud, as is in .kamakirc") + parser.add_option("--synnefo-repo", dest="synnefo_repo", default=None, + help="Specify the synnefo repo to use") + parser.add_option("--synnefo-branch", dest="synnefo_branch", default=None, + help="Specify the synnefo branch to use") + parser.add_option("--pull-request", dest="pull_request", default=None, + help="Test a Github pull request.") parser.add_option("-f", "--flavor", dest="flavor", default=None, help="Flavor to use for the server." " Supports both search by name (reg expression)" @@ -79,7 +109,7 @@ def main(): # Too many branches. pylint: disable-msg=R0912 help="Upload/Install the public ssh keys contained" " in this file to the server") parser.add_option("--name", dest="server_name", default=None, - help=""), + help="") parser.add_option("-n", "--build-id", dest="build_id", default=None, type="int", help="Specify a number to use to identify this build." @@ -108,6 +138,9 @@ def main(): # Too many branches. pylint: disable-msg=R0912 parser.add_option("--no-colors", dest="use_colors", default=True, action="store_false", help="Don't use colorful output messages.") + parser.add_option("--ignore-ssl", "-k", dest="ignore_ssl", + default=None, action="store_true", + help="Don't verify SSL certificates.") (options, args) = parser.parse_args() @@ -133,14 +166,28 @@ def main(): # Too many branches. pylint: disable-msg=R0912 parser.print_help() print print msg - return + sys.exit(1) else: setattr(options, command, True) + if options.pull_request is not None: + if (options.synnefo_repo is not None or + options.synnefo_branch is not None): + print "ERROR: Options 'synnefo_repo' and/or 'synnefo_branch'" \ + " cannot be given with 'pull_request'" + sys.exit(1) + # ---------------------------------- # Initialize SynnefoCi utils.USE_COLORS = options.use_colors - synnefo_ci = utils.SynnefoCI(config_file=options.config_file, + utils.IGNORE_SSL = options.ignore_ssl + config_file = options.config_file + if config_file is not None: + config_file = os.path.expanduser(config_file) + if not os.path.exists(config_file): + print "Configuration file '%s' does not exist!" % config_file + config_file = None + synnefo_ci = utils.SynnefoCI(config_file=config_file, build_id=options.build_id, cloud=options.kamaki_cloud) @@ -151,7 +198,11 @@ def main(): # Too many branches. pylint: disable-msg=R0912 image=options.image, ssh_keys=options.ssh_keys, server_name=options.server_name) - synnefo_ci.clone_repo(local_repo=options.local_repo) + synnefo_ci.clone_repo( + synnefo_repo=options.synnefo_repo, + synnefo_branch=options.synnefo_branch, + local_repo=options.local_repo, + pull_request=options.pull_request) if getattr(options, BUILD_SYNNEFO_CMD, False): synnefo_ci.build_packages() if options.fetch_packages: @@ -172,6 +223,8 @@ def main(): # Too many branches. pylint: disable-msg=R0912 synnefo_ci.x2go_plugin(options.x2go_output) if getattr(options, DELETE_SERVER_CMD, False): synnefo_ci.destroy_server() + if getattr(options, SHELL_CONNECT, False): + synnefo_ci.shell_connect() if __name__ == "__main__": diff --git a/ci/tests.sh b/ci/tests.sh index 908746ed71b352a00ce704da0509284870b5ad71..7849911130b4d7b1ef85903e744cc0b1b7094b5d 100755 --- a/ci/tests.sh +++ b/ci/tests.sh @@ -1,47 +1,200 @@ #!/bin/sh set -e -SNF_MANAGE=$(which snf-manage) || - { echo "Cannot find snf-manage in $PATH" 1>&2; exit 1; } +runAstakosTests () { + if [ -z "$astakos_tests" ]; then return; fi -runTest () { + export SYNNEFO_EXCLUDE_PACKAGES="snf-cyclades-app:snf-admin-app:snf-pithos-app" + CURRENT_COMPONENT=astakos + createSnfManageTest $astakos_tests + runTest +} + +runCycladesTests () { + if [ -z "$cyclades_tests" ]; then return; fi + + export SYNNEFO_EXCLUDE_PACKAGES="snf-pithos-app:snf-astakos-app:snf-admin-app" + CURRENT_COMPONENT=synnefo + createSnfManageTest $cyclades_tests + runTest +} + +runAdminTests () { + if [ -z "$admin_tests" ]; then return; fi + + export SYNNEFO_EXCLUDE_PACKAGES="snf-pithos-app" + CURRENT_COMPONENT=synnefo_admin + createSnfManageTest $admin_tests + runTest +} + +runPithosTests () { + if [ -z "$pithos_tests" ]; then return; fi + + export SYNNEFO_EXCLUDE_PACKAGES="snf-cyclades-app:snf-astakos-app:snf-admin-app" + CURRENT_COMPONENT=pithos + createSnfManageTest $pithos_tests + runTest +} + +runAstakosclientTests () { + if [ -z "$astakosclient_tests" ]; then return; fi + + CURRENT_COMPONENT=astakosclient + for test in $astakosclient_tests; do + createNoseTest $test + runTest + done +} + +createSnfManageTest () { TEST="$SNF_MANAGE test $* --traceback --noinput --settings=synnefo.settings.test" +} - runCoverage "$TEST" +createNoseTest () { + TEST="$NOSE $*" } -runCoverage () { - if coverage >/dev/null 2>&1; then - coverage run $1 - coverage report --include=snf-* +runTest () { + if [ $COVERAGE_EXISTS ]; then + runCoverage "$TEST" else - echo "WARNING: Cannot find coverage in path, skipping coverage tests" 1>&2 - $1 + # Stop here, if we are on dry run + if [ $DRY_RUN ]; then + echo "$TEST" + return + fi + + eval $TEST fi } +runCoverage () { + # Stop here, if we are on dry run + if [ $DRY_RUN ]; then + echo "coverage run $1" + return + fi + + coverage erase + coverage run $1 + coverage report --include="*${CURRENT_COMPONENT}*" +} + +usage(){ + echo "$1: Wrong input." + echo " Usage: $0 [--dry-run] component[.test]" + exit +} + +# Append a string to a given variable. +# +# Arguments: $1: the variable name +# $2: the string +# Note, the variable must be passed by name, so we need to resort to a bit +# complicated parameter expansions +append () { + eval $(echo "$1=\"\$${1}\"\" \"\"$2\"") +} + +# Check if a string contains a substring +# +# Arguments: $1: The string +# $2: The substring +contains () { + case "$1" in + *$2*) return 0;; # True + *) return 1;; # False + esac +} + +# Get a list of tests for a given component. +# +# Arguments: $1: a component to extract tests from or a single component test +# Returns: $(astakos/cyclades/pithos/ac)_tests, +# a list with apps to be tested for each component +extract_tests () { + # Check all components: + # If the given component matches one of the components: + # If total match, return all the tests of the component. + # Else, if its form matches "component.test", extract only the + # test. + # Anything else is considered wrong input + + for c in $ALL_COMPONENTS; do + if contains $1 $c; then + if [ "$1" = "$c" ]; then + append "${c}_tests" "$(eval "echo \$"${c}"_all_tests")" + return + elif contains $1 "$c."; then + append "${c}_tests" $(echo $1 | sed -e 's/^[a-z]*\.//g') + return + fi + fi + done + + usage $1 +} + export SYNNEFO_SETTINGS_DIR=/tmp/snf-test-settings -ASTAKOS_APPS="im quotaholder_app oa2" -CYCLADES_APPS="api db logic plankton quotas vmapi helpdesk userdata" -PITHOS_APPS="api" +astakos_all_tests="im quotaholder_app oa2" +cyclades_all_tests="api db logic plankton quotas vmapi helpdesk userdata volume" +admin_all_tests="admin" +pithos_all_tests="api" +astakosclient_all_tests="astakosclient" +ALL_COMPONENTS="astakos cyclades admin pithos astakosclient" + +astakos_tests="" +cyclades_tests="" +admin_tests="" +pithos_tests="" +astakosclient_tests="" + +if [ "$1" = "--dry-run" ]; then + DRY_RUN=0 + shift +fi TEST_COMPONENTS="$@" if [ -z "$TEST_COMPONENTS" ]; then - TEST_COMPONENTS="astakos cyclades pithos astakosclient" + TEST_COMPONENTS=$ALL_COMPONENTS +fi + +# Check if coverage and snf-manage exist +if command -v coverage >/dev/null 2>&1; then + COVERAGE_EXISTS=0 fi +SNF_MANAGE=$(which snf-manage) || + { echo "Cannot find snf-manage in $PATH" 1>&2; exit 1; } +NOSE=$(which nosetests) || + { echo "Cannot find nosetests in $PATH" 1>&2; exit 1; } + +# Extract tests from a component for component in $TEST_COMPONENTS; do - if [ "$component" = "astakos" ]; then - runTest $ASTAKOS_APPS - elif [ "$component" = "cyclades" ]; then - export SYNNEFO_EXCLUDE_PACKAGES="snf-pithos-app" - runTest $CYCLADES_APPS - elif [ "$component" = "pithos" ]; then - export SYNNEFO_EXCLUDE_PACKAGES="snf-cyclades-app" - runTest $PITHOS_APPS - elif [ "$component" = "astakosclient" ]; then - TEST="nosetests astakosclient" - runCoverage "$TEST" - fi + extract_tests $component done + +echo "|===============|============================" +echo "| Component | Tests" +echo "|---------------|----------------------------" +echo "| Astakos | $astakos_tests" +echo "| Cyclades | $cyclades_tests" +echo "| Admin | $admin_tests" +echo "| Pithos | $pithos_tests" +echo "| Astakosclient | $astakosclient_tests" +echo "|===============|============================" +echo "" + +if [ ! $COVERAGE_EXISTS ]; then + echo "WARNING: Cannot find coverage in path." >&2 + echo "" +fi + +# For each component, run the specified tests. +runAstakosTests +runCycladesTests +runAdminTests +runPithosTests +runAstakosclientTests diff --git a/ci/uninstall.sh b/ci/uninstall.sh new file mode 100755 index 0000000000000000000000000000000000000000..ee17419fd8b258554f04c1fd27e44ca2ae071f1f --- /dev/null +++ b/ci/uninstall.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# `cd` to the top dir of synnefo repository +set -e +cwd=`dirname $0` +cd "$cwd"/.. + +# Do common tasks for install/uninstall purposes +. ./ci/develop-common.sh + +for project in $PROJECTS; do + cd $project + python setup.py develop --uninstall $OPTIONS + cd - +done diff --git a/ci/utils.py b/ci/utils.py index 2ee0639ef25412b926ecfd1fdb86a3d591f73ee5..1a9c78983ff4e59480282dedbf5b3bf445b342c0 100644 --- a/ci/utils.py +++ b/ci/utils.py @@ -1,4 +1,19 @@ #!/usr/bin/env python +# +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. """ Synnefo ci utils module @@ -8,8 +23,10 @@ import os import re import sys import time +import httplib import logging import fabric.api as fabric +import simplejson as json import subprocess import tempfile from ConfigParser import ConfigParser, DuplicateSectionError @@ -19,12 +36,15 @@ from kamaki.clients.astakos import AstakosClient, parse_endpoints from kamaki.clients.cyclades import CycladesClient, CycladesNetworkClient from kamaki.clients.image import ImageClient from kamaki.clients.compute import ComputeClient +from kamaki.clients.utils import https from kamaki.clients import ClientError import filelocker DEFAULT_CONFIG_FILE = "ci_wheezy.conf" # Is our terminal a colorful one? USE_COLORS = True +# Ignore SSL verification +IGNORE_SSL = False # UUID of owner of system images DEFAULT_SYSTEM_IMAGES_UUID = [ "25ecced9-bf53-4145-91ee-cf47377e9fb2", # production (okeanos.grnet.gr) @@ -77,6 +97,26 @@ def _check_fabric(fun): return wrapper +def _kamaki_ssl(ignore_ssl=None): + """Patch kamaki to use the correct CA certificates + + Read kamaki's config file and decide if we are going to use + CA certificates and patch kamaki clients accordingly. + + """ + config = kamaki_config.Config() + if ignore_ssl is None: + ignore_ssl = config.get("global", "ignore_ssl").lower() == "on" + ca_file = config.get("global", "ca_certs") + + if ignore_ssl: + # Skip SSL verification + https.patch_ignore_ssl() + else: + # Use ca_certs path found in kamakirc + https.patch_with_certs(ca_file) + + def _check_kamaki(fun): """Check if kamaki has been initialized""" def wrapper(self, *args, **kwargs): @@ -185,6 +225,9 @@ class SynnefoCI(object): Setup cyclades_client, image_client and compute_client """ + # Patch kamaki for SSL verification + _kamaki_ssl(ignore_ssl=IGNORE_SSL) + config = kamaki_config.Config() if self.kamaki_cloud is None: try: @@ -198,7 +241,7 @@ class SynnefoCI(object): auth_url = config.get_cloud(self.kamaki_cloud, "url") self.logger.debug("Authentication URL is %s" % _green(auth_url)) token = config.get_cloud(self.kamaki_cloud, "token") - #self.logger.debug("Token is %s" % _green(token)) + # self.logger.debug("Token is %s" % _green(token)) self.astakos_client = AstakosClient(auth_url, token) endpoints = self.astakos_client.authenticate() @@ -262,6 +305,12 @@ class SynnefoCI(object): fip['floating_ip_address']) self.network_client.delete_floatingip(fip['id']) + # pylint: disable= no-self-use + @_check_fabric + def shell_connect(self): + """Open shell to remote server""" + fabric.open_shell("export TERM=xterm") + def _create_floating_ip(self): """Create a new floating ip""" networks = self.network_client.list_networks(detail=True) @@ -272,12 +321,12 @@ class SynnefoCI(object): try: fip = self.network_client.create_floatingip(pub_net['id']) except ClientError as err: - self.logger.warning("%s: %s", err.message, err.details) + self.logger.warning("%s", str(err.message).strip()) continue self.logger.debug("Floating IP %s with id %s created", fip['floating_ip_address'], fip['id']) return fip - self.logger.error("No mor IP addresses available") + self.logger.error("No more IP addresses available") sys.exit(1) def _create_port(self, floating_ip): @@ -322,7 +371,10 @@ class SynnefoCI(object): server_id = server['id'] self.write_temp_config('server_id', server_id) self.logger.debug("Server got id %s" % _green(server_id)) - server_user = server['metadata']['users'] + + # An image may have more than one user. Choose the first one. + server_user = server['metadata']['users'].split(" ")[0] + self.write_temp_config('server_user', server_user) self.logger.debug("Server's admin user is %s" % _green(server_user)) server_passwd = server['adminPass'] @@ -349,16 +401,19 @@ class SynnefoCI(object): _run(cmd, False) # Setup apt, download packages - self.logger.debug("Setup apt. Install x2goserver and firefox") + self.logger.debug("Setup apt") cmd = """ echo 'APT::Install-Suggests "false";' >> /etc/apt/apt.conf echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf apt-get update - apt-get install curl --yes --force-yes + apt-get install -q=2 curl --yes --force-yes echo -e "\n\n{0}" >> /etc/apt/sources.list # Synnefo repo's key curl https://dev.grnet.gr/files/apt-grnetdev.pub | apt-key add - + """.format(self.config.get('Global', 'apt_repo')) + _run(cmd, False) + cmd = """ # X2GO Key apt-key adv --recv-keys --keyserver keys.gnupg.net E1F958385BFE2B6E apt-get install x2go-keyring --yes --force-yes @@ -377,9 +432,10 @@ class SynnefoCI(object): echo 'Encoding=UTF-8' >> /usr/share/applications/xterm.desktop echo 'Icon=xterm-color_48x48' >> /usr/share/applications/xterm.desktop echo 'Categories=System;TerminalEmulator;' >> \ - /usr/share/applications/xterm.desktop - """.format(self.config.get('Global', 'apt_repo')) - _run(cmd, False) + /usr/share/applications/xterm.desktop""" + if self.config.get("Global", "setup_x2go") == "True": + self.logger.debug("Install x2goserver and firefox") + _run(cmd, False) def _find_flavor(self, flavor=None): """Find a suitable flavor to use @@ -502,14 +558,16 @@ class SynnefoCI(object): # Use the first network as IPv4 server_ip = networks[0]['ipv4'] - if (".okeanos.io" in self.cyclades_client.base_url or - ".demo.synnefo.org" in self.cyclades_client.base_url): - tmp1 = int(server_ip.split(".")[2]) - tmp2 = int(server_ip.split(".")[3]) - server_ip = "gate.okeanos.io" - server_port = 10000 + tmp1 * 256 + tmp2 - else: - server_port = 22 + # Check if config has ssh_port option and if so, use that port. + server_port = self.config.get("Deployment", "ssh_port") + if not server_port: + # No ssh port given. Get it from API (SNF:port_forwarding) + if '22' in server['SNF:port_forwarding']: + server_ip = server['SNF:port_forwarding']['22']['host'] + server_port = int(server['SNF:port_forwarding']['22']['port']) + else: + server_port = 22 + self.write_temp_config('server_ip', server_ip) self.logger.debug("Server's IPv4 is %s" % _green(server_ip)) self.write_temp_config('server_port', server_port) @@ -646,7 +704,8 @@ class SynnefoCI(object): sys.exit(1) @_check_fabric - def clone_repo(self, local_repo=False): + def clone_repo(self, synnefo_repo=None, synnefo_branch=None, + local_repo=False, pull_request=None): """Clone Synnefo repo from slave server""" self.logger.info("Configure repositories on remote server..") self.logger.debug("Install/Setup git") @@ -659,16 +718,65 @@ class SynnefoCI(object): _run(cmd, False) # Clone synnefo_repo - synnefo_branch = self.clone_synnefo_repo(local_repo=local_repo) + synnefo_branch = self.clone_synnefo_repo( + synnefo_repo=synnefo_repo, synnefo_branch=synnefo_branch, + local_repo=local_repo, pull_request=pull_request) # Clone pithos-web-client - self.clone_pithos_webclient_repo(synnefo_branch) + if self.config.get("Global", "build_pithos_webclient") == "True": + # Clone pithos-web-client + self.clone_pithos_webclient_repo(synnefo_branch) @_check_fabric - def clone_synnefo_repo(self, local_repo=False): + def clone_synnefo_repo(self, synnefo_repo=None, synnefo_branch=None, + local_repo=False, pull_request=None): """Clone Synnefo repo to remote server""" + + assert (pull_request is None or + (synnefo_branch is None and synnefo_repo is None)) + + pull_repo = None + if pull_request is not None: + # Get a Github pull request and run the testsuite in + # a sophisticated way. + # Sophisticated means that it will not just check the remote branch + # from which the pull request originated. Instead it will checkout + # the branch for which the pull request is indented (e.g. + # grnet:develop) and apply the pull request over it. This way it + # checks the pull request against the branch this pull request + # targets. + m = re.search("github.com/([^/]+)/([^/]+)/pull/(\d+)", + pull_request) + if m is None: + self.logger.error("Couldn't find a valid GitHub pull request" + " URL") + sys.exit(1) + + group = m.group(1) + repo = m.group(2) + pull_number = m.group(3) + + # Construct api url + api_url = "/repos/%s/%s/pulls/%s" % \ + (group, repo, pull_number) + headers = {'User-Agent': "snf-ci"} + # Get pull request info + try: + conn = httplib.HTTPSConnection("api.github.com") + conn.request("GET", api_url, headers=headers) + response = conn.getresponse() + payload = json.load(response) + synnefo_repo = payload['base']['repo']['html_url'] + synnefo_branch = payload['base']['ref'] + pull_repo = (payload['head']['repo']['html_url'], + payload['head']['ref']) + finally: + conn.close() + # Find synnefo_repo and synnefo_branch to use - synnefo_repo = self.config.get('Global', 'synnefo_repo') - synnefo_branch = self.config.get("Global", "synnefo_branch") + if synnefo_repo is None: + synnefo_repo = self.config.get('Global', 'synnefo_repo') + if synnefo_branch is None: + synnefo_branch = self.config.get("Global", "synnefo_branch") if synnefo_branch == "": synnefo_branch = \ subprocess.Popen( @@ -709,7 +817,7 @@ class SynnefoCI(object): else: # Clone Synnefo from remote repo self.logger.debug("Clone synnefo from %s" % synnefo_repo) - self._git_clone(synnefo_repo) + self._git_clone(synnefo_repo, directory="synnefo") # Checkout the desired synnefo_branch self.logger.debug("Checkout \"%s\" branch/commit" % synnefo_branch) @@ -722,6 +830,16 @@ class SynnefoCI(object): """ % (synnefo_branch) _run(cmd, False) + # Apply a Github pull request + if pull_repo is not None: + self.logger.debug("Apply patches from pull request %s", + pull_number) + cmd = """ + cd synnefo + git pull --no-edit --no-rebase {0} {1} + """.format(pull_repo[0], pull_repo[1]) + _run(cmd, False) + return synnefo_branch @_check_fabric @@ -736,7 +854,7 @@ class SynnefoCI(object): # Clone pithos-webclient from remote repo self.logger.debug("Clone pithos-webclient from %s" % pithos_webclient_repo) - self._git_clone(pithos_webclient_repo) + self._git_clone(pithos_webclient_repo, directory="pithos-web-client") # Track all pithos-webclient branches cmd = """ @@ -781,7 +899,7 @@ class SynnefoCI(object): """.format(pithos_webclient_branch) _run(cmd, False) - def _git_clone(self, repo): + def _git_clone(self, repo, directory=""): """Clone repo to remote server Currently clonning from code.grnet.gr can fail unexpectedly. @@ -791,7 +909,7 @@ class SynnefoCI(object): cloned = False for i in range(1, 11): try: - _run("git clone %s" % repo, False) + _run("git clone %s %s" % (repo, directory), False) cloned = True break except BaseException: @@ -824,7 +942,8 @@ class SynnefoCI(object): # Build synnefo packages self.build_synnefo() # Build pithos-web-client packages - self.build_pithos_webclient() + if self.config.get("Global", "build_pithos_webclient") == "True": + self.build_pithos_webclient() @_check_fabric def build_synnefo(self): @@ -842,6 +961,7 @@ class SynnefoCI(object): cmd = """ dpkg -i snf-deploy*.deb apt-get -f install --yes --force-yes + snf-deploy keygen """ with fabric.cd("synnefo_build-area"): with fabric.settings(warn_only=True): @@ -901,12 +1021,17 @@ class SynnefoCI(object): schema = self.config.get('Global', 'schema') self.logger.debug("Will use \"%s\" schema" % _green(schema)) - schema_dir = os.path.join(self.ci_dir, "schemas/%s" % schema) - if not (os.path.exists(schema_dir) and os.path.isdir(schema_dir)): - raise ValueError("Unknown schema: %s" % schema) - - self.logger.debug("Upload schema files to server") - _put(os.path.join(schema_dir, "*"), "/etc/snf-deploy/") + self.logger.debug("Update schema files to server") + cmd = """ + schema_dir="synnefo/ci/schemas/{0}" + if [ -d "$schema_dir" ]; then + cp "$schema_dir"/* /etc/snf-deploy/ + else + echo "$schema_dir" does not exist + exit 1 + fi + """.format(schema) + _run(cmd, False) self.logger.debug("Change password in nodes.conf file") cmd = """ @@ -916,8 +1041,7 @@ class SynnefoCI(object): self.logger.debug("Run snf-deploy") cmd = """ - snf-deploy keygen --force - snf-deploy --disable-colors --autoconf all + snf-deploy --disable-colors --autoconf synnefo """ _run(cmd, True) @@ -929,9 +1053,7 @@ class SynnefoCI(object): self.logger.debug("Install needed packages") cmd = """ - pip install -U mock - pip install -U factory_boy - pip install -U nose + pip install -U mock factory_boy nose coverage """ _run(cmd, False) diff --git a/contrib/migrate-data b/contrib/migrate-data index 46171ad4d7e0481daa1a40f3a6226555cd7eef7e..f07c4bba257c1e0a7243d13ae355425f756f515b 100755 --- a/contrib/migrate-data +++ b/contrib/migrate-data @@ -1,37 +1,19 @@ #!/usr/bin/env python -# Copyright 2011-2012 GRNET S.A. All rights reserved. -# -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: -# -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. -# -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from binascii import hexlify diff --git a/contrib/migrate-db b/contrib/migrate-db index f9a37aade6af1b82fcc0dbabc6491560ccc2e059..adf7433f4a06ad1599860c947b8505bb153c43d5 100755 --- a/contrib/migrate-db +++ b/contrib/migrate-db @@ -1,37 +1,19 @@ #!/usr/bin/env python -# Copyright 2011-2012 GRNET S.A. All rights reserved. -# -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: -# -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. -# -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from sqlalchemy import Table from sqlalchemy.sql import select, and_ diff --git a/contrib/migrate-users b/contrib/migrate-users index 4a0b2370ebd76b2a0b0a5fe7acf10908556d4edb..5a01606d568423c5de0cc0ecebe19498b1fe5e64 100755 --- a/contrib/migrate-users +++ b/contrib/migrate-users @@ -1,37 +1,19 @@ #!/usr/bin/env python -# Copyright 2011-2012 GRNET S.A. All rights reserved. -# -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: -# -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. -# -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from sqlalchemy import Table from sqlalchemy.sql import select diff --git a/contrib/migrate.py b/contrib/migrate.py index ded082c5f27ef2775bf4cbe3252737c341ae57d0..452e8272e109a58b03ac5738e2c2408f0dde0de0 100644 --- a/contrib/migrate.py +++ b/contrib/migrate.py @@ -1,37 +1,19 @@ #!/usr/bin/env python -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from sqlalchemy import create_engine from sqlalchemy import Table, Column, String, MetaData diff --git a/contrib/snf-pithos-tools/README b/contrib/snf-pithos-tools/README index 560f527fc683eb45f0b110711075d89a935d2d69..b5e063b39908da9823f0a8fe6648052f87bc9f22 100644 --- a/contrib/snf-pithos-tools/README +++ b/contrib/snf-pithos-tools/README @@ -2,7 +2,7 @@ README ====== Pithos is a file storage service, built by GRNET using Django (https://www.djangoproject.com/). -Learn more about Pithos at: http://code.grnet.gr/projects/pithos +Learn more about Pithos at: https://www.synnefo.org/docs/synnefo/latest/pithos.html Consult COPYRIGHT for licensing information. diff --git a/contrib/snf-pithos-tools/pithos/__init__.py b/contrib/snf-pithos-tools/pithos/__init__.py index d3e1674a2f8a9a764550afa47d9f6693947c73ee..5cda842903d0bfec117db1c3c5a43281d46eb551 100644 --- a/contrib/snf-pithos-tools/pithos/__init__.py +++ b/contrib/snf-pithos-tools/pithos/__init__.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # this is a namespace package try: diff --git a/contrib/snf-pithos-tools/pithos/tools/dispatcher.py b/contrib/snf-pithos-tools/pithos/tools/dispatcher.py index acaa72333b3d24997e1492d8ef829d413eb44b4f..d818fafa28a82da091d7ad4cda46429062bb00ec 100755 --- a/contrib/snf-pithos-tools/pithos/tools/dispatcher.py +++ b/contrib/snf-pithos-tools/pithos/tools/dispatcher.py @@ -1,37 +1,19 @@ #!/usr/bin/env python -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import sys import logging diff --git a/contrib/snf-pithos-tools/pithos/tools/fs.py b/contrib/snf-pithos-tools/pithos/tools/fs.py index f8b860e505b4e91162fd6c8e2e309afa373dd1cc..18b01d3dc45afc8538a609a8c74fd28e1ddafaed 100755 --- a/contrib/snf-pithos-tools/pithos/tools/fs.py +++ b/contrib/snf-pithos-tools/pithos/tools/fs.py @@ -1,37 +1,19 @@ #!/usr/bin/env python -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from cStringIO import StringIO from errno import (EACCES, EBADF, EINVAL, EISDIR, EIO, ENOENT, ENOTDIR, diff --git a/contrib/snf-pithos-tools/pithos/tools/lib/client.py b/contrib/snf-pithos-tools/pithos/tools/lib/client.py index c0bb9e39fe87742b3d787da566f017af7c1a5eac..c85cb346249839db06a8867c023b7ee5b3d8365a 100644 --- a/contrib/snf-pithos-tools/pithos/tools/lib/client.py +++ b/contrib/snf-pithos-tools/pithos/tools/lib/client.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from httplib import HTTPConnection, HTTPSConnection, HTTP from sys import stdin diff --git a/contrib/snf-pithos-tools/pithos/tools/lib/hashmap.py b/contrib/snf-pithos-tools/pithos/tools/lib/hashmap.py index 977d8fcdf76765c813624a9a03a7de3cc5903bfb..78ecd683d75e5982d6fccbd589fa7a44207d30b8 100644 --- a/contrib/snf-pithos-tools/pithos/tools/lib/hashmap.py +++ b/contrib/snf-pithos-tools/pithos/tools/lib/hashmap.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import hashlib import os diff --git a/contrib/snf-pithos-tools/pithos/tools/lib/transfer.py b/contrib/snf-pithos-tools/pithos/tools/lib/transfer.py index 857af9f68f76342b5fe7cb8ef5820f0f74f8c45c..74a050d74b983842d7db3391902bee2f2724c4bd 100644 --- a/contrib/snf-pithos-tools/pithos/tools/lib/transfer.py +++ b/contrib/snf-pithos-tools/pithos/tools/lib/transfer.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import os import types diff --git a/contrib/snf-pithos-tools/pithos/tools/lib/util.py b/contrib/snf-pithos-tools/pithos/tools/lib/util.py index 77685c5ad1f71a550e74257b38038d11ac6143a5..d22dbdc1d318f3f63156f4c178fb9538e7605d26 100644 --- a/contrib/snf-pithos-tools/pithos/tools/lib/util.py +++ b/contrib/snf-pithos-tools/pithos/tools/lib/util.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import os diff --git a/contrib/snf-pithos-tools/pithos/tools/sh.py b/contrib/snf-pithos-tools/pithos/tools/sh.py index eb279841445522e55b63ad16c4f8ee5855191197..329871ce970d0c10d95f79747b858cbc02c93470 100755 --- a/contrib/snf-pithos-tools/pithos/tools/sh.py +++ b/contrib/snf-pithos-tools/pithos/tools/sh.py @@ -1,37 +1,19 @@ #!/usr/bin/env python -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from getpass import getuser from optparse import OptionParser diff --git a/contrib/snf-pithos-tools/pithos/tools/sync.py b/contrib/snf-pithos-tools/pithos/tools/sync.py index 7ca4b2351386d8afe2c4c083193c80c83f6d79e6..f5eabc006838e8c2af0658c06b570d64b8973e08 100755 --- a/contrib/snf-pithos-tools/pithos/tools/sync.py +++ b/contrib/snf-pithos-tools/pithos/tools/sync.py @@ -1,37 +1,19 @@ #!/usr/bin/env python -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import os import sqlite3 diff --git a/contrib/snf-pithos-tools/pithos/tools/test.py b/contrib/snf-pithos-tools/pithos/tools/test.py index 1a50d0588670bda4fde68b5d72101b8455225565..c2d9b216d987423184cdbab67c5a4d28049f3acb 100755 --- a/contrib/snf-pithos-tools/pithos/tools/test.py +++ b/contrib/snf-pithos-tools/pithos/tools/test.py @@ -1,38 +1,20 @@ #!/usr/bin/env python #coding=utf8 -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from pithos.tools.lib.client import Pithos_Client, Fault from pithos.tools.lib.util import get_user, get_auth, get_url diff --git a/contrib/snf-pithos-tools/setup.py b/contrib/snf-pithos-tools/setup.py index 87a844f77430e421e3c4f3a4895f2804f43df69e..371ecf40f3bbb3dc4934e177654508ddebaa0c90 100644 --- a/contrib/snf-pithos-tools/setup.py +++ b/contrib/snf-pithos-tools/setup.py @@ -1,37 +1,19 @@ #!/usr/bin/env python -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # import distribute_setup @@ -169,7 +151,7 @@ def find_package_data( setup( name='snf-pithos-tools', version=VERSION, - license='BSD', + license='GNU GPLv3', url='http://www.synnefo.org/', description=SHORT_DESCRIPTION, long_description=README + '\n\n' + CHANGES, diff --git a/devflow.conf b/devflow.conf index f16d9164393c42c6df15b205902a03c1571fd644..959a2cbe112579af87097be5c476d92a7c85786e 100644 --- a/devflow.conf +++ b/devflow.conf @@ -7,6 +7,8 @@ version_file = "snf-astakos-app/astakos/version.py" [[snf-cyclades-app]] version_file = "snf-cyclades-app/synnefo/versions/app.py" + [[snf-admin-app]] + version_file = "snf-admin-app/synnefo_admin/version.py" [[snf-cyclades-gtools]] version_file = "snf-cyclades-gtools/synnefo/versions/ganeti.py" [[snf-pithos-app]] diff --git a/docs/admin-guide.rst b/docs/admin-guide.rst index 53c252b73e3e2e3fd43ad80868b8d6f4e8232975..f9996dadfba3f0820ea53dd8fb7ff7e313331de9 100644 --- a/docs/admin-guide.rst +++ b/docs/admin-guide.rst @@ -21,6 +21,62 @@ all the interactions between them. :target: _images/synnefo-arch2.png +Required system users and groups (synnefo, archipelago) +======================================================= + +Since v0.16, Synnefo requires an Archipelago installation for the Pithos +backend. Archipelago on the other hand, supports both NFS and RADOS as +storage backends. This leads us to various components that have specific +access rights. + +Synnefo ships its own configuration files under ``/etc/synnefo``. In +order those files not to be compromised, they are owned by +``root:synnefo`` with group read access (mode 640). Since Gunicorn, +which serves Synnefo by default, needs read access to the configuration +files and we don't want it to run as root, it must run with group +``synnefo``. + +Cyclades and Pithos talk to Archipelago over some named pipes under +``/dev/shm/posixfd``. This directory is created by Archipelago, owned by +the user/group that Archipelago runs as, and at the same time it must be +accessible by Gunicorn. Therefore we let Gunicorn run as ``synnefo`` +user and Archipelago as ``archipelago:synnefo`` (by default it rus as +``archipelago:archipelago``). Beware that the ``synnefo`` user and +group is created by snf-common package. + +Archipelago must have a storage backend to physically store blocks, maps +and locks. This can be either an NFS or a RADOS cluster. + +NFS backing store +----------------- +In case of NFS, Archipelago must have permissions to write on the +exported dirs. We choose to have ``/srv/archip`` exported with +``blocks``, ``maps``, and ``locks`` subdirectories. They are owned by +``archipelago:synnefo`` and have ``g+ws`` access permissions. So +Archipelago will be able to read/write in these directories. We could +have the whole NFS isolated from Synnefo (owned by +``archipelago:archipelago`` with ``640`` access permissions) but we +choose not to (e.g. some future extension could require access to the +backing store directly from Synnefo). + +Due to NFS restrictions, all Archipelago nodes must have common uid for +the ``archipelago`` user and common gid for the ``synnefo`` group. So +before any Synnefo installation, we create them here in advance. We +assume that ids 200 and 300 are available across all nodes. + +.. code-block:: console + + # addgroup --system --gid 200 synnefo + # adduser --system --uid 200 --gid 200 --no-create-home \ + --gecos Synnefo synnefo + + # addgroup --system --gid 300 archipelago + # adduser --system --uid 300 --gid 300 --no-create-home \ + --gecos Archipelago archipelago + +Normally the ``snf-common`` and ``archipelago`` packages are responsible +for creating the required system users and groups. + Identity Service (Astakos) ========================== @@ -218,8 +274,8 @@ user in the following cases: :ref:`authentication methods policies <auth_methods_policies>`). If all of the above fail to trigger automatic activation, an email is sent to -the persons listed in ``HELPDESK``, ``MANAGERS`` and ``ADMINS`` settings, -notifing that there is a new user pending for moderation and that it's up to +the persons listed in ``ACCOUNT_NOTIFICATIONS_RECIPIENTS`` setting, +notifying that there is a new user pending for moderation and that it's up to the administrator to decide if the user should be activated. The UI also shows a corresponding 'pending moderation' message to the user. The administrator can activate a user using the ``snf-manage user-modify`` command: @@ -234,7 +290,7 @@ activate a user using the ``snf-manage user-modify`` command: Once the activation process finishes, a greeting message is sent to the user email address and a notification for the activation to the persons listed in -``HELPDESK``, ``MANAGERS`` and ``ADMINS`` settings. Once activated the user is +``ACCOUNT_NOTIFICATIONS_RECIPIENTS`` setting. Once activated the user is able to login and access the Synnefo services. Additional authentication methods @@ -312,61 +368,60 @@ Upon success, the system renews the token (if it has expired), logins the user and sets the cookie, before redirecting the user to the ``next`` parameter value. -Setting quota limits -~~~~~~~~~~~~~~~~~~~~ - -Set default quota -````````````````` -To inspect current default base quota limits, run:: - - # snf-manage resource-list - -You can modify the default base quota limit for all future users with:: - - # snf-manage resource-modify <resource_name> --default-quota <value> - -Set base quota for individual users -``````````````````````````````````` +Projects and quota +~~~~~~~~~~~~~~~~~~ -For individual users that need different quota than the default -you can set it for each resource like this:: +Synnefo supports granting resources and controling their quota through the +mechanism of *projects*. A project is considered as a pool of finite +resources. Every actual resources allocated by a user (e.g. a Cyclades VM or +a Pithos container) is also assigned to a project where the user is a +member to. For each resource a project specifies the maximum amount that can +be assigned to it and the maximum amount that a single member can assign to it. - # use this to display quota / uuid - # snf-manage user-show 'uuid or email' --quota +Default quota +````````````` - # snf-manage user-modify <user-uuid> --base-quota 'cyclades.vm' 10 +Upon user creation, a special purpose user-specific project is automatically +created in order to hold the quota provided by the system. These *system* +projects are identified with the same UUID as the user. -You can set base quota for all existing users, with possible exceptions, using:: +To inspect the quota that future users will receive by default through their +base projects, check column ``system_default`` in:: - # snf-manage user-modify --all --base-quota cyclades.vm 10 --exclude uuid1,uuid2 + # snf-manage resource-list -All quota for which values different from the default have been set, -can be listed with:: +You can modify the default system quota limit for all future users with:: - # snf-manage quota-list --with-custom=True + # snf-manage resource-modify <resource_name> --system-default <value> +You can also control the default quota a new project offers to its members +if a limit is not specified in the project application (`project default`). +In particular, if a resource is not meant to be visible to the end user, +then it's best to set its project default to infinite. -Enable the Projects feature -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. code-block:: console -If you want to enable the projects feature so that users may apply -on their own for resources by creating and joining projects, -in ``20-snf-astakos-app-settings.conf`` set:: + # snf-manage resource-modify cyclades.total_ram --project-default inf - # this will make the 'projects' page visible in the dashboard - ASTAKOS_PROJECTS_VISIBLE = True -You can change the maximum allowed number of pending project applications -per user with:: +Grant extra quota through projects +`````````````````````````````````` - # snf-manage resource-modify astakos.pending_app --default-quota <number> +A user can apply for a new project through the web interface or the API. +Once it is approved by the administrators, the applicant can join the +project and let other users in too. -You can also set a user-specific limit with:: +A project member can make use of the quota granted by the project by +specifying this particular project when creating a new quotable entity. - # snf-manage user-modify <user-uuid> --base-quota 'astakos.pending_app' 5 +Note that quota are not accumulative: in order to allocate a 100GB disk, +one must be in a project that grants at least 100GB; it is not possible to +add up quota from different projects. Note also that if allocating an entity +requires multiple resources (e.g. cpu and ram for a Cyclades VM) these must +be all assigned to a single project. -When users apply for projects they are not automatically granted -the resources. They must first be approved by the administrator. +Control projects +```````````````` To list pending project applications in astakos:: @@ -381,13 +436,39 @@ To deny an application:: # snf-manage project-control --deny <app id> -Users designated as *project admins* can approve, deny, or modify +Before taking an action, on can inspect project status, settings and quota +limits with:: + + # snf-manage project-show <project-uuid> + +For an initialized project, option ``--quota`` also reports the resource +usage. + +Users designated as *project admins* can approve or deny an application through the web interface. In ``20-snf-astakos-app-settings.conf`` set:: # UUIDs of users that can approve or deny project applications from the web. ASTAKOS_PROJECT_ADMINS = [<uuid>, ...] +Set quota limits +```````````````` + +One can change the quota limits of an initialized project with:: + + # snf-manage project-modify <project-uuid> --limit <resource_name> <member_limit> <project_limit> + +One can set system quota for all accepted users (that is, set limits for system +projects), with possible exceptions, with:: + + # snf-manage project-modify --all-system-projects --exclude <uuid1>,<uuid2> --limit ... + +Quota for a given resource are reported for all projects that the user is +member in with:: + + # snf-manage user-show <user-uuid> --quota + +With option ``--projects``, owned projects and memberships are also reported. Astakos advanced operations --------------------------- @@ -635,27 +716,24 @@ Enabling this feature consists of the following steps: .. _select_pithos_storage: -Select Pithos storage backend -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Pithos storage backend +~~~~~~~~~~~~~~~~~~~~~~ + +Starting from Synnefo version 0.16, we introduce Archipelago as the new storage +backend. Archipelago will act as a storage abstraction layer between Pithos and +NFS, RADOS or any other storage backend driver that Archipelago supports. For +more information about backend drivers please check Archipelago documentation. -Starting from Synnefo 0.15.1 we introduce the ability to select or change the -storage backend. If you have already enabled and configured RADOS as your -secondary storage solution you can now explicitly select your storage -backend being only RADOS. +Since this version care must be taken when restarting Archipelago on a Pithos +worker node. Pithos acts as an Archipelago peer and must be stopped first +before trying to restart Archipelago for any reason. -A new variable has been introduced called PITHOS_BACKEND_STORAGE with -possible values 'nfs' and 'rados', default value is 'nfs'. -For those users that need to migrate from NFS to RADOS and have not enabled the -dual mode of operation from the beginning of their installation, you can -use a synchronization script that is provided in order to synchronize the data -from NFS to Rados. The script can be found at -`/usr/lib/pithos/tools/pithos-sync-rados.sh`. +If you need to restart Archipelago on a running Pithos worker follow the +procedure below:: -Since this version the dual mode of operation is not supported any more, -meaning you will not be able to keep double Pithos objects anymore in NFS and -RADOS. -After installing v0.15.1 you will have to choose between the storage backend -you want to use. + pithos-host$ /etc/init.d/gunicorn stop + pithos-host$ /etc/init.d/archipelago restart + pithos-host$ /etc/init.d/gunicorn start Compute/Network/Image Service (Cyclades) @@ -688,8 +766,8 @@ to Cyclades. Working with Cyclades --------------------- -Flavors -~~~~~~~ +Flavors and Volume Types +~~~~~~~~~~~~~~~~~~~~~~~~ When creating a VM, the user must specify the `flavor` of the virtual server. Flavors are the virtual hardware templates, and provide a description about @@ -701,8 +779,9 @@ Flavors are created by the administrator and the user can select one of the available flavors. After VM creation, the user can resize his VM, by adding/removing CPU and RAM. -Cyclades support different storage backends that are described by the disk -template of the flavor, which is mapped to Ganeti's instance `disk template`. +Cyclades support different storage backends that are described by the `volume +type` of the flavor. Each volume type contains a `disk template` attribute +which is mapped to Ganeti's instance `disk template`. Currently the available disk templates are the following: * `file`: regulars file @@ -715,16 +794,24 @@ Currently the available disk templates are the following: - `ext_archipelago`: External shared storage provided by `Archipelago <http://www.synnefo.org/docs/archipelago/latest/index.html>`_. +Volume types are created by the administrator using the `snf-manage +volume-type-create` command and providing the `disk template` and a +human-friendly name: + +.. code-block:: console + + $ snf-manage volume-type-create --disk-template=drbd --name=DRBD + Flavors are created by the administrator using `snf-manage flavor-create` command. The command takes as argument number of CPUs, amount of RAM, the size -of the disks and the disk templates and create the flavors that belong to the +of the disks and the volume type IDs and creates the flavors that belong to the cartesian product of the specified arguments. For example, the following -command will create two flavors of `40G` disk size with `drbd` disk template, +command will create two flavors of `40G` disk size of volume type with ID `1`, `4G` RAM and `2` or `4` CPUs. .. code-block:: console - $ snf-manage flavor-create 2,4 4096 40 drbd + $ snf-manage flavor-create 2,4 4096 40 1 To see the available flavors, run `snf-manage flavor-list` command. The administrator can delete a flavor by using `flavor-modify` command: @@ -804,7 +891,7 @@ delete the Cyclades Images but will leave the Pithos file as is (unregister). Apart from using `kamaki` to see and hangle the available images, the administrator can use `snf-manage image-list` and `snf-manage image-show` -commands to list and inspect the available public images. Also, the `--user-id` +commands to list and inspect the available public images. Also, the `--user` option can be used the see the images of a specific user. Virtual Servers @@ -819,7 +906,7 @@ others, by a prefix in their names. This prefix is defined in Apart from handling Cyclades VM at the Ganeti level, the administrator can also use the `snf-manage server-*` commands. These command cover the most -common tasks that are relative with VM handling. Below we describe come +common tasks that are relative with VM handling. Below we describe some of them, but for more information you can use the `--help` option of all `snf-manage server-* commands`. These command cover the most @@ -831,11 +918,11 @@ bypassing automatic VM allocation. .. code-block:: console - $ snf-manage server-create --flavor-id=1 --image-id=fc0f6858-f962-42ce-bf9a-1345f89b3d5e \ - --user-id=7cf4d078-67bf-424d-8ff2-8669eb4841ea --backend-id=2 \ + $ snf-manage server-create --flavor=1 --image=fc0f6858-f962-42ce-bf9a-1345f89b3d5e \ + --user=7cf4d078-67bf-424d-8ff2-8669eb4841ea --backend-id=2 \ --password='example_passw0rd' --name='test_vm' -The above commnd will create a new VM for user +The above command will create a new VM for user `7cf4d078-67bf-424d-8ff2-8669eb4841ea` in the Ganeti backend with ID 2. By default this command will issue a Ganeti job to create the VM (`OP_INSTANCE_CREATE`) and return. As in other commands, the `--wait=True` @@ -1036,14 +1123,14 @@ better understanding of these commands, refer to their help messages. Create a virtual private network for user `7cf4d078-67bf-424d-8ff2-8669eb4841ea` using the `PHYSICAL_VLAN` flavor, which -means that the network will be uniquely assigned a phsyical VLAN. The network +means that the network will be uniquely assigned a physical VLAN. The network is assigned an IPv4 subnet, described by it's CIDR and gateway. Also, the `--dhcp=True` option is used, to make `nfdhcpd` response to DHCP queries from VMs. .. code-block:: console - $ snf-manage network-create --owner=7cf4d078-67bf-424d-8ff2-8669eb4841ea --name=prv_net-1 \ + $ snf-manage network-create --user=7cf4d078-67bf-424d-8ff2-8669eb4841ea --name=prv_net-1 \ --subnet=192.168.2.0/24 --gateway=192.168.2.1 --dhcp=True --flavor=PHYSICAL_VLAN Inspect the state of the network in Cyclades DB and in all the Ganeti backends: @@ -1059,7 +1146,7 @@ subnet's IPv4 address allocation pool: $ snf-manage subnet-inspect <subnet_id> -Connect a VM to the created private network. The port will be automatically +Connect a VM to the created private network. The port will automatically be assigned an IPv4 address from one of the network's available IPs. This command will result in sending an `OP_INSTANCE_MODIFY` Ganeti command and attaching a NIC to the specified Ganeti instance. @@ -1114,14 +1201,14 @@ from `snf-manage` would look like this: --------------------------------------------------------------------------------------------- 1 Internet None ACTIVE True 10.2.1.0/24 10.2.1.1 False True - $ snf-manage floating-ip-create --owner=7cf4d078-67bf-424d-8ff2-8669eb4841ea --network=1 + $ snf-manage floating-ip-create --user=7cf4d078-67bf-424d-8ff2-8669eb4841ea --network=1 $ snf-manage floating-ip-list --user=7cf4d078-67bf-424d-8ff2-8669eb4841ea id address network user.uuid server ------------------------------------------------------------------------ 38 10.2.1.2 1 7cf4d078-67bf-424d-8ff2-8669eb4841ea 42 - $ snf-manage port-create --owner=7cf4d078-67bf-424d-8ff2-8669eb4841ea --network=1 \ + $ snf-manage port-create --user=7cf4d078-67bf-424d-8ff2-8669eb4841ea --network=1 \ --ipv4-address=10.2.1.2 --floating-ip=38 $ snf-manage port-list --user=7cf4d078-67bf-424d-8ff2-8669eb4841ea @@ -1234,7 +1321,7 @@ As already mentioned Cyclades use a pool of Bridges that must correspond to Physical VLAN at the Ganeti level. A bridge from the pool is assigned to each network of flavor `PHYSICAL_VLAN`. Creation of this pool is done using `snf-manage pool-create` command. For example the following command -will create a pool containing the brdiges from `prv1` to `prv21`. +will create a pool containing the bridges from `prv1` to `prv21`. .. code-block:: console @@ -1269,7 +1356,7 @@ externally reserved, to exclude from allocation. Quotas ~~~~~~ -The andling of quotas for Cyclades resources is powered by Astakos quota +The handling of quotas for Cyclades resources is powered by Astakos quota mechanism. During registration of Cyclades service to Astakos, the Cyclades resources are also imported to Astakos for accounting and presentation. @@ -1358,7 +1445,7 @@ Cyclades database may differ from the real state of VMs and networks in the Ganeti backends. The reconciliation process is designed to synchronize the state of the Cyclades DB with Ganeti. There are two management commands for reconciling VMs and Networks that will detect stale, orphans and out-of-sync -VMs and networks. To fix detected inconsistencies, use the `--fix-all`. +VMs and networks. To fix detected inconsistencies, use the `--fix-all` option. .. code-block:: console @@ -1408,6 +1495,71 @@ To fix detected inconsistencies, use the `--fix` option. $ snf-manage reconcile-pools $ snf-manage reconcile-pools --fix + +.. _admin-guide-vnc: + +snf-vncauthproxy configuration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Since ``snf-vncauthproxy-1.6`` and ``snf-cyclades-app-0.16``, it is possible +to run snf-vncauthproxy on a separate node and have multiple snf-vncauthproxy +instances / nodes, to serve clients. + +The ``CYCLADES_VNCAUTHPROXY_OPTS`` setting has become a list of dictionaries, +each of which defines one snf-vncauthproxy instance. Each vncauthproxy should +be properly configured to accept control connections by the Cylades host (via +the ``--listen-address`` CLI parameter of snf-vncauthproxy) and VNC connections +from clients (via the ``--proxy-listen-address`` CLI parameter. + +For a two-node vncauthproxy setup, the ``CYCLADES_VNCAUTHPROXY_OPTS`` would +look like: + +.. code-block:: console + + CYCLADES_VNCAUTHPROXY_OPTS = [ + { + 'auth_user': 'synnefo', + 'auth_password': 'secret_password', + 'server_address': 'node1.synnefo.live', + 'server_port': 24999, + 'enable_ssl': True, + 'ca_cert': '/path/to/cacert', + 'strict': True, + }, + { + 'auth_user': 'synnefo', + 'auth_password': 'secret_password', + 'server_address': 'node2.synnefo.live', + 'server_port': 24999, + 'enable_ssl': False, + 'ca_cert': '/path/to/cacert', + 'strict': True, + }, + ] + +The ``server_address`` is the host / IP which Cyclades will use for the control +connection, in order to set up the forwarding. + +The vncauthproxy ``DAEMON_OPTS`` option in ``/etc/default/vncauthproxy`` would +look like: + +.. code-block:: console + + DAEMON_OPTS="--pid-file=$PIDFILE --listen-address=node1.synnefo.live --proxy-listen-address=node1.synnefo.live" + +The ``--proxy-listen-address`` is the host / IP which clients (Web browsers / +VNC clients) will use to connect to snf-vncauthproxy. + +In case that snf-vncauthproxy doesn't run on the same node as the Cyclades +node, it is highly recommended to enable SSL on the control socket, using +strict verification of the server certificate. The only caveat, for the time +being, is that the same certificate, provided to snf-vncauthproxy, is used for +both the control and the client connections. If the control and client host +(``--listen-address`` and ``--proxy-listen-address`` parameters, respectively) +differ, you should make sure to generate a certificate covering both (using the +one as common name / CN, and specifying the other as a subject alternative +name). + .. _admin-guide-stats: VM stats collecting @@ -1489,7 +1641,7 @@ host and export the RRD directory to the snf-stats-app node via e.g. NFS. ``GRAPH_PREFIX`` is the directory where collectd stores the resulting stats graphs. You should create it manually, in case it doesn't exist. -.. code-block:: +.. code-block:: console # mkdir /var/cache/snf-stats-app/ # chown www-data:wwwdata /var/cache/snf-stats-app/ @@ -1501,6 +1653,7 @@ directory. snf-stats-app, based on the ``STATS_BASE_URL`` setting will export the following URL 'endpoints`: + * CPU stats bar: ``STATS_BASE_URL``/v1.0/cpu-bar/<encrypted VM hostname> * Network stats bar: ``STATS_BASE_URL``/v1.0/net-bar/<encrypted VM hostname> * CPU stats daily graph: ``STATS_BASE_URL``/v1.0/cpu-ts/<encrypted VM hostname> @@ -1510,7 +1663,7 @@ following URL 'endpoints`: You can verify that these endpoints are exported by issuing: -.. code-block:: +.. code-block:: console # snf-manage show_urls @@ -1518,9 +1671,10 @@ snf-cyclades-gtools configuration ````````````````````````````````` To enable VM stats collecting, you will need to: - * Install collectd on the every Ganeti (VM-capable) node. + + * Install collectd on every Ganeti (VM-capable) node. * Enable the Ganeti stats plugin in your collectd configuration. This can be - achived by either copying the example collectd conf file that comes with + achieved by either copying the example collectd conf file that comes with snf-cyclades-gtools (``/usr/share/doc/snf-cyclades-gtools/examples/ganeti-stats-collectd.conf``) or by adding the following line to your existing (or default) collectd @@ -1568,14 +1722,14 @@ fetch them when needed. Helpdesk -------- -Helpdesk application provides the ability to view the virtual servers and +The Helpdesk application provides the ability to view the virtual servers and networks of all users, along with the ability to perform some basic actions like administratively suspending a server. You can perform look-ups by user UUID or email, by server ID (vm-$id) or by an IPv4 address. If you want to activate the helpdesk application you can set to `True` the `HELPDESK_ENABLED` setting. Access to helpdesk views (under -`$BASE_URL/helpdesk`) is only to allowed to users that belong to Astakos +`$BASE_URL/helpdesk`) is only allowed to users that belong to Astakos groups defined in the `HELPDESK_PERMITTED_GROUPS` setting, which by default contains the `helpdesk` group. For example, to allow <user_id> to access helpdesk view, you should run the following command in the Astakos @@ -1625,6 +1779,128 @@ these messages and properly updates the state of the Cyclades DB. Subsequent requests to the Cyclades API, will retrieve the updated state from the DB. +Admin Dashboard (Admin) +======================= + +Introduction +------------ + +Admin is the Synnefo component that provides to trusted users the ability to +manage and view various different Synnefo entities such as users, VMs, projects +etc. Additionally, it automatically generates charts and statistics using data +from the Astakos/Cyclades stats. + +Access and permissions +---------------------- + +The Admin dashboard can be accessed by default from the ``ADMIN_BASE_URL`` URL. +Since there is no login form, the user must login on Astakos first and then +visit the above URL. Access will be granted only to users that belong to a +predefined list of Astakos groups. By default, there are three group categories +that are mapped 1-to-1 to Astakos groups: + +* ADMIN_READONLY_GROUP: 'admin-readonly' +* ADMIN_HELPDESK_GROUP: 'helpdesk' +* ADMIN_GROUP: 'admin' + +The group categories can be changed using the ``ADMIN_PERMITTED_GROUPS`` +setting. In order to change the Astakos group that a category corresponds to, +the administrator can specify the group that he/she wants in the +``ADMIN_READONLY_GROUP``, ``ADMIN_HELPDESK_GROUP`` or ``ADMIN_GROUP`` settings. + +Note that while any user that belongs to the ``ADMIN_PERMITTED_GROUPS`` has the +same access to the administrator dashboard, the actions that are allowed for a +group may differ. That's because Admin implements a Role-Based Access Control +(RBAC) policy, which can be changed from the ``ADMIN_RBAC`` setting. By +default, users in the ``ADMIN_READONLY_GROUP`` cannot perform any actions. On +the other hand, users in the ``ADMIN_GROUP`` can perform all actions. In the +middle of the spectrum is the ``ADMIN_HELPDESK_GROUP``, which by default +performs a small subset of reversible actions. + +Seting up Admin +--------------- + +Admin is bundled by default with a list of sane settings. The most important +one, ``ADMIN_ENABLED``, is set to ``True`` and defines whether Admin will be used +or not. + +The administrator simply has to create the necessary Astakos groups and +add trusted users in them. The following example will create an admin group and +will add a user in it: + +.. code-block:: console + + snf-manage group-add admin + snf-manage user-modify --add-group=admin <user_id> + +Finally, the administrator must edit the ``20-snf-admin-app-general.conf`` +settings file, uncomment the ``ADMIN_BASE_URL`` setting and assign the +appropriate URL to it. In most cases, this URL will be the top-level URL of the +Admin node, with the optional addition of an extra path (e.g. ``/admin``) in +order to distinguish it from different components. + +That's all that is required for a single-node setup. For a multi-node setup, +please consult the following section: + +Multi-node Setup +~~~~~~~~~~~~~~~~ + +Admin by design does not use the Astakos/Cyclades API for any action. Instead, +it requires direct access to the Astakos/Cyclades database as well as the +settings of their nodes. As a result, when installing Admin in a node, the +Astakos and Cyclades packages will also be installed. + +In order to disable the Astakos/Cyclades API in the Admin node, the +administrator can add the following line in ``99-locals.conf`` (you can create +it if doesn't exist): + +.. code-block:: console + + ROOT_URLCONF="synnefo_admin.urls" + +Note that the above change does not interfere with the ``ADMIN_BASE_URL``, +which will be used normally. + +Furthermore, if Astakos and Cyclades have separate databases, then they must be +defined in the ``DATABASES`` setting of ``10-snf-webproject-database.conf``. An +example setup is the following: + +.. code-block:: console + + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': 'snf_apps_cyclades', + 'HOST': <Cyclades host>, + <...snip..> + }, 'cyclades': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': 'snf_apps_cyclades', + 'HOST': <Cyclades host>, + <...snip..> + }, 'astakos': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': 'snf_apps_astakos', + 'HOST': <Astakos host>, + <...snip..> + } + } + + DATABASE_ROUTERS = ['snf_django.utils.routers.SynnefoRouter'] + +You may notice that there are three databases instead of two. That's because +Django requires that every ``DATABASES`` setting has a *default* database. In +our case, we suggest that you use as default the Cyclades database. Finally, +you must not forget to add the ``DATABASE_ROUTERS`` setting in the above +example that must always be used in multi-db setups. + +Disabling Admin +--------------- + +The easiest way to disable the Admin Dashboard is to set the ``ADMIN_ENABLED`` +setting to ``False``. + + List of all Synnefo components ============================== @@ -1638,8 +1914,9 @@ They are also available from our apt repository: ``apt.dev.grnet.gr`` * `snf-pithos-webclient <http://www.synnefo.org/docs/pithos-webclient/latest/index.html>`_ * `snf-cyclades-app <http://www.synnefo.org/docs/snf-cyclades-app/latest/index.html>`_ * `snf-cyclades-gtools <http://www.synnefo.org/docs/snf-cyclades-gtools/latest/index.html>`_ + * `snf-admin-app <http://www.synnefo.org/docs/snf-admin-app/latest/index.html>`_ * `astakosclient <http://www.synnefo.org/docs/astakosclient/latest/index.html>`_ - * `snf-vncauthproxy <https://code.grnet.gr/projects/vncauthproxy>`_ + * `snf-vncauthproxy <https://github.com/grnet/snf-vncauthproxy>`_ * `snf-image <http://www.synnefo.org/docs/snf-image/latest/index.html/>`_ * `snf-image-creator <http://www.synnefo.org/docs/snf-image-creator/latest/index.html>`_ * `snf-occi <http://www.synnefo.org/docs/snf-occi/latest/index.html>`_ @@ -1650,10 +1927,10 @@ They are also available from our apt repository: ``apt.dev.grnet.gr`` Synnefo management commands ("snf-manage") ========================================== -Each Synnefo service, Astakos, Pithos and Cyclades are controlled by the +Each Synnefo service, Astakos, Pithos and Cyclades is controlled by the administrator using the "snf-manage" admin tool. This tool is an extension of the Django command-line management utility. It is run on the host that runs -each service and provides different types of commands depending the services +each service and provides different types of commands depending on the services running on the host. If you are running more than one service on the same host "snf-manage" adds all the corresponding commands for each service dynamically, providing a unified admin environment. @@ -1773,6 +2050,10 @@ network-remove Delete a network flavor-create Create a new flavor flavor-list List flavors flavor-modify Modify a flavor +volume-type-create Create a new volume type +volume-type-list List volume types +volume-type-show Show volume type details +volume-type-modify Modify a volume type image-list List images image-show Show image details pool-create Create a bridge or mac-prefix pool @@ -1814,8 +2095,8 @@ Running: snf-component-register [<component_name>] -automates the registration of the standard Synnefo components (astakos, -cyclades, and pithos) in astakos database. It internally uses the script: +automates the registration of the standard Synnefo components (Astakos, +Cyclades, and Pithos) in Astakos database. It internally uses the script: .. code-block:: console @@ -1842,7 +2123,7 @@ Name Description ============================ =========================== delete Remove an account from the Pithos DB export-quota Export account quota in a file -list List existing/dublicate accounts +list List existing/duplicate accounts merge Move an account contents in another account set-container-quota Set container quota for all or a specific account ============================ =========================== @@ -1853,7 +2134,7 @@ The "kamaki" API client To upload, register or modify an image you will need the **kamaki** tool. Before proceeding make sure that it is configured properly. Verify that -*image.url*, *file.url*, *user.url* and *token* are set as needed: +*url* and *token* are set as needed: .. code-block:: console @@ -2124,52 +2405,52 @@ description and a link to their content: * ``snf-astakos-app/astakos/im/templates/im/email.txt`` Base email template. Contains a contact email and a “thank you†message. - (`Link <https://code.grnet.gr/projects/synnefo/repository/revisions/master/changes/snf-astakos-app/astakos/im/templates/im/email.txt>`_) + (`Link <https://github.com/grnet/synnefo/blob/master/snf-astakos-app/astakos/im/templates/im/email.txt>`_) * ``snf-astakos-app/astakos/im/templates/im/activation_email.txt`` Email sent to user that prompts him/her to click on a link provided to activate the account. - Extends “email.txt†(`Link <https://code.grnet.gr/projects/synnefo/repository/revisions/master/changes/snf-astakos-app/astakos/im/templates/im/activation_email.txt>`_) + Extends “email.txt†(`Link <https://github.com/grnet/synnefo/blob/master/snf-astakos-app/astakos/im/templates/im/activation_email.txt>`_) * ``snf-astakos-app/astakos/im/templates/im/invitation.txt`` Email sent to an invited user. He/she has to click on a link provided to activate the account. - Extends “email.txt†(`Link <https://code.grnet.gr/projects/synnefo/repository/revisions/master/changes/snf-astakos-app/astakos/im/templates/im/invitation.txt>`_) + Extends “email.txt†(`Link <https://github.com/grnet/synnefo/blob/master/snf-astakos-app/astakos/im/templates/im/invitation.txt>`_) * ``snf-astakos-app/astakos/im/templates/im/switch_accounts_email.txt`` Email sent to user upon his/her request to associate this email address with a shibboleth account. He/she has to click on a link provided to activate the - association. Extends “email.txt†(`Link <https://code.grnet.gr/projects/synnefo/repository/revisions/master/changes/snf-astakos-app/astakos/im/templates/im/switch_accounts_email.txt>`_) + association. Extends “email.txt†(`Link <https://github.com/grnet/synnefo/blob/master/snf-astakos-app/astakos/im/templates/im/switch_accounts_email.txt>`_) * ``snf-astakos-app/astakos/im/templates/im/welcome_email.txt`` Email sent to inform the user that his/ her account has been activated. Extends “email.txt†- (`Link <https://code.grnet.gr/projects/synnefo/repository/revisions/master/changes/snf-astakos-app/astakos/im/templates/im/welcome_email.txt>`_) + (`Link <https://github.com/grnet/synnefo/blob/master/snf-astakos-app/astakos/im/templates/im/welcome_email.txt>`_) * ``snf-astakos-app/astakos/im/templates/registration/email_change_email.txt`` Email sent to user when he/she has requested new email address assignment. The user has to click on a link provided to validate this action. Extends - “email.txt†(`Link <https://code.grnet.gr/projects/synnefo/repository/revisions/master/changes/snf-astakos-app/astakos/im/templates/registration/email_change_email.txt>`_) + “email.txt†(`Link <https://github.com/grnet/synnefo/blob/master/snf-astakos-app/astakos/im/templates/registration/email_change_email.txt>`_) * ``snf-astakos-app/astakos/im/templates/registration/password_email.txt`` Email sent for resetting password purpose. The user has to click on a link provided - to validate this action. Extends “email.txt†(`Link <https://code.grnet.gr/projects/synnefo/repository/revisions/master/changes/snf-astakos-app/astakos/im/templates/registration/password_email.txt>`_) + to validate this action. Extends “email.txt†(`Link <https://github.com/grnet/synnefo/blob/master/snf-astakos-app/astakos/im/templates/registration/password_email.txt>`_) * ``snf-astakos-app/astakos/im/templates/im/projects/project_approval_notification.txt`` Informs the project owner that his/her project has been approved. Extends - “email.txt†(`Link <https://code.grnet.gr/projects/synnefo/repository/revisions/master/changes/snf-astakos-app/astakos/im/templates/im/projects/project_approval_notification.txt>`_) + “email.txt†(`Link <https://github.com/grnet/synnefo/blob/master/snf-astakos-app/astakos/im/templates/im/projects/project_approval_notification.txt>`_) * ``snf-astakos-app/astakos/im/templates/im/projects/project_denial_notification.txt`` Informs the project owner that his/her project application has been denied - explaining the reasons. Extends “email.txt†(`Link <https://code.grnet.gr/projects/synnefo/repository/revisions/master/changes/snf-astakos-app/astakos/im/templates/im/projects/project_denial_notification.txt>`_) + explaining the reasons. Extends “email.txt†(`Link <https://github.com/grnet/synnefo/blob/master/snf-astakos-app/astakos/im/templates/im/projects/project_denial_notification.txt>`_) * ``snf-astakos-app/astakos/im/templates/im/projects/project_membership_change_notification.txt`` An email is sent to a user containing information about his project membership (whether he has been accepted, rejected or removed). Extends “email.txt†(`Link - <https://code.grnet.gr/projects/synnefo/repository/revisions/master/changes/snf-astakos-app/astakos/im/templates/im/projects/project_membership_change_notification.txt>`_) + <https://github.com/grnet/synnefo/blob/master/snf-astakos-app/astakos/im/templates/im/projects/project_membership_change_notification.txt>`_) * ``snf-astakos-app/astakos/im/templates/im/projects/project_membership_enroll_notification.txt`` Informs a user that he/she has been enrolled to a project. Extends - “email.txt†(`Link <https://code.grnet.gr/projects/synnefo/repository/revisions/master/changes/snf-astakos-app/astakos/im/templates/im/projects/project_membership_enroll_notification.txt>`_) + “email.txt†(`Link <https://github.com/grnet/synnefo/blob/master/snf-astakos-app/astakos/im/templates/im/projects/project_membership_enroll_notification.txt>`_) * ``snf-astakos-app/astakos/im/templates/im/projects/project_membership_leave_request_notification.txt`` An email is sent to the project owner to make him aware of a user having - requested to leave his project. Extends “email.txt†(`Link <https://code.grnet.gr/projects/synnefo/repository/revisions/master/changes/snf-astakos-app/astakos/im/templates/im/projects/project_membership_leave_request_notification.txt>`_) + requested to leave his project. Extends “email.txt†(`Link <https://github.com/grnet/synnefo/blob/master/snf-astakos-app/astakos/im/templates/im/projects/project_membership_leave_request_notification.txt>`_) * ``snf-astakos-app/astakos/im/templates/im/projects/project_membership_request_notification.txt`` An email is sent to the project owner to make him/her aware of a user having - requested to join his project. Extends “email.txt†(`Link <https://code.grnet.gr/projects/synnefo/repository/revisions/master/changes/snf-astakos-app/astakos/im/templates/im/projects/project_membership_request_notification.txt>`_) + requested to join his project. Extends “email.txt†(`Link <https://github.com/grnet/synnefo/blob/master/snf-astakos-app/astakos/im/templates/im/projects/project_membership_request_notification.txt>`_) * ``snf-astakos-app/astakos/im/templates/im/projects/project_suspension_notification.txt`` An email is sent to the project owner to make him/her aware of his/her project - having been suspended. Extends “email.txt†(`Link <https://code.grnet.gr/projects/synnefo/repository/revisions/master/changes/snf-astakos-app/astakos/im/templates/im/projects/project_suspension_notification.txt>`_) + having been suspended. Extends “email.txt†(`Link <https://github.com/grnet/synnefo/blob/master/snf-astakos-app/astakos/im/templates/im/projects/project_suspension_notification.txt>`_) * ``snf-astakos-app/astakos/im/templates/im/projects/project_termination_notification.txt`` An email is sent to the project owner to make him/her aware of his/her project - having been terminated. Extends “email.txt†(`Link <https://code.grnet.gr/projects/synnefo/repository/revisions/master/changes/snf-astakos-app/astakos/im/templates/im/projects/project_termination_notification.txt>`_) + having been terminated. Extends “email.txt†(`Link <https://github.com/grnet/synnefo/blob/master/snf-astakos-app/astakos/im/templates/im/projects/project_termination_notification.txt>`_) .. warning:: Django templates language: @@ -2260,7 +2541,7 @@ The RabbitMQ nodes that form the cluster, are declared to Synnefo through the `AMQP_HOSTS` setting. Each time a Synnefo component needs to connect to RabbitMQ, one of these nodes is chosen in a random way. The client that Synnefo uses to connect to RabbitMQ, handles connection failures transparently and -tries to reconnect to a different node. As long as one of these nodes are up +tries to reconnect to a different node. As long as one of these nodes is up and running, functionality of Synnefo should not be downgraded by the RabbitMQ node failures. @@ -2341,6 +2622,12 @@ The logging configuration dictionary is defined in The administrator can have logging control by modifying the ``LOGGING_SETUP`` dictionary, and defining subloggers with different handlers and log levels. +By default snf-manage will log any command that is being executed along with +its output under the directory ``LOG_DIR``/commands. The ``LOG_DIR`` directory +can be changed from the ``00-snf-common-admins.conf`` configuration file and +the whole snf-manage logging mechanism can be disabled by changing the +``LOGGER_EXCLUDE_COMMANDS`` setting to ".\*". + .. _scale-up: @@ -2540,6 +2827,112 @@ Node10: All sections: :ref:`Scale out Guide <i-synnefo>` +Regions, Zones and Clusters +=========================== + +Region +------ + +A Region is a single Synnefo installation, with +Compute/Network/Image/Volume/Object Store services. A Region is associated with +one set of Synnefo DBs (Astakos DB, Pithos DB and Cyclades DB). Every Region has a +distinct set of API endpoints, e.g., +`https://cloud.example.com/cyclades/compute/v2.0`. Two Regions are most times +located geographically far from each other, e.g. "Europe", "US-East". A Region +comprises multiple Zones. + +Zone +---- + +A Zone is a set of Ganeti clusters, in a potentially geographically distinct +location, e.g. "Athens", "Rome". All clusters have access to the same physical +networks, and are considered a single failure domain, e.g., they access the +network over the same router. A Zone comprises muliple Ganeti clusters. + +Ganeti cluster +-------------- + +A Ganeti cluster is a set of Ganeti nodes (physical machines). One of the nodes +has the role of "Ganeti master". If this node goes down, another node may +undertake the master role. Ganeti nodes run Virtual Machines (VMs). VMs can live +migrate inside a Ganeti cluster. A Ganeti cluster comprises multiple physical +hardware nodes, most times geographically close to each other. + +VM mobility +----------- + +VMs may move across Regions, Zones, Ganeti clusters and physical nodes. Before we +describe how that's possible, we will describe the different kinds of moving, +providing the corresponding terminology: + +Live migration +~~~~~~~~~~~~~~ + +The act of moving a running VM from physical node to physical node without any +impact on its operation. The VM continues to run on its new physical location, +completely unaffected, and without any service downtime or dropped connections. +Live migration typically requires shared storage and networking between the source +and destination nodes. + +Live migration is issued by the administrator in the background and is transparent +to the VM user. + +Failover +~~~~~~~~ + +The act of moving a VM from physical node to physical node by stopping it first on +the source node, then re-starting it on the destination node. There is short +service downtime, during the time the VM boots up, and client connections are +dropped. + +Failover is issued by the administrator in the background and the VM user will +experience a reboot. + +Snapshot Failover +~~~~~~~~~~~~~~~~~ + +The act of moving a VM from physical node to physical node via a point-in-time +snapshot. That is, stopping a VM on the source node, taking a snapshot, then +creating a new VM from that snapshot. + +Snapshot failover is issued by the VM user and not the administrator. + +Disaster Recovery +----------------- + +In Synnefo terminology, Disaster Recovery is the process of sustaining a disaster +in one datacenter, and ensuring business continuity by performing live migration +or failover of running/existing VMs, or respawning VMs from previously made +snapshots. Based on the method used, this can work inside a single Ganeti cluster, +across Ganeti clusters in the same Zone, or across Zones. + +Specifically: + +Live migration is only supported inside a single Ganeti cluster. Ganeti supports +live migration between nodes in the same cluster with or without shared storage. +Live migration is done at the Ganeti level and is transparent to Synnefo. + +Failover is supported inside a Ganeti cluster, across Ganeti clusters and across +Zones. Ganeti supports failover inside a Ganeti cluster with or without shared +storage, which poses minimum downtime for the VM. Failover inside the same Ganeti +cluster is done at the Ganeti level and is transparent to Synnefo. + +Ganeti also provides tools for failing over VMs across different Ganeti clusters, +meaning that one can use them to failover VMs across Ganeti clusters of the same +Zone or across Ganeti clusters of different Zones, thus moving across Zones. +Failing over across different Ganeti clusters requires copying of data, resulting +in longer downtimes, depending on the geographical distance and network between +them. Failover across Ganeti clusters, either in the same or different Zones, is +not transparent to Synnefo and requires manual import of intances at Synnefo level +too, by the administrator. + +Snapshot failover supports moving VMs across all domains. It is issued by the VM +user and is done at the Synnefo level without the need of running anything at the +Ganeti level or by the administrator. + +In the future Synnefo will also support moving VMs across different Regions. + + Upgrade Notes ============= @@ -2554,12 +2947,14 @@ Upgrade Notes v0.14.9 -> v0.14.10 <upgrade/upgrade-0.14.10> v0.14 -> v0.15 <upgrade/upgrade-0.15> v0.15 -> v0.15.1 <upgrade/upgrade-0.15.1> + v0.15 -> v0.16 <upgrade/upgrade-0.16> Changelog, NEWS =============== +* v0.16 :ref:`Changelog <Changelog-0.16>`, :ref:`NEWS <NEWS-0.16>` * v0.15.2 :ref:`Changelog <Changelog-0.15.1>`, :ref:`NEWS <NEWS-0.15.2>` * v0.15.1 :ref:`Changelog <Changelog-0.15.1>`, :ref:`NEWS <NEWS-0.15.1>` * v0.15 :ref:`Changelog <Changelog-0.15>`, :ref:`NEWS <NEWS-0.15>` diff --git a/docs/api-guide.rst b/docs/api-guide.rst index b08f064fe9c1d988e668f97b573f137d83fc8f6c..9b69124228a07e1d88c2e66e76ff30fc0bd543e9 100644 --- a/docs/api-guide.rst +++ b/docs/api-guide.rst @@ -17,13 +17,14 @@ Most Synnefo services have a corresponding OpenStack API: | Cyclades/Compute Service -> OpenStack Compute API | Cyclades/Network Service -> OpenStack Networking ("Neutron") API | Cyclades/Image Service -> OpenStack Image ("Glance") API +| Cyclades/Block Storage Service -> OpenStack Block Storage ("Cinder") API | Pithos/Storage Service -> OpenStack Object Storage API | Astakos/Identity Service -> OpenStack Identity ("Keystone") API | Astakos/Quota Service -> Proprietary API | Astakos/Resource Service -> Proprietary API | Astakos/Project Service -> Proprietary API -Below, we will describe all Synnefo APIs with conjuction to the OpenStack APIs. +Below, we will describe all Synnefo APIs with conjunction to the OpenStack APIs. Identity Service API (Astakos) @@ -51,6 +52,18 @@ following Synnefo specific (proprietary) API: Resource and Quota API <quota-api-guide.rst> +Weblogin Service API (Astakos) +============================== + +The Weblogin Service is implemented inside Astakos and have the +following Synnefo API: + +.. toctree:: + :maxdepth: 2 + + Weblogin API <weblogin-api-guide.rst> + + Project Service API =================== @@ -105,6 +118,20 @@ This is the Cyclades/Image API: Image API (Glance) <image-api-guide> +Block Storage Service API (Cyclades) +==================================== + +The Block Storage Service is implemented inside Cyclades. It exposes the +OpenStack Block Storage ("Cinder") API with minor changes wherever needed. + +This is the Cyclades/Block Storage API: + +.. toctree:: + :maxdepth: 2 + + Block Storage API (Cinder) <blockstorage-api-guide> + + Storage Service API (Pithos) ============================ diff --git a/docs/archipelago.rst b/docs/archipelago.rst deleted file mode 100644 index a461fe0bc531902b3a89a0080a1294e9d28516e2..0000000000000000000000000000000000000000 --- a/docs/archipelago.rst +++ /dev/null @@ -1,22 +0,0 @@ -.. _archipelago: - -Volume Service (archipelago) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Introduction -============ - -Every Volume inside a VM can be thought of as a linearly addressable set of -fixed-size blocks. The storage of the actual blocks is orthogonal to the task of -exposing a single block device for use by each VM. Bridging the gap between the -VMs performing random access to Volumes and the storage of actual blocks is -Archipelago: a custom storage handling layer which handled volumes as set of -distinct blocks in the backend, a process we call volume composition. For the -actual storage of blocks we are currently experimenting with RADOS, the -distributed object store underlying the Ceph parallel filesystem, to solve the -problem of reliable, fault-tolerant object storage through replication on -multiple storage nodes. Archipelago itself is agnostic to the actual block -storage backend. - -Archipelago is under active development and will be available soon. - diff --git a/docs/astakos.rst b/docs/astakos.rst index e58c617d99a8edcc56797db7d6ecc96d602e3bae..75c7839c8dc4c8b663a77a66e9ca51b182e2c73f 100644 --- a/docs/astakos.rst +++ b/docs/astakos.rst @@ -29,5 +29,5 @@ authentication, along with the Synnefo Account API for quota, user group and project management. Please also see the :ref:`Admin Guide <admin-guide>` for more information and the -Installation Guide (:ref: `Debian <install-guide-debian>`/:ref: `CentOS +Installation Guide (:ref:`Debian <install-guide-debian>` / :ref:`CentOS <install-guide-centos>`) for installation instructions. diff --git a/docs/blockstorage-api-guide.rst b/docs/blockstorage-api-guide.rst new file mode 100644 index 0000000000000000000000000000000000000000..a3b62950146f6ad98526bdd52db0d32765836143 --- /dev/null +++ b/docs/blockstorage-api-guide.rst @@ -0,0 +1,1351 @@ +.. _blockstorage-api-guide: + +API Guide +********* + +:ref:`Cyclades <cyclades>` include the Block Storage Service of +`Synnefo <http://www.synnefo.org>`_. The Cyclades/Block Storage API complies +with +`OpenStack Block Storage <http://developer.openstack.org/api-ref-blockstorage-v2.html>`_ +version 2, with custom extensions when needed. + +This document's goals are: + +* Define the Cyclades/Block Storage REST API +* Clarify the differences between Cyclades and OpenStack/Block Storage + +Users and developers who wish to access Cyclades through its REST API are +advised to use the +`kamaki <http://www.synnefo.org/docs/kamaki/latest/index.html>`_ command-line +client and the associated python library, instead of making direct calls. + +General API Information +======================= + +Authentication +-------------- + +All requests use the same authentication method: an ``X-Auth-Token`` header is +passed to the service, which is used to authenticate the user and retrieve user +related information. No other user details are passed through HTTP. + + +API Operations +============== + +.. rubric:: Volumes + +==================================== =============================== ====== ======== ========== +Description URI Method Cyclades OS/Block Storage +==================================== =============================== ====== ======== ========== +`List <#list-volumes>`_ ``/volumes`` GET ✔ ✔ +\ ``/volumes/detail`` GET ✔ ✔ +`Create <#create-volume>`_ ``/volumes`` POST ✔ ✔ +`Get Details <#get-volume-details>`_ ``/volumes/<volume id>`` GET ✔ ✔ +`Update <#update-volume>`_ ``/volumes/<volume id>`` PUT ✔ ✔ +`Delete <#delete-volume>`_ ``/volumes/<volume id>`` DELETE ✔ ✔ +`Reassign <#reassign-volume>`_ ``/volumes/<volume id>/action`` POST ✔ **✘** +==================================== =============================== ====== ======== ========== + +.. rubric:: Snapshots + +====================================== ============================ ====== ======== ========== +Description URI Method Cyclades OS/Block Storage +====================================== ============================ ====== ======== ========== +`List <#list-snapshots>`_ ``/snapshots`` GET ✔ ✔ +\ ``/snapshots/detail`` GET ✔ ✔ +`Create <#create-snapshot>`_ ``/snapshots`` POST ✔ ✔ +`Get Details <#get-snapshot-details>`_ ``/snapshots/<snapshot id>`` GET ✔ ✔ +`Update <#update-snapshot>`_ ``/snapshots/<snapshot id>`` PUT ✔ ✔ +`Delete <#delete-snapshot>`_ ``/snapshots/<snapshot id>`` DELETE ✔ ✔ +====================================== ============================ ====== ======== ========== + +.. rubric:: Volume types + +========================================= ================================== ====== ======== ========== +Description URI Method Cyclades OS/Block Storage +========================================= ================================== ====== ======== ========== +`List <#list-volume-types>`_ ``/volume-types`` GET ✔ ✔ +`Get Details <#get-volume-type-details>`_ ``/volume-types/<volume-type id>`` GET ✔ ✔ +========================================= ================================== ====== ======== ========== + +List Volumes +------------ + +List all volumes owned by the user. + +.. rubric:: Request + +=================== ====== ======== ========== +URI Method Cyclades OS/Block Storage +=================== ====== ======== ========== +``/volumes`` GET ✔ ✔ +``/volumes/detail`` GET ✔ ✔ +=================== ====== ======== ========== + +* Both requests return a list of volumes. The first returns just ``id``, + ``display_name`` and ``links``, while the second returns the + `full collection <#volume-ref>`_ of volume attributes + +| +============== ========================= ======== ========== +Request Header Value Cyclades OS/Block Storage +============== ========================= ======== ========== +X-Auth-Token User authentication token required required +============== ========================= ======== ========== + +.. rubric:: Response + +=========================== ===================== +Return Code Description +=========================== ===================== +200 (OK) Request succeeded +401 (Unauthorized) Missing or expired user token +500 (Internal Server Error) The request cannot be completed because of an +\ internal error +503 (Service Unavailable) The service is not currently available +=========================== ===================== + +| + +Response body contents:: + + volumes: [ + { + <volume attribute>: <value>, + ... + }, ... + ] + +The volume attributes are listed `here <#volume-ref>`_ + +*Example List Volumes: JSON (regular)* + +.. code-block:: javascript + + GET https://example.org/cyclades/v2/volumes + + { + "volumes": [ + { + "links": [ + { + "href": "https://example.org/cyclades/v2/volumes/42", + "rel": "self" + }, { + "href": "https://example.org/cyclades/v2/volumes/42", + "rel": "bookmark" + } + ], + "id": "42", + "display_name": "Volume One", + }, { + "links": [ + { + "href": "https://example.org/cyclades/v2/volumes/43", + "rel": "self" + }, { + "href": "https://example.org/cyclades/v2/volumes/43", + "rel": "bookmark" + } + ], + "id": "43", + "display_name": "Volume Two", + } + ] + } + +*Example List Volumes: JSON (detail)* + +.. code-block:: javascript + + GET https://example.org/cyclades/v2/volumes/detail + + { + "volumes": [ + { + "links": [ + { + "href": "https://example.org/cyclades/v2/volumes/42", + "rel": "self" + }, { + "href": "https://example.org/cyclades/v2/volumes/42", + "rel": "bookmark" + } + ], + "id": "42", + "display_name": "Volume One", + "status": "AVAILABLE", + "size": 2, + "display_description": "The First Volume", + "created_at": "2014-02-21T19:52:04.949734", + "metadata": {}, + "snapshot_id": null, + "source_volid": null, + "image_id": null, + "attachments": [], + "volume_type": 1, + "delete_on_termination": True, + "project": "1234" + }, { + "links": [ + { + "href": "https://example.org/cyclades/v2/volumes/43", + "rel": "self" + }, { + "href": "https://example.org/cyclades/v2/volumes/43", + "rel": "bookmark" + } + ], + "id": "43", + "display_name": "Volume Two", + "status": "AVAILABLE", + "size": 3, + "display_description": "The Second Volume", + "created_at": "2014-03-21T19:52:04.949734", + "metadata": {"requested_by": "John"}, + "snapshot_id": null, + "source_volid": null, + "image_id": null, + "attachments": [], + "volume_type": 2, + "delete_on_termination": False, + "project": "1234" + }, + ] + } + +Get Volume Details +------------------ + +This operation returns detailed information for a volume + +.. rubric:: Request + +======================== ====== ======== ========== +URI Method Cyclades OS/Block Storage +======================== ====== ======== ========== +``/volumes/<volume id>`` GET ✔ ✔ +======================== ====== ======== ========== + +| + +============== ========================= ======== ========== +Request Header Value Cyclades OS/Block Storage +============== ========================= ======== ========== +X-Auth-Token User authentication token required required +============== ========================= ======== ========== + +.. rubric:: Response + +=========================== ===================== +Return Code Description +=========================== ===================== +200 (OK) Request succeeded +400 (Bad Request) Malformed volume id +401 (Unauthorized) Missing or expired user token +404 (Not Found) Volume not found +500 (Internal Server Error) The request cannot be completed because of an +\ internal error +503 (Service Unavailable) No available backends or service currently +\ unavailable +=========================== ===================== + +| + +Response body contents:: + + volume: { + <volume attribute>: <value>, + ... + } + +Volume attributes are explained `here <#volume-ref>`_ + +*Example Get Volume Response* + +.. code-block:: javascript + + GET https://example.org/cyclades/v2/volumes/44 + + { + "volume": { + "links": [ + { + "href": "https://example.org/cyclades/v2/volumes/44", + "rel": "self" + }, { + "href": "https://example.org/cyclades/v2/volumes/44", + "rel": "bookmark" + } + ], + "id": "44", + "display_name": "Volume Three", + "status": "CREATING", + "size": 10, + "display_description": null, + "created_at": "2014-05-13T19:52:04.949734", + "metadata": {}, + "snapshot_id": null, + "source_volid": null, + "image_id": null, + "attachments": [], + "volume_type": 2, + "delete_on_termination": False, + "project": "1234" + } + } + +Create Volume +------------- + +Create a new volume + +.. rubric:: Request + +============ ====== ======== ========== +URI Method Cyclades OS/Block Storage +============ ====== ======== ========== +``/volumes`` POST ✔ ✔ +============ ====== ======== ========== + +| +============== ========================= ======== ========== +Request Header Value Cyclades OS/Block Storage +============== ========================= ======== ========== +X-Auth-Token User authentication token required required +Content-Type Type or request body required required +Content-Length Length of request body required required +============== ========================= ======== ========== + +Request body contents:: + + volume: { + <volume attribute>: <value>, + ... + } + +=================== ================================ ======== ================ +Volume Attribute Value Cyclades OS/Block Storage +=================== ================================ ======== ================ +size Volume size in GB required required +server_id An existing VM to create from required **✘** +availability_zone Respond in xml **✘** ✔ +source_volid Existing volume to create from **✘** ✔ +display_description A description ✔ ✔ +snapshot_id Existing snapshot to create from ✔ ✔ +display_name The name required ✔ +imageRef Image to create from ✔ ✔ +volume_type The associated volume type ✔ ✔ +bootable Whether the volume is bootable **✘** ✔ +metadata Key-Value metadata pairs ✔ ✔ +project Assigned project for quotas ✔ **✘** +=================== ================================ ======== ================ + +*Example Create Volume Request: JSON* + +.. code-block:: javascript + + POST https://example.org/cyclades/v2/volumes + + { + "volume": { + "size": 10, + "display_name": "Volume Three", + "server_id": "117", + "volume_type": 1, + } + } + +.. rubric:: Response + +=========================== ===================== +Return Code Description +=========================== ===================== +202 (OK) Request succeeded +400 (Bad Request) Malformed request data +401 (Unauthorized) Missing or expired user token +403 (Forbidden) User is not allowed to perform this operation +404 (Not Found) Resource (server_id, imageRef, etc,) not found +413 (Over Limit) Exceeded some resource limit +500 (Internal Server Error) The request cannot be completed because of an +\ internal error +503 (Service Unavailable) No available backends or service currently +\ unavailable +=========================== ===================== + +| + +Response body contents:: + + volume: { + <volume attribute>: <value>, + ... + } + +Volume attributes are `listed here <#server-ref>`_. + +*Example Create Volume Response: JSON* + +.. code-block:: javascript + + { + "volume": { + "links": [ + { + "href": "https://example.org/cyclades/v2/volumes/44", + "rel": "self" + }, { + "href": "https://example.org/cyclades/v2/volumes/44", + "rel": "bookmark" + } + ], + "id": "44", + "display_name": "Volume Three", + "status": "CREATING", + "size": 10, + "display_description": null, + "created_at": "2014-05-13T19:52:04.949734", + "metadata": {}, + "snapshot_id": null, + "source_volid": null, + "image_id": null, + "attachments": [], + "volume_type": 1, + "delete_on_termination": True, + "project": "1234" + } + } + +Update Volume +------------- + +.. rubric:: Response + +======================== ====== ======== ========== +URI Method Cyclades OS/Block Storage +======================== ====== ======== ========== +``/volumes/<volume id>`` PUT ✔ ✔ +======================== ====== ======== ========== + +| +============== ========================= ======== ========== +Request Header Value Cyclades OS/Block Storage +============== ========================= ======== ========== +X-Auth-Token User authentication token required required +Content-Type Type or request body required required +Content-Length Length of request body required required +============== ========================= ======== ========== + +Request body contents:: + + volume: { + <volume attribute>: <value>, + ... + } + +===================== ===================== ======== ========== +Attribute Description Cyclades OS/Block Storage +===================== ===================== ======== ========== +display_name Server name ✔ ✔ +display_description Descrition ✔ ✔ +delete_on_termination Switch this attribute ✔ **✘** +===================== ===================== ======== ========== + +*Example Rename Server Request: JSON* + +.. code-block:: javascript + + POST https://example.org/cyclades/v2/volumes/42 + + {"volume": {"display_name": "New name"}} + +.. rubric:: Response + +=========================== ===================== +Return Code Description +=========================== ===================== +200 (OK) Request succeeded +400 (Bad Request) Malformed request +401 (Unauthorized) Missing or expired user token +403 (Forbidden) User is not allowed to perform this operation +404 (Not Found) Volume not found +409 (Build In Progress) Volume is not ready yet +500 (Internal Server Error) The request cannot be completed because of an +\ internal error +503 (Service Unavailable) No available backends or service currently +\ unavailable +=========================== ===================== + +Response body contents:: + + volume: { + <volume attribute>: <value>, + ... + } + +Volume attributes are explained `here <#volume-ref>`_ + +*Example update volume Response* + +.. code-block:: javascript + + { + "volume": { + "id": "42", + "display_name": "New Name", + ... + } + } + +Update Volume Metadata +---------------------- + +.. rubric:: Response + +================================= ======== ======== ========== +URI Method Cyclades OS/Block Storage +================================= ======== ======== ========== +``/volumes/<volume id>/metadata`` POST/PUT ✔ ✔ +================================= ======== ======== ========== + +* POST will create new metadata for the specified Volume if the key doesn't + exist, while it will update metadata for which the key already exists. +* PUT will delete any old existing metadata and it'll replace them with + the ones specified in the request. + +============== ========================= ======== ========== +Request Header Value Cyclades OS/Block Storage +============== ========================= ======== ========== +X-Auth-Token User authentication token required required +Content-Type Type or request body required required +Content-Length Length of request body required required +============== ========================= ======== ========== + +Request body contents:: + + volume: { + <key>: <value>, + ... + } + +*Example Append Metadata Request: JSON* + +.. code-block:: javascript + + POST https://example.org/cyclades/v2/volumes/42/metadata + + {"metadata": {"key_to_append": "value_to_append"}} + +.. rubric:: Response + +=========================== ===================== +Return Code Description +=========================== ===================== +200 (OK) Request succeeded +400 (Bad Request) Malformed request +401 (Unauthorized) Missing or expired user token +403 (Forbidden) User is not allowed to perform this operation +404 (Not Found) Volume not found +500 (Internal Server Error) The request cannot be completed because of an +\ internal error +503 (Service Unavailable) No available backends or service currently +\ unavailable +=========================== ===================== + +Response body contents:: + + metadata: { + <key>: <value>, + ... + } + +*Example update volume Response* + +.. code-block:: javascript + + { + "metadata": { + "key1": "value1", + "key2": "value2", + ... + } + } + +Delete Volume +------------- + +.. rubric:: Request + +======================== ====== ======== ========== +URI Method Cyclades OS/Block Storage +======================== ====== ======== ========== +``/volumes/<volume id>`` DELETE ✔ ✔ +======================== ====== ======== ========== + +| +============== ========================= ======== ========== +Request Header Value Cyclades OS/Block Storage +============== ========================= ======== ========== +X-Auth-Token User authentication token required required +============== ========================= ======== ========== + +.. rubric:: Response + +=========================== ===================== +Return Code Description +=========================== ===================== +202 (OK) Request succeeded +400 (Bad Request) Malformed server id +401 (Unauthorized) Missing or expired user token +404 (Not Found) Volume not found +409 (Build In Progress) Volume is not ready yet +500 (Internal Server Error) The request cannot be completed because of an +\ internal error +503 (Service Unavailable) Action not supported or service currently +\ unavailable +=========================== ===================== + +Reassign Volume +--------------- + +Reassign the volume to a (different) project (change quota limits) + +=============================== ====== ======== ========== +URI Method Cyclades OS/Block Storage +=============================== ====== ======== ========== +``/volumes/<volume id>/action`` POST ✔ ✔ +=============================== ====== ======== ========== + +| +============== ========================= ======== ========== +Request Header Value Cyclades OS/Block Storage +============== ========================= ======== ========== +X-Auth-Token User authentication token required required +============== ========================= ======== ========== + +.. rubric:: Request + +Request body contents:: + + reassign: {project: <project id>} + +*Example reassign volume Request* + +.. code-block:: javascript + + POST https://example.org//cyclades/v2/volumes/42/action + + {"reassign": {"project": "4321"}} + + +.. rubric:: Response + +=========================== ===================== +Return Code Description +=========================== ===================== +200 (OK) Request succeeded +400 (Bad Request) Malformed request +401 (Unauthorized) Missing or expired user token +403 (Forbidden) User is not allowed to perform this operation +404 (Not Found) Volume not found +409 (Build In Progress) Volume is not ready yet +500 (Internal Server Error) The request cannot be completed because of an +\ internal error +503 (Service Unavailable) No available backends or service currently +\ unavailable +=========================== ===================== + + +List Snapshots +-------------- + +List all snapshots related to the user. + +.. rubric:: Request + +====================== ====== ======== ========== +URI Method Cyclades OS/Block Storage +====================== ====== ======== ========== +``/snapshots`` GET ✔ ✔ +``/snapshots/detail`` GET ✔ ✔ +====================== ====== ======== ========== + +* Both requests return a list of snapshots. The first returns just ``id``, + ``display_name`` and ``links``, while the second returns the + `full collection <#snapshot-ref>`_ of snapshot attributes + +| +============== ========================= ======== ========== +Request Header Value Cyclades OS/Block Storage +============== ========================= ======== ========== +X-Auth-Token User authentication token required required +============== ========================= ======== ========== + +.. rubric:: Response + +=========================== ===================== +Return Code Description +=========================== ===================== +200 (OK) Request succeeded +401 (Unauthorized) Missing or expired user token +500 (Internal Server Error) The request cannot be completed because of an +\ internal error +503 (Service Unavailable) The service is not currently available +=========================== ===================== + +| + +Response body contents:: + + snapshots: [ + { + <snapshot attribute>: <value>, + ... + }, ... + ] + +The snapshot attributes are listed `here <#snapshot-ref>`_ + +*Example List Snapshots: JSON (regular)* + +.. code-block:: javascript + + GET https://example.org/cyclades/v2/snapshots + + { + "snapshots": [ + { + "links": [ + { + "href": "https://example.org/cyclades/v2/snapshots/42", + "rel": "self" + }, { + "href": "https://example.org/cyclades/v2/snapshots/42", + "rel": "bookmark" + } + ], + "id": "42", + "display_name": "Snapshot One", + "status": "AVAILABLE", + "size": 2, + "display_description": null, + "created_at": "2014-05-19T19:52:04.949734", + "metadata": {}, + "volume_id": "123", + "os-extended-snapshot-attribute:progress": "100%" + }, { + "links": [ + { + "href": "https://example.org/cyclades/v2/snapshots/43", + "rel": "self" + }, { + "href": "https://example.org/cyclades/v2/snapshots/43", + "rel": "bookmark" + } + ], + "id": "43", + "display_name": "Snapshot Two", + "status": "AVAILABLE", + "size": 3, + "display_description": null, + "created_at": "2014-05-20T19:52:04.949734", + "metadata": {}, + "volume_id": "124", + "os-extended-snapshot-attribute:progress": "100%" + } + ] + } + +*Example List Snapshots: JSON (detail)* + +.. code-block:: javascript + + GET https://example.org/cyclades/v2/snapshots/detail + + { + "snapshots": [ + { + "links": [ + { + "href": "https://example.org/cyclades/v2/snapshots/42", + "rel": "self" + }, { + "href": "https://example.org/cyclades/v2/snapshots/42", + "rel": "bookmark" + } + ], + "id": "42", + "display_name": "Snapshot One", + "status": "AVAILABLE", + "size": 2, + "display_description": null, + "created_at": "2014-05-19T19:52:04.949734", + "metadata": {}, + "volume_id": "123", + "os-extended-snapshot-attribute:progress": "100%" + }, { + "links": [ + { + "href": "https://example.org/cyclades/v2/snapshots/43", + "rel": "self" + }, { + "href": "https://example.org/cyclades/v2/snapshots/43", + "rel": "bookmark" + } + ], + "id": "43", + "display_name": "Snapshot Two", + "status": "AVAILABLE", + "size": 3, + "display_description": null, + "created_at": "2014-05-20T19:52:04.949734", + "metadata": {}, + "volume_id": "124", + "os-extended-snapshot-attribute:progress": "100%" + } + ] + } + +Get Snapshot Details +-------------------- + +This operation returns detailed information for a snapshot + +.. rubric:: Request + +============================ ====== ======== ========== +URI Method Cyclades OS/Block Storage +============================ ====== ======== ========== +``/snapshots/<snapshot id>`` GET ✔ ✔ +============================ ====== ======== ========== + +| + +============== ========================= ======== ========== +Request Header Value Cyclades OS/Block Storage +============== ========================= ======== ========== +X-Auth-Token User authentication token required required +============== ========================= ======== ========== + +.. rubric:: Response + +=========================== ===================== +Return Code Description +=========================== ===================== +200 (OK) Request succeeded +400 (Bad Request) Malformed volume id +401 (Unauthorized) Missing or expired user token +404 (Not Found) Snapshot not found +500 (Internal Server Error) The request cannot be completed because of an +\ internal error +503 (Service Unavailable) No available backends or service currently +\ unavailable +=========================== ===================== + +| + +Response body contents:: + + snapshot: { + <snapshot attribute>: <value>, + ... + } + +Snapshot attributes are explained `here <#snapshot-ref>`_ + +*Example Get Snapshot Response* + +.. code-block:: javascript + + GET https://example.org/cyclades/v2/snapshots/sn4p5h071 + + { + "snapshot": { + "links": [ + { + "href": "https://example.org/cyclades/v2/snapshots/42", + "rel": "self" + }, { + "href": "https://example.org/cyclades/v2/snapshots/42", + "rel": "bookmark" + } + ], + "id": "42", + "display_name": "Snapshot One", + "status": "AVAILABLE", + "size": 2, + "display_description": null, + "created_at": "2014-05-19T19:52:04.949734", + "metadata": {}, + "volume_id": "123", + "os-extended-snapshot-attribute:progress": "100%", + } + } + +Create Snapshot +--------------- + +Create a new snapshot + +.. rubric:: Request + +============== ====== ======== ========== +URI Method Cyclades OS/Block Storage +============== ====== ======== ========== +``/snapshots`` POST ✔ ✔ +============== ====== ======== ========== + +| +============== ========================= ======== ========== +Request Header Value Cyclades OS/Block Storage +============== ========================= ======== ========== +X-Auth-Token User authentication token required required +Content-Type Type or request body required required +Content-Length Length of request body required required +============== ========================= ======== ========== + +Request body contents:: + + snapshot: { + <snapshot attribute>: <value>, + ... + } + +=================== ================================ ======== ================ +Volume Attribute Value Cyclades OS/Block Storage +=================== ================================ ======== ================ +volume_id Volume to create snapshot from required required +display_name The name ✔ ✔ +display_description A description ✔ ✔ +force Whether to snapshot **✘** ✔ +=================== ================================ ======== ================ + +*Example Create Volume Request: JSON* + +.. code-block:: javascript + + POST https://example.org/cyclades/v2/volumes + + { + "volume": { + "volume_id": "44", + "display_name": "Snapshot Three" + } + } + +.. rubric:: Response + +=========================== ===================== +Return Code Description +=========================== ===================== +202 (OK) Request succeeded +400 (Bad Request) Malformed request data +401 (Unauthorized) Missing or expired user token +403 (Forbidden) User is not allowed to perform this operation +404 (Not Found) Snapshot not found +413 (Over Limit) Exceeded some resource limit +500 (Internal Server Error) The request cannot be completed because of an +\ internal error +503 (Service Unavailable) No available backends or service currently +\ unavailable +=========================== ===================== + +| + +Response body contents:: + + snapshot: { + <snapshot attribute>: <value>, + ... + } + +Snapshots attributes are `listed here <#snapshot-ref>`_. + +*Example Create Snapshot Response: JSON* + +.. code-block:: javascript + + { + "snapshot": { + "links": [ + { + "href": "https://example.org/cyclades/v2/snapshots/44", + "rel": "self" + }, { + "href": "https://example.org/cyclades/v2/snapshots/44", + "rel": "bookmark" + } + ], + "id": "44", + "display_name": "Snapshot Three", + "status": "CREATING", + "size": 10, + "display_description": null, + "created_at": "2014-05-19T19:52:04.949734", + "metadata": {}, + "volume_id": "123", + "os-extended-snapshot-attribute:progress": "100%", + } + } + +Update Snapshot +--------------- + +.. rubric:: Response + +============================ ====== ======== ========== +URI Method Cyclades OS/Block Storage +============================ ====== ======== ========== +``/snapshots/<snapshot id>`` PUT ✔ ✔ +============================ ====== ======== ========== + +| +============== ========================= ======== ========== +Request Header Value Cyclades OS/Block Storage +============== ========================= ======== ========== +X-Auth-Token User authentication token required required +Content-Type Type or request body required required +Content-Length Length of request body required required +============== ========================= ======== ========== + +Request body contents:: + + snapshot: { + <snapshot attribute>: <value>, + ... + } + +=================== ===================== ======== ========== +Attribute Description Cyclades OS/Block Storage +=================== ===================== ======== ========== +display_name Server name ✔ ✔ +display_description Descrition ✔ ✔ +=================== ===================== ======== ========== + +*Example Rename Server Request: JSON* + +.. code-block:: javascript + + POST https://example.org/cyclades/v2/snapshots/44 + + {"snapshot": {"display_name": "New name"}} + +.. rubric:: Response + +=========================== ===================== +Return Code Description +=========================== ===================== +200 (OK) Request succeeded +400 (Bad Request) Malformed request +401 (Unauthorized) Missing or expired user token +403 (Forbidden) User is not allowed to perform this operation +404 (Not Found) Snapshot not found +409 (Build In Progress) Snapshot is not ready yet +500 (Internal Server Error) The request cannot be completed because of an +\ internal error +503 (Service Unavailable) No available backends or service currently +\ unavailable +=========================== ===================== + +Response body contents:: + + snapshot: { + <snapshot attribute>: <value>, + ... + } + +Snapshot attributes are explained `here <#snapshot-ref>`_ + +*Example update snapshot Response* + +.. code-block:: javascript + + { + "snapshot": { + "id": "44", + "display_name": "New Name", + ... + } + } + +Delete Snapshot +--------------- + +.. rubric:: Request + +============================ ====== ======== ========== +URI Method Cyclades OS/Block Storage +============================ ====== ======== ========== +``/snapshots/<snapshot id>`` DELETE ✔ ✔ +============================ ====== ======== ========== + +| +============== ========================= ======== ========== +Request Header Value Cyclades OS/Block Storage +============== ========================= ======== ========== +X-Auth-Token User authentication token required required +============== ========================= ======== ========== + +.. rubric:: Response + +=========================== ===================== +Return Code Description +=========================== ===================== +202 (OK) Request succeeded +400 (Bad Request) Malformed server id +401 (Unauthorized) Missing or expired user token +404 (Not Found) Snapshot not found +409 (Build In Progress) Snapshot is not ready yet +500 (Internal Server Error) The request cannot be completed because of an +\ internal error +503 (Service Unavailable) Action not supported or service currently +\ unavailable +=========================== ===================== + + +List Volume Types +----------------- + +.. rubric:: Request + +========== ====== ======== ========== +URI Method Cyclades OS/Block Storage +========== ====== ======== ========== +``/types`` GET ✔ ✔ +========== ====== ======== ========== + +| +============== ========================= ======== ========== +Request Header Value Cyclades OS/Block Storage +============== ========================= ======== ========== +X-Auth-Token User authentication token required required +============== ========================= ======== ========== + +.. rubric:: Response + +=========================== ===================== +Return Code Description +=========================== ===================== +200 (OK) Request succeeded +401 (Unauthorized) Missing or expired user token +500 (Internal Server Error) The request cannot be completed because of an +\ internal error +503 (Service Unavailable) The service is not currently available +=========================== ===================== + +| + +Response body contents:: + + volume_types: [ + { + <volume type attribute>: <value>, + ... + }, ... + ] + +The volume type attributes are listed `here <#volume-type-ref>`_ + +*Example List Volumes: JSON (regular)* + +.. code-block:: javascript + + GET https://example.org/cyclades/v2/types + + { + "volumes": [ + { + "id": 1, + "display_name": "Basic type", + "extra_specs": {...} + }, { + "id": 2, + "display_name": "Special type", + "extra_specs": {...} + } + ] + } + +Get Volume Type Details +----------------------- + +This operation returns detailed information for a volume type + +.. rubric:: Request + +=========================== ====== ======== ========== +URI Method Cyclades OS/Block Storage +=========================== ====== ======== ========== +``/types/<volume type id>`` GET ✔ ✔ +=========================== ====== ======== ========== + +| + +============== ========================= ======== ========== +Request Header Value Cyclades OS/Block Storage +============== ========================= ======== ========== +X-Auth-Token User authentication token required required +============== ========================= ======== ========== + +.. rubric:: Response + +=========================== ===================== +Return Code Description +=========================== ===================== +200 (OK) Request succeeded +400 (Bad Request) Malformed volume type id +401 (Unauthorized) Missing or expired user token +404 (Not Found) Volume type not found +500 (Internal Server Error) The request cannot be completed because of an +\ internal error +503 (Service Unavailable) No available backends or service currently +\ unavailable +=========================== ===================== + +| + +Response body contents:: + + volume_type: { + <volume type attribute>: <value>, + ... + } + +Volume attributes are explained `here <#volume-type-ref>`_ + +*Example Get Volume Response* + +.. code-block:: javascript + + GET https://example.org/cyclades/v2/types/1 + + { + "volume_type": { + "id": 1, + "display_name": "Volume Three", + "extra_specs": {...} + } + } + + +Index of Attributes +------------------- + +.. _volume-ref: + +Volume Attributes +................. + +===================== ======== ================ +Volume attribute Cyclades OS/Block Storage +===================== ======== ================ +id ✔ ✔ +display_name ✔ ✔ +links ✔ ✔ +status ✔ ✔ +size ✔ ✔ +display_description ✔ ✔ +created_at ✔ ✔ +metadata ✔ ✔ +snapshot_id ✔ ✔ +source_volid ✔ ✔ +attachments ✔ ✔ +volume_type ✔ ✔ +delete_on_termination ✔ ✔ +image_id ✔ **✘** +project ✔ **✘** +availability_zone **✘** ✔ +bootable **✘** ✔ +===================== ======== ================ + +* **id** The unique volume ID + +* **display_name** A name for the volume + +* **links** The reference links for the volume + +* **status** The volume status can be CREATING, AVAILABLE or DELETED + +* **size** The size of the volume in GB + +* **display_description** A description of the volume + +* **created_at** Date and time of volumes' creation + +* **metadata** A list of key-value metadata pairs + +* **snapshot_id** The ID of the snapshot this volume was created from + +* **source_volid** The ID of the source volume, this volume was created from + +* **attachments** One or more instance attachments + +* **volume_type** The type of the volume (See Volume types API) + +* **delete_on_termination** Whether this volume will be deleted on termination + +* **image_id** The ID of the image this volume was created from + +* **project** The ID of the project this volume is assigned to (quotas) + +.. _snapshot-ref: + +Snapshot Attributes +................... + +========================================= ======== ================ +Snapshot attribute Cyclades OS/Block Storage +========================================= ======== ================ +id ✔ ✔ +display_name ✔ ✔ +links ✔ ✔ +display_description ✔ ✔ +status ✔ ✔ +created_at ✔ ✔ +size ✔ ✔ +volume_id ✔ ✔ +metadata ✔ ✔ +os-extended-snapshot-attribute:progress ✔ ✔ +os-extended-snapshot-attribute:project_id **✘** ✔ +========================================= ======== ================ + +* **id** The unique snapshot ID + +* **display_name** A name for the snapshot + +* **links** The reference links for the snapshot + +* **status** The snapshot status can be CREATING, AVAILABLE or DELETED + +* **size** The size of the snapshot in GB + +* **display_description** A description of the snapshot + +* **created_at** Date and time of snapshots' creation + +* **volume_id** The volume this is a snapshot of + +* **metadata** A list of key-value metadata pairs + +* **os-extended-snapshot-attribute:progress** creation progress + +.. _volume-type-ref: + +Volume Type Attributes +...................... + +================== ======== ================ +Snapshot attribute Cyclades OS/Block Storage +================== ======== ================ +id ✔ ✔ +display_name ✔ ✔ +extra_specs ✔ ✔ +================== ======== ================ + +* **id** The ID of the volume type + +* **display_name** A name for the volume type + +* **extra_specs** A dictionary of various specifications + diff --git a/docs/compute-api-guide.rst b/docs/compute-api-guide.rst index 3362a8864b961ea90eb2bd4ce776d5ba5d84d1b6..3b9b5953ccb5975e097dd397c30806cea0e57326 100644 --- a/docs/compute-api-guide.rst +++ b/docs/compute-api-guide.rst @@ -33,7 +33,7 @@ Authentication -------------- All requests use the same authentication method: an ``X-Auth-Token`` header is -passed to the servive, which is used to authenticate the user and retrieve user +passed to the service, which is used to authenticate the user and retrieve user related information. No other user details are passed through HTTP. Efficient Polling with the Changes-Since Parameter @@ -51,7 +51,7 @@ Efficient Polling with the Changes-Since Parameter Limitations ----------- -* Version MIME type and vesionless requests are not currently supported. +* Version MIME type and versionless requests are not currently supported. * Cyclades only supports JSON Requests and JSON/XML Responses. XML Requests are currently not supported. @@ -96,7 +96,7 @@ Description URI `Get Meta Item <#get-server-metadata-item>`_ ``/servers/<server-id>/metadata/<key>`` GET ✔ ✔ `Update Meta Item <#update-server-metadata-item>`_ ``/servers/<server-id>/metadata/<key>`` PUT ✔ ✔ `Delete Meta Item <#delete-server-metadata>`_ ``/servers/<server-id>/metadata/<key>`` DELETE ✔ ✔ -`Actions <#server-actions>`_ ``servers/<server id>/action`` POST ✔ ✔ +`Actions <#server-actions>`_ ``/servers/<server id>/action`` POST ✔ ✔ ================================================== ========================================= ====== ======== ========== .. rubric:: Flavors @@ -507,27 +507,49 @@ Request body contents:: ... } -=========== ====================== ======== ========== -Attributes Description Cyclades OS/Compute -=========== ====================== ======== ========== -name The server name ✔ ✔ -imageRef Image id ✔ ✔ -flavorRef Resources flavor ✔ ✔ -personality Personality contents ✔ ✔ -metadata Custom metadata ✔ ✔ -networks Connection information ✔ ✔ -.. project .. Project UUID .. ✔ .. **✘** -=========== ====================== ======== ========== +=========== ==================== ======== ========== +Attributes Description Cyclades OS/Compute +=========== ==================== ======== ========== +name The server name ✔ ✔ +imageRef Image id ✔ ✔ +flavorRef Resources flavor ✔ ✔ +personality Personality contents ✔ ✔ +metadata Custom metadata ✔ ✔ +project Project assignment ✔ **✘** +=========== ==================== ======== ========== * **name** can be any string -* **imageRed** and **flavorRed** should refer to existing images and hardware +* **imageRef** and **flavorRef** should refer to existing images and hardware flavors accessible by the user * **metadata** are ``key``:``value`` pairs of custom server-specific metadata. There are no semantic limitations, although the ``OS`` and ``USERS`` values should rather be defined +* **project** (optional) is the project where the VM is to be assigned. If not + given, user's system project is assumed (identified with the same uuid as the + user). + +* **personality** (optional) is a list of personality injections. A personality + injection is a way to add a file into a virtual server while creating it. + Each change modifies/creates a file on the virtual server. The injected data + (``contents``) should not exceed 10240 *bytes* in size and must be base64 + encoded. The file mode should be a number, not a string. A personality + injection contains the following attributes: + +====================== =================== ======== ========== +Personality Attributes Description Cyclades OS/Compute +====================== =================== ======== ========== +path File path on server ✔ ✔ +contents Data to inject ✔ ✔ +group User group ✔ **✘** +mode File access mode ✔ **✘** +owner File owner ✔ **✘** +====================== =================== ======== ========== + +*Example Create Server Request: JSON* + * **personality** (optional) is a list of `personality injections <#personality-ref>`_ @@ -762,6 +784,7 @@ netTimeSeries Network load / time graph URL *Example Get Server Stats Response: JSON* .. code-block:: javascript + GET https://example.org/compute/v2.0/servers/5678/stats { "stats": { @@ -1584,6 +1607,7 @@ Operations Cyclades OS/Compute `Reboot <#reboot-server>`_ ✔ ✔ `Get Console <#get-server-console>`_ ✔ **✘** `Set Firewall <#set-server-firewall-profile>`_ ✔ **✘** +`Reassign <#reassign-server>`_ ✔ **✘** `Change Admin Password <#os-compute-specific>`_ **✘** ✔ `Rebuild <#os-compute-specific>`_ **✘** ✔ `Resize <#resize-server>`_ ✔ ✔ @@ -1767,23 +1791,27 @@ Request body contents:: .. note:: Response body should be empty -.. Server reassign -.. ............. +Reassign Server +............... -.. Each resource is assigned to a project. A Synnefo project is a set of resource -.. limits e.g., maximum number of CPU cores per user, maximum ammount of RAM, etc. +This operation assigns the VM to a different project. +Each resource is assigned to a project. A Synnefo project is a set of resource +limits e.g., maximum number of CPU cores per user, maximum ammount of RAM, etc. -.. Although its resource is assigned exactly one project, a user may be a member -.. of more, so that different resources are registered to different projects. +Although its resource is assigned exactly one project, a user may be a member +of more, so that different resources are registered to different projects. -.. Project reassignment is the process of assigning a project to a different -.. project +Request body contents:: -.. Request body contents:: -.. reassign: { project: <project ID>} + reassign: { project: <project-id>} -.. .. code-block:: javascript -.. "reassign": { "project": "s0m3-pr0j3ct-1d"} +*Example Action reassign: JSON** + +.. code-block:: javascript + + {"reassign": {"project": "9969f2fd-86d8-45d6-9106-5e251f7dd92f"}} + +.. note:: Response body should be empty OS/Compute Specific ................... @@ -2564,6 +2592,8 @@ Return Code Description 503 (Service Unavailable) The server is not currently available =========================== ===================== +.. note:: In case of a 204 code, the response body should be empty. + Index of Attributes ------------------- diff --git a/docs/conf.py b/docs/conf.py index a4a3f1e2a96c92b366f7615b27f2c1c8ea2d596c..0ec07f4a4bf7b8216bd20272825a2fdb102d0449 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -8,7 +8,7 @@ reload(synnefo.versions) from synnefo.versions.app import __version__ project = u'synnefo' -copyright = u'2012-2013, GRNET' +copyright = u'2012-2014, GRNET' version = __version__ release = __version__ html_title = 'synnefo ' + version @@ -42,30 +42,23 @@ html_theme_options = { 'codetextcolor': '#333333' } -html_static_path = ['_static'] htmlhelp_basename = 'synnefodoc' intersphinx_mapping = { - 'pithon': ('http://docs.python.org/', None), + 'python': ('http://docs.python.org/', None), 'django': ('https://docs.djangoproject.com/en/dev/', 'https://docs.djangoproject.com/en/dev/_objects/') } -SYNNEFO_DOCS_BASE_URL = 'http://www.synnefo.org/docs' -SYNNEFO_PROJECTS = { - 'synnefo': 'dev', - 'pithos': 'dev', - 'snf-webproject': 'dev', - 'snf-common': 'dev', - 'snf-image': 'dev', - 'snf-astakos-client': 'dev', - 'snf-cyclades-app': 'dev' -} +SYNNEFO_PROJECTS = ['synnefo', 'archipelago', 'kamaki', 'snf-image', + 'snf-image-creator', 'nfdhcpd', 'snf-vncauthproxy', + 'snf-network'] + +SYNNEFO_DOCS_BASEURL = 'https://www.synnefo.org/docs/%s/latest/objects.inv' -for name, ver in SYNNEFO_PROJECTS.iteritems(): - intersphinx_mapping[name.replace("-","")] = (SYNNEFO_DOCS_BASE_URL + - '%s/%s/' % (name, ver), - None) +for project in SYNNEFO_PROJECTS: + project_url = SYNNEFO_DOCS_BASEURL % project + intersphinx_mapping[project.replace('-', '')] = (os.path.dirname(project_url), project_url) extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', diff --git a/docs/cyclades.rst b/docs/cyclades.rst index d13e40df0c491028d7a6472ffad764b9c125e5c9..8bceaa755d4d0044bfa8291796e0fc8a3f9ac791 100644 --- a/docs/cyclades.rst +++ b/docs/cyclades.rst @@ -1,18 +1,18 @@ .. _cyclades: -Compute/Network/Image Service (Cyclades) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Compute/Network/Image/Volume Service (Cyclades) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Cyclades is the Synnefo component that implements the Compute, Network, Image and Volume services. It exposes the associated OpenStack REST APIs: OpenStack -Compute, Network, Glance and soon also Cinder. Cyclades is the part which -manages multiple Ganeti clusters at the backend. Cyclades issues commands to a -Ganeti cluster using Ganeti's Remote API (RAPI). The administrator can expand -the infrastructure dynamically by adding new Ganeti clusters to reach -datacenter scale. Cyclades knows nothing about low-level VM management -operations, e.g., the handling of VM creations, migrations among physical -nodes and node downtimes; the design and implementation of the -end-user API is orthogonal to VM handling at the backend. +Nova, Neutron, Glance and Cinder. Cyclades is the part which manages multiple +Ganeti clusters at the backend. Cyclades issues commands to a Ganeti cluster +using Ganeti's Remote API (RAPI). The administrator can expand the +infrastructure dynamically by adding new Ganeti clusters to reach datacenter or +multi-datacenter scale. Cyclades knows nothing about low-level VM management +operations, e.g., the handling of VM creations, migrations among physical nodes +and node downtimes; the design and implementation of the end-user API is +orthogonal to VM handling at the backend. There are two distinct, asynchronous paths in the interaction between Synnefo and Ganeti. The `effect` path is activated in response to a user request; @@ -26,8 +26,8 @@ Users have full control over their VMs: they can create new ones, start them, shutdown, reboot, and destroy them. For the configuration of their VMs they can select number of CPUs, size of RAM and system disk, and operating system from pre-defined Images including popular Linux distros (Debian, Ubuntu, CentOS, -Fedora, Gentoo, Archlinux, OpenSuse), MS-Windows Server 2008 R2 and 2012 as -well as FreeBSD. +Fedora, Gentoo, Archlinux, OpenSuse, Oracle Linux), MS-Windows Server 2008 R2, +2012 and 2012 R2, as well as BSD (FreeBSD, NetBSD, OpenBSD). The REST API for VM management, being OpenStack compatible, can interoperate with 3rd party tools and client libraries. diff --git a/docs/design/cyclades-networking.rst b/docs/design/cyclades-networking.rst index 62a55b3c4c9171d991998156bcf65da98faa8288..01bde906a52f0f64b9ea1cd5d19f85901e3f9752 100644 --- a/docs/design/cyclades-networking.rst +++ b/docs/design/cyclades-networking.rst @@ -10,7 +10,7 @@ Flavor, either an isolated Layer-2 broadcast domain or a Layer-3 network. Networks can either be private or public. Private networks are reserved for the user who created it, while public networks are created by the administrator and are visible to all users. Also, networks can be marked with the -`--router:external` attribute to indicate that are external networks (public +`--router:external` attribute to indicate they are external networks (public internet) Currently there are four available Networks Flavors: @@ -23,7 +23,7 @@ Currently there are four available Networks Flavors: unique MAC prefix that is assigned to each network. The administrator can limit which networks can be created via API with the -`API_ENABLED_NETOWRK_FLAVORS` setting. +`API_ENABLED_NETWORK_FLAVORS` setting. The attributes for network objects are the following: @@ -299,7 +299,7 @@ Finally, the request can contain the following optional attributes: Example request: -.. code:: json +.. code-block:: json {"port": { "name": "port1", diff --git a/docs/design/logging-management-commands.rst b/docs/design/logging-management-commands.rst new file mode 100644 index 0000000000000000000000000000000000000000..13900693cbf3b183f4631e0f2792cde721138ada --- /dev/null +++ b/docs/design/logging-management-commands.rst @@ -0,0 +1,113 @@ +=================================================== +Logging mechanism for Synnefo's management commands +=================================================== + + +Abstract +======== + +Log all stdout and stderr output of every invocation of snf-manage, on unique +filenames under a given directory. + + +Current state and shortcomings +============================== + +All Synnefo's management commands are written as custom django-admin commands. +This means that every management command is in fact a class that extends +Django's BaseCommand class. + +Django's *BaseCommand* provides the attributes ``self.stdout`` and +``self.stderr`` and Django's documentation encourages the users to use these +attributes if they wish to write to the console. Django doesn't provide an +option to write the output to files and the user has to implement this +explicitly when implementing the ``handle`` method. + +We would like to extend the above mechanism to allow every ``snf-manage`` +command to log all stdout and stderr output on a unique filename under a given +directory. The implementation should change nothing in the way that users write +management commands (only acceptable change is that the new commands may have +to inherit a new class and not the *BaseCommand* one). This means that +existing management commands should play out of the box and also that the +logging mechanism will globally apply to all of them. + +A new Synnefo setting named **LOGGER_EXCLUDE_COMMANDS** has been added that +specifies which commands will not be logged. By default, commands that do not +alter the state of the server (i.e. \*-list and \*-show commands) will be +excluded from the logging mechanism. One can disable this logging mechanism all +together by setting the above variable to ".\*". + + +Proposed changes +================ + +In this section we will try to explain the way that the new logging mechanism +will be implemented as well as the reasons behind these decisions. + +As we previously saw, we want the logging mechanism to be global and to work +for all the ``snf-manage`` commands without extra effort. This means that the +management commands will continue to use the ``self.stdout`` and +``self.stderr`` attributes from *BaseCommand* class to provide console +output. Therefor we have to provide our own ``self.stdout`` and ``self.stderr`` +objects that will preserve the previous functionality and log to files at the +same time. There are two ways to achieve that: + +Patch the Django's *BaseCommand* class and replace ``self.stdout`` and +``self.stderr`` attributes. + + This solution requires the minimum amount of changes to the management + commands' code as they will use our patched version of *BaseCommand*. + The downside is that we have to patch a library provided class. We are not + encouraging these type of patches because it obfuscates the code (the + programmer is expecting to use Django's *BaseCommand* class, not ours) + and does not preserve compatibility with other Django versions (if the + implementation of Django's *BaseCommand* changes our patch will not + work). + +Create a new class that extends Django's *BaseCommand*. + + The downside of this solution is that we have to change the existing code + so all management commands will inherit our new class and not Django's + *BaseCommand*. But we find this solution to be cleaner. + +For the above reasons we decided to go with the second option. + +Django's ``self.stdout`` and ``self.stderr`` are implemented as *OutputWrapper* +objects. We will create our own class (**SynnefoOutputWrapper**) which will use +python's logging library to handle the file part of the logging and the +original *OutputWrapper* object to handle the console part (since we want to +preserve the functionality of Django's *OutputWrapper* and the style functions +it uses to pretty print the messages). + +Our new class has to be a **descriptor**. This is because *BaseCommand* doesn't +initialize the ``stdout`` and ``stderr`` attributes at ``__init__`` but sets +them only when it needs to (meaning inside the *execute* method). + +The above classes will be written in snf-django-lib package meaning that all +the other packages will have a dependency in snf-django-lib. + +We will combine timestamp, command name and PID to form unique names, e.g.: +20140120113432-server-modify-4564, where "4564" was the PID. The timestamp will +be first so that files will be chronologically sorted. + + +Implementation details +====================== + +The implementation will follow the following steps: + +- Change current management commands to use ``self.stdout`` and ``self.stderr`` + to provide console output instead of ``sys.stdout``, ``print`` or anything + else. This change complies with Django's documentation. + +- Write a new class that will replace Django's *OutputWrapper*. + +- Change the **SynnefoCommand** class so that it will extend Django's + *BaseCommand* and will replace ``stdout`` and ``stderr`` attributes. + +- Change all management commands to inherit the **SynnefoBaseCommand** class. + +- Update package dependencies. + +- Add a new Synnefo setting to allow the user to change the directory where + the output will be saved. diff --git a/docs/design/multi-db-transactions.rst b/docs/design/multi-db-transactions.rst new file mode 100644 index 0000000000000000000000000000000000000000..23c82e1f247706a1414aaae75119ae4b9d311222 --- /dev/null +++ b/docs/design/multi-db-transactions.rst @@ -0,0 +1,67 @@ +.. _multi-db-transactions: + +=============================== +Multi-DB transactions in Django +=============================== + +In this document, we will talk about Django transactions. We will explain why +transactions are a problem in a multi-db scenario and propose a way to solve +this problem. + +Django transactions +=================== + +Django has a useful library to handle database transactions called +`transcation`. From this library, Synnefo uses two decorators: +`commit_on_success` and `commit_manually`. + +* `commit_on_success` opens a transaction with the database on enter and + commits any queued changes on successful exit. Should an exception occur + midway, we can rest assured that there is nothing half-committed. +* `commit_manually` opens a transaction with the database on enter but does not + commit/rollback anything, unless explicitly told so using the + `commit()`/`rollback()` functions. + +Using these decorators as-is is fine when the Cyclades/Astakos nodes have to +deal with one database. In the case of two or more databases however, the user +needs to add the `using` argument in these decorators to define the database +that will be used to open a transaction. Else, they use the 'default' database. + +Solution +======== + +The most straight-forward solution is to add in all decorators a `using` +argument with **cyclades** or **astakos** as database names, and enforce that +all settings from now on will have these two entries. + +The only issue with this approach is that the core problem, that is, how to +chose between multiple databases in the application level, will be solved with +two different ways. The one way is with the `using` argument in transactions, +while the other one is with *database routers*. + +Therefore, we propose a solution for the transaction problem that converges +with database routers. + +The solution is the following: + +* We will write wrappers for the `commit_on_success` and `commit_manuall` + decorators. There will be one wrapper for Astakos and one for Cycades. * + When called, these wrappers will decide which is the appropriate database to + start a transaction. The decision will use a function that is also used by + database routers (`select_db`), and determines which is the correct db for a + model. This is the converging point between transactions and database + routers. +* Once the decision is made, the wrappers will return the original + `commit_on_success` / `commit_manually` decorators, but with the chosen db + as value of the `using` argument. + +To sum up, the only changes we need to do is to add a `transaction.py` in +Astakos and Cyclades, write the wrappers and replace this import: + +.. code-block:: python + + from django.db import transaction + +with the Astakos/Cyclades `transaction.py` file, in any code that uses the +`commit_on_success`/`commit_manually` decorators. + diff --git a/docs/design/resource-pool-projects.rst b/docs/design/resource-pool-projects.rst index 5b04bc682fc47062a8530d0e932074e9cfa12c0b..6a050edc1f3a34c23ed4c0629442beca693d2188 100644 --- a/docs/design/resource-pool-projects.rst +++ b/docs/design/resource-pool-projects.rst @@ -40,8 +40,10 @@ cannot exceed the former. A limit on the number of members allowed is still enforced. Projects will be the sole source of resources. Current base quota offered to -users by the system will be expressed in terms of special-purpose *base* -projects. +users by the system will be expressed in terms of special-purpose *system* +projects. Due to the central role that projects now acquire, we will alter +the project schema to facilitate project creation and modification without +the extra overhead of submitting and approving applications. Implementation details ====================== @@ -88,29 +90,31 @@ Deallocation is always allowed as long as usage does not fall below zero. Counters with zero usage and limit could by garbage collected by Astakos, if needed. -Base projects -------------- +System projects +--------------- For reasons of uniformity, we replace the base quota mechanism with projects. -In a similar vein to OpenStack tenants, we define new user-specific *base* +In a similar vein to OpenStack tenants, we define new user-specific *system* projects to account for the base quota for each user. These projects should be clearly associated with a single user, restrict join/leave actions and -specify the quota granted by the system. When a new user is created, -their base project will be automatically created and linked back to the user. -User activation will trigger project activation, granting the default resource -quota. Base projects will have no owner, marked thusly as `system' projects. -The administrator can, following the usual project logic, alter quota by -modifying the project. Users cannot apply for modification of their base -projects. - -Projects will, from now on, be identified by a UUID. Base projects will +specify the quota granted by the system. When a user is accepted, their system +project will be automatically created, activated, and linked back to the user, +granting the default resource quota. These projects will have no owner, marked +thusly as `system' projects. The administrator can, following the usual +project logic, alter quota by modifying the project. Users cannot apply for +modification of their system projects. + +Projects will, from now on, be identified by a UUID. System projects will receive the same UUID as the user itself. ProjectID, which appears above in -the Quotaholder entries, refers to the project UUID. +the Quotaholder entries, refers to the project UUID. When a system project is +created for a user, there is a slight probability the user's UUID is already +in use by another project; in this case one can only delete the user and +create a new one in its place. Base quota will be expressed both in terms of a project-level and a member-level limit. This will result in two operationally equivalent Quotaholder counters, as in the following example. In the future, we could -admit third-party users to a user's base project; in that case, those +admit third-party users to a user's system project; in that case, those counters would differ. :: @@ -120,13 +124,54 @@ counters would differ. cyclades.vm project:uuid None 5 1 cyclades.vm user:uuid project:uuid 5 1 -System default quota --------------------- +Private projects +---------------- + +Since the introduction of system projects will explode the number of total +projects, we will need to control their visibility. We add a new flag +*private* in project definitions. A private project can only be accessed by +its owner and members and not be advertized in the UI. System projects are +marked as private. + +Decouple projects from applications +----------------------------------- + +System projects do not fit well in the current project/application scheme, +because no user has applied for them. Moveover, we would like to easily +modify project properties, particularly quota limits, without the need to +apply for an application for each project and then approve it. + +We will decouple projects from applications by incorporating the project +definition into the project object rather than relying on an application. +The system will directly make a new (system) project upon user creation and a +privileged user will be able to modify an existing project by directly +modifying it. An unprivileged user will still need to make an application. + +The project model is adapted to reference the *last* application that is +related to the project, if any---projects automatically created by the +system reference no application. For an uninitialized project, this +denotes the original application through which the project was made. If +the application is denied or cancelled, the whole project is considered +deleted. + +Applications as modifications +````````````````````````````` + +Application for a new project is created in state ``pending`` and its +properties are copied into a new project object, which is in state +``uninitialized``. To preserve this equality, we disallow modifications of +uninitialized projects, either in-place or through an application. An +already activated project can be modified by submitting an application +containing just the desired changes. An application object stores the +specified changes and should remain read-only. + +System default quota and resource registration +---------------------------------------------- Each resource registered in the system is assigned a default quota limit. A newly-activated user is given these limits as their base quota. This is till now done by copying the default limits as user's entries in -AstakosUserQuota. Default limits will from now on be copied into the base +AstakosUserQuota. Default limits will from now on be copied into the system project's resource definitions. Conventional projects are created through a project application, which @@ -139,14 +184,10 @@ one specifying the default base quota, in order to fill in missing limits for conventional projects. It will be controled by a new option ``--project-default`` of command ``resource-modify``. -Private projects ----------------- - -Since the introduction of base projects will explode the number of total -projects, we will need to control their visibility. We add a new flag -*private* in project definitions. A private project can only be accessed by -its owner and members and not be advertised in the UI. Base projects are -marked as private. +When a project is activated, either directly in the case of system projects +or through the approval of a project application, limits for resources not +specified are automatically completed by consulting the appropriate +skeleton. Allocation of a new resource ---------------------------- @@ -198,10 +239,7 @@ In Cyclades, each VM, floating IP, or other distinct resource should be linked to a project. Pithos should link containers to projects. Astakos will handle its own resource ``astakos.pending_app`` in a special -way: it will always be charged at the user's base project. This resource -is marked with ``allow_in_projects = False`` in its definition. Since quota -is now project-based, this flag will now be interpreted as forbidding usage -in non-base projects. +way: it will always be charged at the user's system project. Resource reassignment --------------------- @@ -268,8 +306,8 @@ This will issue the following provisions to the Quotaholder:: } ] -API extensions --------------- +API changes +----------- API call ``GET /quotas`` is extended to incorporate project-level quota. The response contains entries for all projects for which a user/project pair @@ -312,11 +350,15 @@ regardless of user:: } } +``GET /service_project_quotas`` will be used in a similar way as ``GET +/service_quotas`` to get the project-level quotas for resources associated +with the Synnefo component that makes the request. + All service API calls that create resources can specify the project where they will be attributed. In cyclades, ``POST /servers`` (likewise for networks and floating IPs) will -receive an extra argument ``project``. If it is missing, the user's base +receive an extra argument ``project``. If it is missing, the user's system project will be assumed. In calls detailing a resource (e.g., ``GET /servers/<server_id>``), the field ``tenant_id`` will contain the project id. @@ -336,12 +378,30 @@ container to a given project, the latter reassigns an existing container. Field ``x-container-policy-project`` will be retrieved by a ``HEAD`` call at the container level. +Changes in the projects API +``````````````````````````` + +``PUT /projects/<proj_id>`` will be used to mod a new project replacing +``POST``. It now expects a dictionary with just the desired +changes, not a complete project definition. It is only allowed if the +project is already activated. + +``GET /projects/<proj_id>`` changes to include a ``last_application`` field, +if applicable. + +Application actions (approve, deny, dismiss, cancel) are integrated into +project actions and expect an extra ``app_id`` argument to specify the +application. Actions are allowed only on a project's last application; +the application id is required in order to avoid races. + +The applications API is removed, incorporated into the projects API. + User interface -------------- User quota will be presented per project, including the aggregate activity of other project members: the Resource Usage page will include a drop-down -menu with all relevant projects. By default, user's base project will +menu with all relevant projects. By default, user's system project will be assumed. When choosing a project, usage for all resources will be presented for the given project in the following style:: @@ -397,16 +457,15 @@ Quota can be queried per user or project:: ------------------------ cyclades.vm 100 50 -A new command ``snf-manage project-modify`` will automate the process of -applying/approving applications in order to modify some project settings, -such as the quota limits. +A new command ``snf-manage project-modify`` will enable in-place +modification of project properties, such as their quota limits. Currently, the administrator can change the user base quota with: ``snf-manage user-modify <id> --base-quota <resource> <capacity>``. This will be removed in favor of the ``project-modify`` command, so that all quota are handled in a uniform way. Similar to ``user-modify --all``, -``project-modify`` will get options ``--all-base`` and ``--all-non-base`` to -allow updating quota in bulk. +``project-modify`` will get options ``--all-system-projects`` to +allow updating base quota in bulk. Migration steps =============== @@ -418,24 +477,24 @@ Existing projects need to be converted to resource-pool ones. The following steps must be taken in Astakos: * compute project-level limits for each resource as max_members * member-level limit - * create base projects based on base quota for each user + * create system projects based on base quota for each user * make Quotaholder entries for projects and user/project pairs - * assign all current usage to the base projects (both project + * assign all current usage to the system projects (both project and user/project entries) * set usage for all other entries to zero Cyclades and Pithos should initialize their project attribute on each resource -with the user's base project, that is, the same UUID as the resource owner. +with the user's system project, that is, the same UUID as the resource owner. Initial resource reassignment ----------------------------- -Once migration has finished, users will be off-quota on their base project, +Once migration has finished, users will be off-quota on their system project, if they had used additional quota from projects. To alleviate this situation, each service can attempt to reassign resources to other projects, following this strategy: * consult Astakos for projects and quota for a given user * select resources that can fit in another project - * issue a commission to decrease usage of the base project and likewise + * issue a commission to decrease usage of the system project and likewise increase usage of the available project * record the new ProjectUUID for the reassigned resources diff --git a/docs/design/volume-snapshots.rst b/docs/design/volume-snapshots.rst new file mode 100644 index 0000000000000000000000000000000000000000..344b4bd364809aef29ee950e2726e7e0a8811d98 --- /dev/null +++ b/docs/design/volume-snapshots.rst @@ -0,0 +1,209 @@ +==================== +Volume snapshots +==================== + +The goal of this design document is to record and present the proposed method +for Volume snapshotting in Synnefo. We will describe the whole work-flow from +the user's API down to Archipelago and up again. + +Snapshot functionality +======================= + +The snapshot functionality aims to provide the user with the following: + +a. A thin snapshot of an Archipelago volume: |br| + The snapshot should capture the volume's data state at the time the + snapshot was requested. |br| + **Note:** The VM user is accountable for the consistency of its volume. + Journaled filesystems or prior shutdown of the VM is advised. +#. Presenting the snapshot instantly as a regular file + on Pithos: |br| + Users can view their snapshots in Pithos as any other file that they have + uploaded and can subsequently download them. +#. A registered Synnefo snapshot, ready to be deployed: |br| + Essentially, if a snapshotted volume includes an OS installation, it can be + used as any other Synnefo OS image. This allows users to create + *restoration points* for their VMs. + +Side goals +^^^^^^^^^^ + +In order to make the snapshot process as slim as possible, the following goals +must also be met: + +a. Low computational/storage overhead: |br| + If many users send snapshot requests, the service should be able to respond + quick. Also, a snapshotted volume should not incur any significant storage + overhead. +#. Solid reconciliation process: |br| + If a snapshot request fails, the system should do the necessary cleanups or + at least make it easy to reconcile the affected databases. + +Snapshot creation +======================== + +An illustration of the proposed method follows below: + +|create_snapshot| + +Each step of the procedure is explained below: + +#. The Cyclades App receives a snapshot request. The request is expected to + originate from the user and be sent via an API client or the Cyclades UI. A + valid snapshot request should target an existing volume ID. +#. The Cyclades App uses its Pithos Backend to create a `snapshot record`_ in + the Pithos database. The snapshot record is explained in the following + section. It is initially set with the values that are seen in the diagram. +#. The Cyclades App creates a snapshot job and sends it to the Ganeti + Master. +#. The Ganeti Master in turn, designates the snapshot job to the appropriate + Ganeti Noded. +#. The Ganeti Noded runs the corresponding Archipelago ``ExtStorage`` script + and invokes the Vlmc tool to create the snapshot. +#. The Vlmc tool instructs Archipelago to create a snapshot by sending a + snapshot request. + +Once Archipelago has (un)successfully created the snapshot, the response is +propagated to the Ganeti Master which in turn creates an event about this +snapshot job and its execution result. + +7. snf-ganeti-eventd is informed about this event, using an ``inotify()`` + mechanism, and forwards it to the snf-dispatcher. +#. The snf-dispatcher uses its Pithos Backend to update the ``snapshot status`` + property of the snapshot record that was created in Step 2. According to the + result of the snapshot request, the snapshot status is set as ``Ready`` or + ``Error``. This means that, as far as Cinder is concerned, the snapshot is + ready. +#. The ``Available`` attribute however is still ``0``. Swift (Pithos) will make + it available (``1``) and thus usable, the first time it will try to ping + back (a.k.a. the first time someone tries to access it). + +Snapshot record +^^^^^^^^^^^^^^^^^ + +The snapshot record has the following attributes: + ++-------------------+--------------------------------------+---------------+ +| Key | Value | Service | ++===================+======================================+===============+ +| file_name | Generated (string - see below) | Swift | ++-------------------+--------------------------------------+---------------+ +| available | Generated (boolean) | Swift | ++-------------------+--------------------------------------+---------------+ + +and the following properties: + ++-------------------+--------------------------------------+---------------+ +| Key | Value | Service | ++===================+======================================+===============+ +| snapshot_name | User-provided (string) | Cinder | ++-------------------+--------------------------------------+---------------+ +| snapshot_status | Generated (see Cinder API) | Cinder | ++-------------------+--------------------------------------+---------------+ +| EXCLUDE_ALL_TASKS | Generated (string) | Glance | ++-------------------+--------------------------------------+---------------+ + +The file_name is a string that has the following form:: + + snf-snap-<vol_id>-<counter> + +where: + +* ``<vol_id>`` is the Volume ID and +* ``<counter>`` is the number of times that the volume has been snapshotted and + increases monotonically. + +The snapshot name should thus be unique. + +"Available" attribute +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``available`` attribute is a Swift attribute that is introduced with +snapshots and applies to all Pithos files. When a Pithos file is "available", +it means that its map file has been created and points to the correct data. +Normally, all Pithos files have their map file properly created before adding a +record in the Pithos database. Snapshots however are an exception to this rule, +since their map file is created asynchronously. + +Therefore, the creation of a Pithos file has the following rules: + +* If the file is a snapshot, the ``available`` attribute is set to "0". +* For all other files, the ``available`` attribute is set to "1". + +**Note:** ``available`` can change from "0" to "1", but never the opposite. + +The update of the ``available`` attribute happens implicitly after the creation +of the snapshot, when a request reads the file record from the +Pithos database. The following diagram shows how can a request (download +request for example) update the ``available`` attribute. + +|available_attribute| + +In short, what happens is: + +#. The user asks to download the file from the Pithos App. +#. The Pithos App checks the file record and finds that the ``available`` + attribute is "0". +#. It then pings Archipelago to check the status of the map. +#. If the map exists, it sets ``available`` to "1" and can use the map file to + serve the data. + +VM creation from snapshot +=============================== + +The following diagram illustrates the VM creation process from a snapshot +image. + +|spawn_from_snapshot| + +The steps are explained in detail below: + +#. A user who has the registered Images list (which includes all Snapshots + too), requests to create a VM from the Cyclades App, using one of the + registered snapshots of the list. +#. The Cyclades App sends a VM creation job to the Ganeti Master with the + appropriate data. The data differ according to the disk template: + + * If the template is ext, then the "origin" field has the archipelago map + name. + * For any other template, the archipelago map name is passed in the "img_id" + property of OS parameters. + +#. The Ganeti Master designates the job to the appropriate Ganeti Noded. +#. The Ganeti Noded will create the disks, according to the requested disk + template: + + a. If the disk template is "ext", the following execution path is taken: + + 1. The Ganeti Noded runs the corresponding Archipelago ``ExtStorage`` + script and invokes the Vlmc tool. + #. The Vlmc tool instructs Archipelago to create a volume from a + snapshot. + + b. If the disk template is other than "ext", e.g. "drbd", Ganeti Noded + creates a new, empty disk. + +#. After the volume has been created, Ganeti Noded instructs snf-image to + prepare it for deployment. The parameters that are passed to snf-image are + the OS parameters of Step 2, and the main ones for each disk template are + shown in the diagram, next to the "Ext?" decision box. According to the disk + template, snf-image has two possible execution paths: + + a. If the disk template is "ext", there is no need to copy any data. + Also, the image has the "EXCLUDE_ALL_TASKS" property set to "yes", so + snf-image will run no customization scripts and will simply return. + b. If the disk template is other than "ext", e.g. "drbd", snf-image will + copy the snapshot's data from Pithos to the created DRBD disk. As above, + since the image has the "EXCLUDE_ALL_TASKS" property set to "yes", + snf-image will run no customization scripts. + +.. |br| raw:: html + + <br /> + +.. |create_snapshot| image:: /images/create-snapshot.png + +.. |available_attribute| image:: /images/available-attribute.png + +.. |spawn_from_snapshot| image:: /images/spawn-from-snapshot.png + diff --git a/docs/design/volumes.rst b/docs/design/volumes.rst new file mode 100644 index 0000000000000000000000000000000000000000..e5e817843e86eefaa1ab3c8ef1446b53ae046a9d --- /dev/null +++ b/docs/design/volumes.rst @@ -0,0 +1,146 @@ +Cyclades Volumes +^^^^^^^^^^^^^^^^ + +This document describes the extension of Cyclades to handle block storage +devices, referred to as Volumes. + + +Current state and shortcomings +============================== + +Currently one block storage device is created and destroyed together with the +virtual servers. One disk is created per server with the size and the disk +template that are described by the server's flavor. The disk is destroyed when +the server is destroyed. Also, There is no way to attach/detach disks to +existing servers and there is no API to expose information about the server's +disks. + + +Proposed changes +================ + +Cyclades will be able to manage volumes so that users can create volumes and +dynamically attach them to servers. Volumes can be created either empty or +filled with data from a specified source (image, volume or snapshot). Also, +users can dynamically remove volumes. Finally, a server can be created with +multiple volumes at once. + + +Implementation details +====================== + +Known Limitations +----------------- + +While addition and removal of disks is supported by Ganeti, it is not supported +to handle disks that are not attached to an instance. There is no way to create +a disk without attaching it to an instance, neither a way to detach a disk from +an instance without deleting it. Future versions of Ganeti will make disks +separate entities, thus overcoming the above mentioned issues. Until then, +this issues will also force a limitation to the way Cyclades will be handling +Volumes. Specifically, Cyclades volumes will not be attached/detached from +servers; they will be only be added and removed from them. + +Apart from Ganeti's inability to manage disks as separate entities, attaching +and detaching a disk is not meaningful for storage solutions that are not +shared between Ganeti nodes and clusters, because this would require copying +the data of the disks between nodes. Thus, the ability to attach/detach a disk +will only be available for disks of externally mirrored template (EXT_MIRRORED +disk templates). + +Also, apart from the root volume of an instance, initializing a volume with +data is currently only available for Archipelago volumes, since there is no +mechanism in Ganeti for filling data in other type of volumes (e.g. file, lvm, +drbd). Until then, creating a volume from a source, other than the root volume +of the instance, will only be supported for Archipelago. + +Finally, an instance can not have disks of different disk template. + +Cyclades internal representation +-------------------------------- + +Each volume will be represented by the new `Volume` database model, containing +information about the Volume, like the following: + +* name: User defined name +* description: User defined description +* userid: The UUID of the owner +* size: Volume size in GB +* disk_template: The disk template of the volume +* machine: The server the volume is currently attached, if any +* source: The UUID of the source of the volume, prefixed by it's type, e.g. "snapshot:41234-43214-432" +* status: The status of the volume + + +Each Cyclades Volume corresponds to the disk of a Ganeti instance, uniquely +named `snf-vol-$VOLUME_ID`. + +API extensions +-------------- + +The Cyclades API will be extended with all the needed operations to support +Volume management. The API will be compatible with OpenStack Cinder API. + +Volume API +`````````` + +The new `volume` service will be implemented and will provide the basic +operations for volume management. This service provides the `/volumes` and +`/types` endpoints, almost as described in the OS Cinder API, with the only +difference that we will extend `POST /volumes/volumes` with a required +field to represent the server that the volume will be attached, and which is +named `server_id`. + + +Compute API extensions +`````````````````````` + +The API that is exposed by the `volume` service is enough for the addition and +removal of volumes to existing servers. However, it does not cover creating a +server from a volume or creating a server with more than one volumes. For this +reason, the `POST /servers` API call will be extended with the +`block_device_mapping_v2` attribute, as defined in the OS Cinder API, which is +a list of dictionaries with the following keys: + +* source_type: The source of the volume (`volume` | `snapshot` | `image` | `blank`) +* uuid: The uuid of the source (if not blank) +* size: The size of the volume +* delete_on_termination: Whether to preserve the volume on instance deletion. + +If the type is `volume`, then the volume that is specified by the `uuid` field +will be used. Otherwise, a new volume will be created which will contain the +data of the specified source (`image` or `snapshot`). If the source type +is `blank` then the new volume will be empty. + +Also, we will implement `os_volume-attachments` extension, containing the +following operations: + +* List server attachments(volumes) +* Show server attachment information +* Attach volume to server (notImplemented until Ganeti supports detachable volumes) +* Detach volume from server (notImplemented until Ganeti supports detachable volumes) + +Quotas +------ + +The total size of volumes will be quotable, and will be directly mapped to +existing `cyclades.disk` resource. In the future, we may implement having +different quotas for each volume type. + +Command-line interface +---------------------- + +The following management commands will be implemented: + +* `volume-create`: Create a new volume +* `volume-remove`: Remove a volume +* `volume-list`: List volumes +* `volume-info`: Show volume details +* `volume-inspect`: Inspect volume in DB and Ganeti + + +The following commands will be extended: + +* `server-create`: extended to specify server's volumes +* `server-inspect`: extended to display server's volumes +* `reconcile-servers`: extended to reconcile server's volumes diff --git a/docs/dev-guide.rst b/docs/dev-guide.rst index b8e22efadb5315cecf3c4de76b3d320fc90bbce9..7bd2dda883b77ad1aefdd96f5ede2ec42d9b907e 100644 --- a/docs/dev-guide.rst +++ b/docs/dev-guide.rst @@ -3,99 +3,259 @@ Synnefo Developer's Guide ^^^^^^^^^^^^^^^^^^^^^^^^^ -This is the complete Synnefo Developer's Guide. +The suggested method of setting up an environment for development purposes is +first to install Synnefo on a Debian Wheezy system and then to use Python's +`development mode +<http://www.ewencp.org/blog/a-brief-introduction-to-packaging-python/>`_ to run +Synnefo from a cloned repo and see your changes instantly. -Environment set up -================== +Synnefo installation +~~~~~~~~~~~~~~~~~~~~ -First of all you have to set up a developing environment for Synnefo. +There are two main ways of installing Synnefo, each of which will be described +below: -**1. Create a new VM** +* `Existing Debian Wheezy system (snf-deploy)`_: This method requires that you + have setup a Debian Wheezy system. On this setup, you can install Synnefo + using ``snf-deploy``. +* `Synception (snf-ci)`_: This method builds Synnefo within Synnefo (you read + right), which means that you need to have an account in an existing Synnefo + installation. Then, using ``snf-ci`` in conjunction with ``kamaki``, you can + create a new Debian Wheezy VM in that installation. The rest is handled + automatically by ``snf-ci``, which uses ``snf-deploy`` to install Synnefo in + that VM. -It has been tested on Debian Wheezy. It is expected to work with other -releases (e.g., Squeeze) too, as long as they are supported by -``snf-deploy``. +.. note:: -**2. Build your own Synnefo installation** + The first method will build Synnefo from our stable branch. If your + development takes place in ``develop`` or in a ``feature`` branch, then you + may need to update your installation manually, e.g. run database migrations + or install new packages. -Follow the instructions `here <http://www.synnefo.org/docs/synnefo/latest/quick-install-guide.html>`_ -to build Synnefo on a single node using ``snf-deploy``. -**3. Install GitPython** +Existing Debian Wheezy system (snf-deploy) +------------------------------------------ -.. code-block:: console +**1. Install Synnefo** - # pip install gitpython +In order to create a one-node Synnefo installation, you can follow the +instructions `here +<http://www.synnefo.org/docs/synnefo/latest/quick-install-guide.html>`_. -**4. Install devflow** +**2. Install Devflow** -Devflow is a tool to manage versions, helps implement the git flow development process, -and builds Python and Debian packages. You will need it to create your code's version. +Once you're done, you will need to install ``devflow``. Devflow is a tool that +helps you to manage package versions, implement the git flow development +process and build Python and Debian packages. You will need it to create your +code's version. .. code-block:: console # pip install devflow -**5. Get Synnefo code** -First you need to install git +**3. Clone Synnefo repo** + +First, you need to install ``git``: .. code-block:: console # apt-get install git -And now get the Synnefo code from the official Synnefo repository +and clone the Synnefo repo, preferably as non-privileged user: .. code-block:: console - # su some_regular_user - $ git clone https://code.grnet.gr/git/synnefo + $ git clone https://github.com/grnet/synnefo.git -Make sure you clone the repository as a regular user. Otherwise you will -have problems with file permissions when deploying. -**6. Code and deploy** +**4. Configure the code's version** -1. Configure the version +Enter the directory where you have cloned the Synnefo repository and issue the +following command: .. code-block:: console $ devflow-update-version -2. Code -3. In every component you change, run as root + +Synception (snf-ci) +------------------- + +**1. Install Kamaki** + +Kamaki is a cli tool for managing clouds. It implements the Openstack APIs and +can be used to create VMs, networks and generally allow the user to do what +he/she would normally do from the cloud's UI. + +If you haven't installed ``kamaki`` yet, you can follow `these +<http://www.synnefo.org/docs/kamaki/latest/installation.html>`_ instructions. + +Once you have ``kamaki`` installed, you need to connect it to your account on +an existing Synnefo installation (called *cloud* in kamaki lingo). To do so, +you can consult the `Setup +<http://www.synnefo.org/docs/kamaki/latest/setup.html#quick-setup>`_ section. +Alternative, you can visit the **API access** page of your account in that +cloud and download your personalized ``.kamakirc`` file. + +**2. Clone Synnefo repo** + +First you need to install ``git`` and ``fabric`` (the latter is used internally +by the ``snf-ci`` script): .. code-block:: console - # python setup.py develop -N + # apt-get install git fabric + +and clone the Synnefo repo, preferably as non-privileged user: + +.. code-block:: console -This does not automatically install dependencies, in order to avoid -confusion with Synnefo packages installed by ``snf-deploy``. External -dependencies have already been installed by ``snf-deploy``; if you introduce -a new dependency, you will have to explicitly install it. + $ git clone https://github.com/grnet/synnefo.git -4. You will need to restart the server with + +**3. Install Synnefo remotely on a VM** + +Enter the directory where you have cloned the Synnefo repository and then enter +the ``ci`` folder. In this folder, you will find the ``snf-ci`` script. A +common usage of ``snf-ci`` is the following: .. code-block:: console - # service gunicorn restart + $ ./ci/snf-ci create,build,deploy --cloud <cloud> + +The above command will use your ``kamaki`` *cloud* that you have setup in +**Step 1**. In this cloud, ``snf-ci`` will create a Debian Wheezy VM, checkout +the **develop** branch from the official Synnefo repo, build the Synnefo +packages from source and install them using ``snf-deploy``. Of course, all the +previous actions can be tweaked with command-line arguments or configuration +files. To see a list of possible command-line arguments, you can use ``snf-ci +-h``. Also, you can edit the ``ci_wheezy.conf`` configuration file for more +permanent changes. -5. If your changes affected ``snf-dispatcher`` (from package - ``snf-cyclades-app``) or ``snf-ganeti-eventd`` (from - ``snf-cyclades-gtools``) you will need to restart these daemons, too. - Since step 3 installed the former under ``/usr/local/``, you need to - make sure that the correct version is evoked. You can override the - version installed by ``snf-deploy`` with +.. tip:: + + You can use the **--local-repo** argument to instruct snf-ci to use the + current branch. This means that you can install Synnefo from any branch, + even your own. + +.. tip:: + + You can view details for the created VM, such as IP, username, password + etc., by doing ``cat /tmp/ci_temp_conf``. + + +Development mode +~~~~~~~~~~~~~~~~ + +At this point you should have a working Synnefo installation. The rest of the +instructions will take place in that Synnefo installation. + +**1. Use Python's development mode** + +From the top directory of your Synnefo repo, you can use the following script: .. code-block:: console - # ln -sf /usr/local/bin/snf-dispatcher /usr/bin/snf-dispatcher + $ ./ci/install.sh -and then restart the daemons +This means that every installed ``snf-*`` package will be overridden (``python +setup.py develop -N``) with the code that exists in the currently checked-out +branch of the cloned Synnefo repo. If you wish to leave the development mode, +you can use another script: .. code-block:: console - # service snf-dispatcher restart - # service snf-ganeti-eventd restart + $ ./ci/uninstall.sh + + +**2. Change Gunicorn permissions** + +If you have cloned the repository as root, then Gunicorn will not be able to +read your source files, since by default its user/group permissions are +``www-data``. However, you can change the Gunicorn permissions by editing the +``/etc/gunicorn.d/synnefo`` configuration file and replacing every ``www-data`` +instance with ``root``. + +.. warning:: + + Gunicorn's should never run as ``root`` in production environments. + +Accessing the Synnefo UI +~~~~~~~~~~~~~~~~~~~~~~~~ + +If you want to access the Synnefo UI through your browser, you can take a look +at the :ref:`access-synnefo` section. + +Caveats regarding code changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +At this point, you would expect that every change you've made in your branch +will be instantly visible to Synnefo. Most of the times this will be the case, +but in certain circumstances you may need to restart a service or move files +around: + +* If you have changed a Synnefo setting or Django view/url, you may need to + restart Gunicorn with: + + .. code-block:: console + + # service gunicorn restart + +* If your changes affected ``snf-dispatcher`` (from package + ``snf-cyclades-app``) or ``snf-ganeti-eventd`` (from ``snf-cyclades-gtools``) + you will need to restart these daemons, too. + + .. code-block:: console + + # service snf-dispatcher restart + # service snf-ganeti-eventd restart + +* If you have edited a static file of a Synnefo component, you will need to + copy it to the respective folder under ``/usr/share/synnefo/static/``. + + +Logs +~~~~ + +You will find useful information for debugging in the following files: + +* ``/var/log/gunicorn/synnefo.log``, for most Synnefo components. +* ``/var/log/apache2/other_vhosts_access.log``, for issues with site + configuration. +* ``/var/log/postgresql/postgresql-9.1-main.log``, for database issues. + + +Testing +~~~~~~~ + +Synnefo has two main testing endpoints: + +* The ``snf-ci`` script can create a VM from a custom branch and run all + available tests on it. +* The ``./ci/tests.sh`` script can be used in an existing Synnefo installation + to run all or specific tests. You can use it as following: + + .. code-block:: console + + $ ./ci/tests.sh [--dry-run] component1[.test] component2[.test] ... + + +Developer Guidelines +~~~~~~~~~~~~~~~~~~~~ + +* If you want to use transactions in your code, you **must NOT** use the + default Django transactions (see the :ref:`Synnefo transactions design doc + <multi-db-transactions>`). Depending on the models you want to edit, you must + import the corresponding transaction implementation. For Astakos models do: + + .. code-block:: python + + from astakos.im import transaction + + + while for Cyclades models do: + + .. code-block:: python -6. Refresh the web page and see your changes + from synnefo.db import transaction diff --git a/docs/identity-api-guide.rst b/docs/identity-api-guide.rst index 94e459cafb9a4dfaf872c4b0fe4f5eb9d8f3b099..146771de1a09eb4706db33888ae2694c41fbb869 100644 --- a/docs/identity-api-guide.rst +++ b/docs/identity-api-guide.rst @@ -126,9 +126,6 @@ Return Code Description 500 (Internal Server Error) The request cannot be completed because of an internal error =========================== ===================== -.. warning:: The service is also available under ``/feedback``. - It will be removed in the next version. - Get User catalogs ^^^^^^^^^^^^^^^^^ @@ -180,9 +177,6 @@ Return Code Description 500 (Internal Server Error) The request cannot be completed because of an internal error =========================== ===================== -.. warning:: The service is also available under ``/user_catalogs``. - It will be removed in the next version. - Service API Operations ---------------------- @@ -243,9 +237,6 @@ Return Code Description 500 (Internal Server Error) The request cannot be completed because of an internal error =========================== ===================== -.. warning:: The service is also available under ``/service/api/user_catalogs``. - It will be removed in the next version. - Tokens API Operations ---------------------- diff --git a/docs/image-api-guide.rst b/docs/image-api-guide.rst index 9756942fa054fc52a89dba1644dfe4f1fb3d4b99..a19d771be76032ed203d181d3e0244c30baccd22 100644 --- a/docs/image-api-guide.rst +++ b/docs/image-api-guide.rst @@ -48,6 +48,17 @@ identity credentials. <http://docs.openstack.org/developer/glance/glanceapi.html#authentication>`_, with the only difference being the suggested identity manager. +Image Metadata Format +--------------------- + +In Cyclades Image API all image metadata are viewed as HTTP headers that are +starting with the `x-image-meta-` prefix. All metadata must be encoded with the +`UTF-8` encoding. Since the image metadata must be valid HTTP headers, user +defined metadata like the image's name or properties must also be properly +quoted. Finally, image properties that are viewed as HTTP headers and are +starting with the `x-image-meta-property-` prefix, are not case-sensitive and +all punctuation characters will be replaced with underscore. + List Available Images --------------------- diff --git a/docs/images/available-attribute.png b/docs/images/available-attribute.png new file mode 100644 index 0000000000000000000000000000000000000000..af5038210768bd244200b7f0936105225546710a Binary files /dev/null and b/docs/images/available-attribute.png differ diff --git a/docs/images/create-snapshot.png b/docs/images/create-snapshot.png new file mode 100644 index 0000000000000000000000000000000000000000..f4303f87cdf94627e37933efc693ccf3a043eacb Binary files /dev/null and b/docs/images/create-snapshot.png differ diff --git a/docs/images/spawn-from-snapshot.png b/docs/images/spawn-from-snapshot.png new file mode 100644 index 0000000000000000000000000000000000000000..5c48675d88944d283bc3005b5273823bcba42d78 Binary files /dev/null and b/docs/images/spawn-from-snapshot.png differ diff --git a/docs/index.rst b/docs/index.rst index 1755477d8f83f8190091f1b3fa24117284f35fe9..5e540419a3dce4fe5296bc05bec702e88cb71126 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,27 +3,30 @@ Welcome to the Synnefo documentation .. image:: /images/synnefo-logo.png -Synnefo is a complete open source cloud stack written in Python that provides -Compute, Network, Image, Volume and Storage services, similar to the ones -offered by AWS. Synnefo manages multiple `Ganeti -<http://code.google.com/p/ganeti>`_ clusters at the backend for handling +Synnefo is a complete open source IaaS cloud stack written in Python that +provides Compute, Network, Image, Volume and Object Storage services, similar to +the ones offered by AWS. Synnefo manages multiple `Ganeti +<http://code.google.com/p/ganeti>`_ clusters at the backend for handling of low-level VM operations and uses `Archipelago <http://www.synnefo.org/docs/archipelago/latest/>`_ to unify cloud storage. To boost 3rd-party compatibility, Synnefo exposes the OpenStack APIs to users. -Synnefo powers GRNET's `~okeanos public cloud service +Synnefo came out of GRNET's `~okeanos public cloud service <http://okeanos.grnet.gr>`_ and you can try it out live at `demo.synnefo.org <http://demo.synnefo.org>`_. -Synnefo has three main components providing the corresponding services: +Synnefo has three main components which provide the corresponding services: .. toctree:: :maxdepth: 1 - Astakos: Identity/Account services <astakos> - Pithos: File/Object Storage service <pithos> + Astakos: Identity/Account/Quota services <astakos> + Pithos: Object Storage service <pithos> Cyclades: Compute/Network/Image/Volume services <cyclades> +It :ref:`unifies storage resources <unify>` (Objects/Volumes/Images/Snapshots) +using Archipelago as the common storage substrate for all services. + This is an overview of the Synnefo services: .. image:: images/synnefo-overview.png @@ -127,6 +130,17 @@ There are also the following tools: Design ====== +v0.16 +----- + +.. toctree:: + :maxdepth: 1 + + Logging mechanism for Synnefo's management commands <design/logging-management-commands> + Resource-pool projects design <design/resource-pool-projects> + Volumes design <design/volumes> + Volume Snapshots design <design/volume-snapshots> + v0.15 ----- @@ -143,7 +157,7 @@ Drafts .. toctree:: :maxdepth: 1 - Resource-pool projects design <design/resource-pool-projects> + Multi-DB transactions in Django <design/multi-db-transactions> Contact diff --git a/docs/install-guide-centos.rst b/docs/install-guide-centos.rst index 5e7d2611e3f5f035cab8f2a15b32aea2cd4cb990..93f7add699c26d13eedfc675698490a2752c6c87 100644 --- a/docs/install-guide-centos.rst +++ b/docs/install-guide-centos.rst @@ -3,6 +3,13 @@ Administrator's Installation Guide (on CentOS 6.5) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. warning:: + + Currently, we don't provide packages for release candidate versions of + Synnefo for CentOS 6.5. Please consult the docs for `Synnefo 0.15.2 + <https://www.synnefo.org/docs/synnefo/0.15.2/index.html>`_ + instead. + This is the Administrator's installation guide on CentOS 6.5. It describes how to install the whole Synnefo stack on two (2) physical nodes, @@ -25,13 +32,6 @@ guide and just stop after the "Testing of Pithos" section. Installation of Synnefo / Introduction ====================================== -We will install the services with the above list's order. The last three -services will be installed in a single step (at the end), because at the moment -they are contained in the same software component (Cyclades). Furthermore, we -will install all services in the first physical node, except Pithos which will -be installed in the second, due to a conflict between the snf-pithos-app and -snf-cyclades-app component (scheduled to be fixed in the next version). - For the rest of the documentation we will refer to the first physical node as "node1" and the second as "node2". We will also assume that their domain names are "node1.example.com" and "node2.example.com" and their public IPs are "203.0.113.1" and @@ -79,9 +79,9 @@ You also need a shared directory visible by both nodes. Pithos will save all data inside this directory. By 'all data', we mean files, images, and Pithos specific mapping data. If you plan to upload more than one basic image, this directory should have at least 50GB of free space. During this guide, we will -assume that node1 acts as an NFS server and serves the directory ``/srv/pithos`` +assume that node1 acts as an NFS server and serves the directory ``/srv/arhip`` to node2 (be sure to set no_root_squash flag). Node2 has this directory -mounted under ``/srv/pithos``, too. +mounted under ``/srv/arhip``, too. Before starting the Synnefo installation, you will need basic third party software to be installed and configured on the physical nodes. We will describe @@ -107,6 +107,7 @@ General Synnefo dependencies * ntp (NTP daemon) * gevent * dnsmasq (DNS server) + * Archipelago You can install apache, ntp and rabbitmq by running: @@ -300,33 +301,76 @@ exchanges: We do not need to initialize the exchanges. This will be done automatically, during the Cyclades setup. -Pithos data directory setup -~~~~~~~~~~~~~~~~~~~~~~~~~~~ -As mentioned in the General Prerequisites section, there should be a directory -called ``/srv/pithos`` visible by both nodes. We create and setup the ``data`` -directory inside it: +System user/group setup +~~~~~~~~~~~~~~~~~~~~~~~ + +Before we continue with the installation we have to mention the user and +group that our components will run as. In short Archipelago (and +specifically the ``archipelago`` package) creates the ``archipelago`` +system user and group while synnefo (and specifically the ``snf-common`` +package) creates the ``synnefo`` system user and group. + +This guide uses NFS for Archipelago's physical storage backend. +Archipelago must have permissions to write on the shared dir. As +explained below the shared dir will be owned by ``archipelago:synnefo``. +Due to NFS restrictions, all nodes nodes must have common uid for the +``archipelago`` user and common gid for the ``synnefo`` group. So before +any Synnefo installation, we create them here in advance. We assume that +ids 200 and 300 are available across all nodes. .. code-block:: console - # mkdir /srv/pithos - # cd /srv/pithos - # mkdir data - # chown apache:apache data - # chmod g+ws data + # addgroup --system --gid 200 synnefo + # adduser --system --uid 200 --gid 200 --no-create-home \ + --gecos Synnefo synnefo + + # addgroup --system --gid 300 archipelago + # adduser --system --uid 300 --gid 300 --no-create-home \ + --gecos Archipelago archipelago -This directory must be shared via `NFS <https://en.wikipedia.org/wiki/Network_File_System>`_. -In order to do this, run: + + +NFS data directory setup +~~~~~~~~~~~~~~~~~~~~~~~~ + +The Archipelago directory must be shared via +`NFS <https://en.wikipedia.org/wiki/Network_File_System>`_. +As mentioned in the General Prerequisites section, there should be a +directory called ``/srv/archip/`` with ``blocks``, ``maps``, and +``locks`` subdirectories visible by both nodes. To create it run: + +.. code-block:: console + + # mkdir /srv/archip/ + # cd /srv/archip/ + # mkdir -p {maps,blocks,locks} + +Currently Archipelago is the only one that needs to have access to the +backing store. We could have the whole NFS isolated from Synnefo (owned +by ``archipelago:archipelago`` with ``640`` access permissions) but we +choose not to (e.g. some future extension could require access to the +backing store directly from Synnefo). Thus we set the ownership to +``archipelago:synnefo`` and access permissions to ``g+ws``. .. code-block:: console - # yum install rpcbind nfs-utils + # cd /srv/archip + # chown archipelago:synnefo {maps,blocks,locks} + # chmod 770 {maps,blocks,locks} + # chmod g+s {maps,blocks,locks} + +In order to install the NFS server, run: + +.. code-block:: console + + # yum install rpcbind nfs-kernel-server Now edit ``/etc/exports`` and add the following line: .. code-block:: console - /srv/pithos/ 203.0.113.2(rw,no_root_squash,sync,subtree_check) + /srv/archip/ 203.0.113.2(rw,no_root_squash,sync,subtree_check) Once done, run: @@ -335,6 +379,45 @@ Once done, run: # service rpcbind restart # service nfs restart +Archipelago setup +~~~~~~~~~~~~~~~~~ + +To install Archipelago, run: + +.. code-block:: console + + # yum install archipelago + +Now edit ``/etc/archipelago/archipelago.conf`` and tweak the following settings: + +* ``USER``: Let Archipelago run as ``archipelago`` user (default) + +* ``GROUP``: Let Archipelago run as ``synnefo`` group (archipelago by default) + +* ``SEGMENT_SIZE``: Adjust shared memory segment size according to your machine's + RAM. The default value is 2GB which in some situations might exceed your + machine's physical RAM. Consult also with `Archipelago administrator's guide + <https://www.synnefo.org/docs/archipelago/latest/admin-guide.html>`_ for an + appropriate value. + +Adjust the following settings of ``blockerb`` and ``blockerm`` to point to +their corresponding directories. + +In section ``blockerb`` set: + +* ``archip_dir``: ``/srv/archip/blocks`` + +In section ``blockerm`` set: + +* ``archip_dir``: ``/srv/archip/maps`` +* ``lock_dir``: ``/srv/archip/locks`` + +Finally, start Archipelago: + +.. code-block:: console + + # archipelago restart + DNS server setup ~~~~~~~~~~~~~~~~ @@ -346,7 +429,7 @@ In order to set up a dns server using dnsmasq do the following: # yum install dnsmasq -Then edit your ``/etc/hosts/`` file as follows: +Then edit your ``/etc/hosts`` file as follows: .. code-block:: console @@ -405,6 +488,7 @@ General Synnefo dependencies * gevent * certificates * dnsmasq (DNS server) + * Archipelago You can install the above by running: @@ -553,10 +637,8 @@ Copy the file ``/etc/gunicorn.d/synnefo.example`` to .. warning:: Do NOT start the server yet, because it won't find the - ``synnefo.settings`` module. Also, in case you are using ``/etc/hosts`` - instead of a DNS to get the hostnames, change ``--worker-class=gevent`` to - ``--worker-class=sync``. We will start the server after successful - installation of Astakos. If the server is running:: + ``synnefo.settings`` module. Also set the Gunicorn config file to + ``--config=/etc/synnefo/gunicorn-hooks/gunicorn-archipelago.py``. # service gunicorn stop @@ -665,18 +747,19 @@ notify administrators with a notice that a new account has just been verified. More specifically Astakos sends emails in the following cases - An email containing a verification link after each signup process. -- An email to the people listed in ``ADMINS`` setting after each email - verification if ``ASTAKOS_MODERATION`` setting is ``True``. The email - notifies administrators that an additional action is required in order to - activate the user. -- A welcome email to the user email and an admin notification to ``ADMINS`` - right after each account activation. -- Feedback messages submited from Astakos contact view and Astakos feedback - API endpoint are sent to contacts listed in ``HELPDESK`` setting. -- Project application request notifications to people included in ``HELPDESK`` - and ``MANAGERS`` settings. +- An email to the people listed in ``ACCOUNT_NOTIFICATIONS_RECIPIENTS`` + setting after each email verification if ``ASTAKOS_MODERATION`` setting is + ``True``. The email notifies administrators that an additional action is + required in order to activate the user. +- A welcome email to the user email and a notification to + ``ACCOUNT_NOTIFICATIONS_RECIPIENTS`` right after each account activation. +- Feedback messages submitted from Astakos contact view and Astakos feedback + API endpoint are sent to contacts listed in + ``FEEDBACK_NOTIFICATIONS_RECIPIENTS`` setting. +- Project application request notifications to people included in + ``PROJECT_NOTIFICATIONS_RECIPIENTS`` setting. - Notifications after each project members action (join request, membership - accepted/declinde etc.) to project members or project owners. + accepted/declined etc.) to project members or project owners. Astakos uses the Django internal email delivering mechanism to send email notifications. A simple configuration, using an external smtp server to @@ -713,11 +796,13 @@ Refer to `Django documentation <https://docs.djangoproject.com/en/1.4/topics/email/>`_ for additional information on available email settings. -As refered in the previous section, based on the operation that triggers -an email notification, the recipients list differs. Specifically, for -emails whose recipients include contacts from your service team -(administrators, managers, helpdesk etc) Synnefo provides the following -settings located in ``00-snf-common-admins.conf``: +As referred in the previous section, based on the operation that triggers an +email notification, the recipients list differs. For convenience (and backward +compatibility), Astakos defines three service teams (administrators, managers +and helpdesk) and send the above notifications to these teams in a +preconfigured way (ie. project notifications are sent to the members of +managers and helpdesk teams). These settings are located in +``00-snf-common-admins.conf``: .. code-block:: python @@ -877,15 +962,17 @@ Notice that in this installation astakos and cyclades are in node1 and pithos is Setting Default Base Quota for Resources ---------------------------------------- -We now have to specify the limit on resources that each user can employ -(exempting resources offered by projects). When specifying storage or -memory size limits you can append a unit to the value, i.e. 10240 MB, -10 GB etc. Use the special value ``inf``, if you don't want to restrict a -resource. +All resources are registered with unlimited quota. We now have to restrict +the limit on the resources we wish to control. We can set the default quota +a new user is offered by the system (`system default`) with .. code-block:: console - # snf-manage resource-modify --default-quota-interactive + # snf-manage resource-modify <resource-name> --system-default <value> + +When specifying storage or memory size limits you can append a unit to the +value, i.e. 10240 MB, 10 GB etc. Use the special value ``inf``, if you don't +want to restrict a resource. Setting Resource Visibility --------------------------- @@ -1008,6 +1095,42 @@ This package provides the standalone Pithos web client. The web client is the web UI for Pithos and will be accessible by clicking "Pithos" on the Astakos interface's cloudbar, at the top of the Astakos homepage. +Installation of Archipelago on node 2 +===================================== + +To install Archipelago, run: + +.. code-block:: console + + # yum install archipelago + + +Now edit ``/etc/archipelago/archipelago.conf`` and tweak the following settings: + +* ``USER``: Let Archipelago run as ``archipelago`` user (default) + +* ``GROUP``: Let Archipelago run as ``synnefo`` group (defaults to archipelago) + +* ``SEGMENT_SIZE``: Adjust shared memory segment size according to your machine's + RAM. The default value is 2GB which in some situations might exceed your + machine's physical RAM. Consult also with `Archipelago administrator's guide + <https://www.synnefo.org/docs/archipelago/latest/admin-guide.html>`_ for an + appropriate value. + +In section ``blockerb`` set: + +* ``archip_dir``: ``/srv/arhip/blocks`` + +In section ``blockerm`` set: + +* ``archip_dir``: ``/srv/arhip/maps`` +* ``lock_dir``: ``/srv/arhip/locks`` + +Finally, restart Archipelago: + +.. code-block:: console + + # service archipelago restart .. _conf-pithos: @@ -1027,10 +1150,8 @@ Copy the file ``/etc/gunicorn.d/synnefo.example`` to .. warning:: Do NOT start the server yet, because it won't find the - ``synnefo.settings`` module. Also, in case you are using ``/etc/hosts`` - instead of a DNS to get the hostnames, change ``--worker-class=gevent`` to - ``--worker-class=sync``. We will start the server after successful - installation of Astakos. If the server is running:: + ``synnefo.settings`` module. Also set the Gunicorn config file to + ``--config=/etc/synnefo/gunicorn-hooks/gunicorn-archipelago.py``. # service gunicorn stop @@ -1052,7 +1173,6 @@ this options: PITHOS_BASE_URL = 'https://node2.example.com/pithos' PITHOS_BACKEND_DB_CONNECTION = 'postgresql://synnefo:example_passw0rd@node1.example.com:5432/snf_pithos' - PITHOS_BACKEND_BLOCK_PATH = '/srv/pithos/data' PITHOS_SERVICE_TOKEN = 'pithos_service_token22w' @@ -1063,11 +1183,6 @@ find the Pithos backend database. Above we tell Pithos that its database is ``example_passw0rd``. All those settings where setup during node1's "Database setup" section. -The ``PITHOS_BACKEND_BLOCK_PATH`` option tells to the Pithos app where to find -the Pithos backend data. Above we tell Pithos to store its data under -``/srv/pithos/data``, which is visible by both nodes. We have already setup this -directory at node1's "Pithos data directory setup" section. - The ``ASTAKOS_AUTH_URL`` option informs the Pithos app where Astakos is. The Astakos service is used for user management (authentication, quotas, etc.) @@ -1152,12 +1267,12 @@ First install the package nfs-common by running: root@node2:~ # yum install nfs-utils -now create the directory /srv/pithos/ and mount the remote directory to it: +Now create the directory /srv/arhip/ and mount the remote directory to it: .. code-block:: console - root@node2:~ # mkdir /srv/pithos/ - root@node2:~ # mount -t nfs 203.0.113.1:/srv/pithos/ /srv/pithos/ + root@node2:~ # mkdir /srv/arhip/ + root@node2:~ # mount -t nfs 203.0.113.1:/srv/arhip/ /srv/arhip/ Servers Initialization ---------------------- @@ -1360,28 +1475,36 @@ Ganeti nodes: It's time to install Ganeti. To be able to use hotplug (which will be part of the official Ganeti 2.10), we recommend using our Ganeti package version: -``2.8.4+snap1+b64v1+kvm2+ext1+lockfix1+ipfix1+ifdown1+backports5-1`` +``2.10.7+snap1+b64v1+ext1+lockfix1+ifdown1+qmp1+bpo1-1`` Let's briefly explain each patch set: - * snap adds snapshot support for ext disk template + * snap extends snapshot support for the ext disk template (separate LU) * b64 saves networks' bitarrays in a more compact representation - * kvm adds migration_caps hypervisor param * ext - * exports logical id in hooks * allows arbitrary params to reach kvm command (i.e. cache overrides disk_cache hvparam, heads and secs define the disk's geometry) * lockfix is a workaround for Issue #621 - * ipfix does not require IP if mode is routed (needed for IPv6 only NICs) * ifdown cleans up node's configuration upon instance migration/shutdown - * backports is a set of patches backported from stable-2.10 + * qmp replace HMP with QMP commands during hotplug + * bpo is a set of patches backported from later branches + + * Make name and uuid Disk attributes reach bdev (2.11) + * IDiskParams fixes (2.11) + * Proper support for the --cdrom option (2.12) + * Add migration capabilities as an hvparam (2.13) + * Convert hv_kvm to a package (2.12) + * Extend QMP support (2.12) + * Add access to IDiskParams (2.13) + * Support userspace access for ExtStorage (2.13) + * Allow NICs with routed mode and no IP (2.13) + * Add support for KVM multiqueue virtio-net (2.12) + * Support Snapshot() for the ExtStorage interface (2.13) + * Support disk hotplug even with chroot or SM (2.13) + * Some refactor wrt NICs at the HV level (2.12) - * Hotplug support - * Better networking support (NIC configuration scripts) - * Change IP pool to support NAT instances - * Change RAPI to accept depends body argument and shutdown_timeout .. note:: @@ -1398,7 +1521,7 @@ Ganeti will make use of drbd. To install drbd, you're gonna need to use packages from the `ELRepo <http://elrepo.org/tiki/tiki-index.php>`_. To install ELRepo, run: -.. code-block:: consolse +.. code-block:: console # rpm -Uvh http://www.elrepo.org/elrepo-release-6-6.el6.elrepo.noarch.rpm @@ -1479,18 +1602,11 @@ Configuration ~~~~~~~~~~~~~ snf-image supports native access to Images stored on Pithos. This means that it can talk directly to the Pithos backend, without the need of providing a -public URL. More details, are described in the next section. For now, the only -thing we need to do, is configure snf-image to access our Pithos backend. - -To do this, we need to set the corresponding variable in -``/etc/default/snf-image``, to reflect our Pithos setup: - -.. code-block:: console - - PITHOS_DATA="/srv/pithos/data" +public URL. More details, are described in the next section. If you have installed your Ganeti cluster on different nodes than node1 and -node2 make sure that ``/srv/pithos/data`` is visible by all of them. +node2 make sure that ``/srv/arhip/`` is visible by all of them and +Archipelago is installed and configured properly. If you would like to use Images that are also/only stored locally, you need to save them under ``IMAGE_DIR``, however this guide targets Images stored only on @@ -2005,7 +2121,7 @@ Edit ``/etc/synnefo/20-snf-cyclades-app-plankton.conf``: .. code-block:: console BACKEND_DB_CONNECTION = 'postgresql://synnefo:example_passw0rd@node1.example.com:5432/snf_pithos' - BACKEND_BLOCK_PATH = '/srv/pithos/data/' + BACKEND_BLOCK_PATH = '/srv/arhip/' In this file we configure the Image Service. ``BACKEND_DB_CONNECTION`` denotes the Pithos database (where the Image files are stored). So we set that @@ -2040,7 +2156,7 @@ Configure the vncauthproxy settings in .. code-block:: console - CYCLADES_VNCAUTHPROXY_OPTS = { + CYCLADES_VNCAUTHPROXY_OPTS = [{ 'auth_user': 'synnefo', 'auth_password': 'secret_password', 'server_address': '127.0.0.1', @@ -2048,13 +2164,32 @@ Configure the vncauthproxy settings in 'enable_ssl': False, 'ca_cert': None, 'strict': False, - } + }] Depending on your snf-vncauthproxy setup, you might want to tweak the above settings. Check the `documentation <http://www.synnefo.org/docs/snf-vncauthproxy/latest/index.html>`_ of snf-vncauthproxy for more information. +You should also provide snf-vncauthproxy with SSL certificates signed by a +trusted CA. You can either copy them to `/var/lib/vncauthproxy/{cert,key}.pem` +or inform vncauthproxy about the location of the certificates (via the +`DAEMON_OPTS` setting in `/etc/default/vncauthproxy`). + +:: + + DAEMON_OPTS="--pid-file=$PIDFILE --cert-file=<path_to_cert> --key-file=<path_to_key>" + +Both files should be readable by the `vncauthproxy` user or group. + +.. note:: + + When installing snf-vncauthproxy on the same node as Cyclades and using the + default settings for snf-vncauthproxy, the certificates should be issued to + the FQDN of the Cyclades worker. Refer to the :ref:`admin guide + <admin-guide-vnc>`, for more information on how to setup vncauthproxy on a + different host / interface. + We have now finished with the basic Cyclades configuration. Database Initialization @@ -2095,7 +2230,7 @@ You can see everything has been setup correctly by running: Enable the new backend by running: -.. code-block:: +.. code-block:: console $ snf-manage backend-modify --drained False 1 @@ -2455,5 +2590,51 @@ to state 'Running' and you will be able to use it. Click 'Console' to connect through VNC out of band, or click on the machine's icon to connect directly via SSH or RDP (for windows machines). + +Installation of Admin on node1 +============================== + +This section describes the installation of Admin. Admin is a Synnefo component +that provides to trusted users the ability to manage and view various different +Synnefo entities such as users, VMs, projects etc. + +We will install Admin on node1. To do so, we install the corresponding +package by running on node1 the following command: + +.. code-block:: console + + # yum install snf-admin-app + +Once the package is installed, we must configure the ``ADMIN_BASE_URL`` +setting. This setting is located in the ``20-snf-admin-app-general.conf`` +settings file. Uncomment it and assign the following URL to it: + + ``https://node1.example.com/admin`` + +Now, we can proceed with testing Admin. + +Testing of Admin +================ + +In order to test the Admin Dashboard, we need a user that belongs to the +`admin` group. We will use the user that was created in `Testing of Astakos`_ +section: + +.. code-block:: console + + root@node1:~ # snf-manage group-add admin + root@node1:~ # snf-manage user-modify 1 --add-group=admin + +Then, you need to login to the Astakos node by visiting the following URL: + + ``https://node1.example.com/astakos`` + +Once you login successfully, you can access the Admin Dashboard from this URL: + + ``https://node1.example.com/admin`` + +This should redirect you to the **Users** table, where there should be an entry +with this user. + Congratulations. You have successfully installed the whole Synnefo stack and connected all components. diff --git a/docs/install-guide-debian.rst b/docs/install-guide-debian.rst index 971751059d88720e58d2357d6293f8da5b10e825..5a54014ce2d8c101cab272ed0018e89e56d714a1 100644 --- a/docs/install-guide-debian.rst +++ b/docs/install-guide-debian.rst @@ -25,13 +25,6 @@ guide and just stop after the "Testing of Pithos" section. Installation of Synnefo / Introduction ====================================== -We will install the services with the above list's order. The last three -services will be installed in a single step (at the end), because at the moment -they are contained in the same software component (Cyclades). Furthermore, we -will install all services in the first physical node, except Pithos which will -be installed in the second, due to a conflict between the snf-pithos-app and -snf-cyclades-app component (scheduled to be fixed in the next version). - For the rest of the documentation we will refer to the first physical node as "node1" and the second as "node2". We will also assume that their domain names are "node1.example.com" and "node2.example.com" and their public IPs are "203.0.113.1" and @@ -62,13 +55,13 @@ Update your list of packages and continue with the installation: # apt-get update -You also need a shared directory visible by both nodes. Pithos will save all -data inside this directory. By 'all data', we mean files, images, and Pithos -specific mapping data. If you plan to upload more than one basic image, this -directory should have at least 50GB of free space. During this guide, we will -assume that node1 acts as an NFS server and serves the directory ``/srv/pithos`` -to node2 (be sure to set no_root_squash flag). Node2 has this directory -mounted under ``/srv/pithos``, too. +From version 0.16 Synnefo is backed by Archipelago and uses it to store all of +its data. In this guide, we will use NFS as a storage backend for Archipelago. +If you plan to upload more than one basic image, this directory should have at +least 50GB of free space. During this guide, we will assume that node1 acts as +an NFS server and serves the directory ``/srv/archip/`` to node2 (be sure to set +no_root_squash flag). Node2 has this directory mounted under ``/srv/archip/``, +too. Before starting the Synnefo installation, you will need basic third party software to be installed and configured on the physical nodes. We will describe @@ -94,6 +87,7 @@ General Synnefo dependencies * ntp (NTP daemon) * gevent * dnsmasq (DNS server) + * Archipelago You can install apache2, postgresql, ntp and rabbitmq by running: @@ -313,23 +307,65 @@ exchanges: We do not need to initialize the exchanges. This will be done automatically, during the Cyclades setup. -Pithos data directory setup -~~~~~~~~~~~~~~~~~~~~~~~~~~~ -As mentioned in the General Prerequisites section, there should be a directory -called ``/srv/pithos`` visible by both nodes. We create and setup the ``data`` -directory inside it: +System user/group setup +~~~~~~~~~~~~~~~~~~~~~~~ + +Before we continue with the installation we have to mention the user and +group that our components will run as. In short Archipelago (and +specifically the ``archipelago`` package) creates the ``archipelago`` +system user and group while synnefo (and specifically the ``snf-common`` +package) creates the ``synnefo`` system user and group. + +This guide uses NFS for Archipelago's physical storage backend. +Archipelago must have permissions to write on the shared dir. As +explained below the shared dir will be owned by ``archipelago:synnefo``. +Due to NFS restrictions, all nodes nodes must have common uid for the +``archipelago`` user and common gid for the ``synnefo`` group. So before +any Synnefo installation, we create them here in advance. We assume that +ids 200 and 300 are available across all nodes. .. code-block:: console - # mkdir /srv/pithos - # cd /srv/pithos - # mkdir data - # chown www-data:www-data data - # chmod g+ws data + # addgroup --system --gid 200 synnefo + # adduser --system --uid 200 --gid 200 --no-create-home \ + --gecos Synnefo synnefo -This directory must be shared via `NFS <https://en.wikipedia.org/wiki/Network_File_System>`_. -In order to do this, run: + # addgroup --system --gid 300 archipelago + # adduser --system --uid 300 --gid 300 --no-create-home \ + --gecos Archipelago archipelago + + +NFS data directory setup +~~~~~~~~~~~~~~~~~~~~~~~~ + +The Archipelago directory must be shared via +`NFS <https://en.wikipedia.org/wiki/Network_File_System>`_. +As mentioned in the General Prerequisites section, there should be a +directory called ``/srv/archip/`` with ``blocks``, ``maps``, and +``locks`` subdirectories visible by both nodes. To create it run: + +.. code-block:: console + + # mkdir /srv/archip/ + # cd /srv/archip/ + # mkdir -p {maps,blocks,locks} + +Currently Archipelago is the only one that needs to have access to the +backing store. We could have the whole NFS isolated from Synnefo (owned +by ``archipelago:archipelago`` with ``640`` access permissions) but we +choose not to (e.g. some future extension could require access to the +backing store directly from Synnefo). Thus we set the ownership to +``archipelago:synnefo`` and access permissions to ``g+ws``. + +.. code-block:: console + + # cd /srv/archip + # chown archipelago:synnefo {maps,blocks,locks} + # chmod 770 {maps,blocks,locks} + # chmod g+s {maps,blocks,locks} + +In order to install the NFS server, run: .. code-block:: console @@ -339,7 +375,7 @@ Now edit ``/etc/exports`` and add the following line: .. code-block:: console - /srv/pithos/ 203.0.113.2(rw,no_root_squash,sync,subtree_check) + /srv/archip/ 203.0.113.2(rw,no_root_squash,sync,subtree_check) Once done, run: @@ -347,6 +383,46 @@ Once done, run: # /etc/init.d/nfs-kernel-server restart +Archipelago setup +~~~~~~~~~~~~~~~~~ + +To install Archipelago, run: + +.. code-block:: console + + root@node1:~ # apt-get install archipelago archipelago-ganeti + root@node1:~ # apt-get install blktap-archipelago-utils blktap-dkms + +Now edit ``/etc/archipelago/archipelago.conf`` and tweak the following settings: + +* ``USER``: Let Archipelago run as ``archipelago`` user (default) + +* ``GROUP``: Let Archipelago run as ``synnefo`` group (archipelago by default) + +* ``SEGMENT_SIZE``: Adjust shared memory segment size according to your machine's + RAM. The default value is 2GB which in some situations might exceed your + machine's physical RAM. Consult also with `Archipelago administrator's guide + <https://www.synnefo.org/docs/archipelago/latest/admin-guide.html>`_ for an + appropriate value. + +Adjust the following settings of ``blockerb`` and ``blockerm`` to point to +their corresponding directories. + +In section ``blockerb`` set: + +* ``archip_dir``: ``/srv/archip/blocks`` + +In section ``blockerm`` set: + +* ``archip_dir``: ``/srv/archip/maps`` +* ``lock_dir``: ``/srv/archip/locks`` + +Finally, start Archipelago: + +.. code-block:: console + + root@node1:~ # /etc/init.d/archipelago start + DNS server setup ~~~~~~~~~~~~~~~~ @@ -358,7 +434,7 @@ In order to set up a dns server using dnsmasq do the following: # apt-get install dnsmasq -Then edit your ``/etc/hosts/`` file as follows: +Then edit your ``/etc/hosts`` file as follows: .. code-block:: console @@ -417,6 +493,8 @@ General Synnefo dependencies * gevent * certificates * dnsmasq (DNS server) + * NFS directory mount + * Archipelago You can install the above by running: @@ -534,6 +612,60 @@ Copy the certificate you created before on node1 (`ca.crt`) under the directory to update the records. +Installation of Archipelago +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To install Archipelago, run: + +.. code-block:: console + + root@node2:~ # apt-get install archipelago archipelago-ganeti + root@node2:~ # apt-get install blktap-archipelago-utils blktap-dkms + +In order to configure Archipelago, the shared data directory must be set up. +Make sure to mount the Archipelago directory after installing the Archipelago +package. + +First install the package nfs-common by running: + +.. code-block:: console + + root@node2:~ # apt-get install nfs-common + +Now create the directory /srv/archip/ and mount the remote directory to it: + +.. code-block:: console + + root@node2:~ # mkdir /srv/archip/ + root@node2:~ # mount -t nfs 203.0.113.1:/srv/archip/ /srv/archip/ + +Now edit ``/etc/archipelago/archipelago.conf`` and tweak the following settings: + +* ``SEGMENT_SIZE``: Adjust shared memory segment size according to your machine's + RAM. The default value is 2GB which in some situations might exceed your + machine's physical RAM. Consult also with `Archipelago administrator's guide + <https://www.synnefo.org/docs/archipelago/latest/admin-guide.html>`_ for an + appropriate value. + +Adjust the following settings of ``blockerb`` and ``blockerm`` to point to +their corresponding directories. + +In section ``blockerb`` set: + +* ``archip_dir``: ``/srv/archip/blocks`` + +In section ``blockerm`` set: + +* ``archip_dir``: ``/srv/archip/maps`` +* ``lock_dir``: ``/srv/archip/locks`` + +Finally, start Archipelago: + +.. code-block:: console + + root@node2:~ # /etc/init.d/archipelago start + + DNS Setup ~~~~~~~~~ @@ -561,7 +693,8 @@ described previously), by running: .. code-block:: console - # apt-get install snf-astakos-app snf-pithos-backend + # apt-get install snf-astakos-app + .. _conf-astakos: @@ -576,17 +709,17 @@ Copy the file ``/etc/gunicorn.d/synnefo.example`` to .. code-block:: console - # mv /etc/gunicorn.d/synnefo.example /etc/gunicorn.d/synnefo + # cp /etc/gunicorn.d/synnefo.example /etc/gunicorn.d/synnefo .. warning:: Do NOT start the server yet, because it won't find the - ``synnefo.settings`` module. Also, in case you are using ``/etc/hosts`` - instead of a DNS to get the hostnames, change ``--worker-class=gevent`` to - ``--worker-class=sync``. We will start the server after successful - installation of Astakos. If the server is running:: - + ``synnefo.settings`` module. We will start the server after successfully + installing of Astakos. If the server is running:: # /etc/init.d/gunicorn stop +.. Also set ``--config=/etc/synnefo/gunicorn-hooks/gunicorn-archipelago.py``. + + Conf Files ---------- @@ -692,18 +825,19 @@ notify administrators with a notice that a new account has just been verified. More specifically Astakos sends emails in the following cases - An email containing a verification link after each signup process. -- An email to the people listed in ``ADMINS`` setting after each email - verification if ``ASTAKOS_MODERATION`` setting is ``True``. The email - notifies administrators that an additional action is required in order to - activate the user. -- A welcome email to the user email and an admin notification to ``ADMINS`` - right after each account activation. -- Feedback messages submited from Astakos contact view and Astakos feedback - API endpoint are sent to contacts listed in ``HELPDESK`` setting. -- Project application request notifications to people included in ``HELPDESK`` - and ``MANAGERS`` settings. +- An email to the people listed in ``ACCOUNT_NOTIFICATIONS_RECIPIENTS`` + setting after each email verification if ``ASTAKOS_MODERATION`` setting is + ``True``. The email notifies administrators that an additional action is + required in order to activate the user. +- A welcome email to the user email and a notification to + ``ACCOUNT_NOTIFICATIONS_RECIPIENTS`` right after each account activation. +- Feedback messages submitted from Astakos contact view and Astakos feedback + API endpoint are sent to contacts listed in + ``FEEDBACK_NOTIFICATIONS_RECIPIENTS`` setting. +- Project application request notifications to people included in + ``PROJECT_NOTIFICATIONS_RECIPIENTS`` setting. - Notifications after each project members action (join request, membership - accepted/declinde etc.) to project members or project owners. + accepted/declined etc.) to project members or project owners. Astakos uses the Django internal email delivering mechanism to send email notifications. A simple configuration, using an external smtp server to @@ -740,11 +874,13 @@ Refer to `Django documentation <https://docs.djangoproject.com/en/1.4/topics/email/>`_ for additional information on available email settings. -As refered in the previous section, based on the operation that triggers -an email notification, the recipients list differs. Specifically, for -emails whose recipients include contacts from your service team -(administrators, managers, helpdesk etc) synnefo provides the following -settings located in ``00-snf-common-admins.conf``: +As referred in the previous section, based on the operation that triggers an +email notification, the recipients list differs. For convenience (and backward +compatibility), Astakos defines three service teams (administrators, managers +and helpdesk) and send the above notifications to these teams in a +preconfigured way (ie. project notifications are sent to the members of +managers and helpdesk teams). These settings are located in +``00-snf-common-admins.conf``: .. code-block:: python @@ -904,15 +1040,17 @@ Notice that in this installation astakos and cyclades are in node1 and pithos is Setting Default Base Quota for Resources ---------------------------------------- -We now have to specify the limit on resources that each user can employ -(exempting resources offered by projects). When specifying storage or -memory size limits you can append a unit to the value, i.e. 10240 MB, -10 GB etc. Use the special value ``inf``, if you don't want to restrict a -resource. +All resources are registered with unlimited quota. We now have to restrict +the limit on the resources we wish to control. We can set the default quota +a new user is offered by the system (`system default`) with .. code-block:: console - # snf-manage resource-modify --default-quota-interactive + # snf-manage resource-modify <resource-name> --system-default <value> + +When specifying storage or memory size limits you can append a unit to the +value, i.e. 10240 MB, 10 GB etc. Use the special value ``inf``, if you don't +want to restrict a resource. Setting Resource Visibility --------------------------- @@ -992,8 +1130,8 @@ Now we need to activate that user. Return to a command prompt at node1 and run: root@node1:~ # snf-manage user-list This command should show you a list with only one user; the one we just created. -This user should have an id with a value of ``1`` and flag "active" and -"verified" set to False. Now run: +This user should have an id with a value of ``1`` and flag "active" +set to False. Now run: .. code-block:: console @@ -1036,7 +1174,6 @@ This package provides the standalone Pithos web client. The web client is the web UI for Pithos and will be accessible by clicking "Pithos" on the Astakos interface's cloudbar, at the top of the Astakos homepage. - .. _conf-pithos: Configuration of Pithos @@ -1055,10 +1192,8 @@ Copy the file ``/etc/gunicorn.d/synnefo.example`` to .. warning:: Do NOT start the server yet, because it won't find the - ``synnefo.settings`` module. Also, in case you are using ``/etc/hosts`` - instead of a DNS to get the hostnames, change ``--worker-class=gevent`` to - ``--worker-class=sync``. We will start the server after successful - installation of Astakos. If the server is running:: + ``synnefo.settings`` module. We will start the server after successful + installation of Pithos. If the server is running:: # /etc/init.d/gunicorn stop @@ -1080,7 +1215,6 @@ this options: PITHOS_BASE_URL = 'https://node2.example.com/pithos' PITHOS_BACKEND_DB_CONNECTION = 'postgresql://synnefo:example_passw0rd@node1.example.com:5432/snf_pithos' - PITHOS_BACKEND_BLOCK_PATH = '/srv/pithos/data' PITHOS_SERVICE_TOKEN = 'pithos_service_token22w' @@ -1091,11 +1225,6 @@ find the Pithos backend database. Above we tell Pithos that its database is ``example_passw0rd``. All those settings where setup during node1's "Database setup" section. -The ``PITHOS_BACKEND_BLOCK_PATH`` option tells to the Pithos app where to find -the Pithos backend data. Above we tell Pithos to store its data under -``/srv/pithos/data``, which is visible by both nodes. We have already setup this -directory at node1's "Pithos data directory setup" section. - The ``ASTAKOS_AUTH_URL`` option informs the Pithos app where Astakos is. The Astakos service is used for user management (authentication, quotas, etc.) @@ -1156,10 +1285,29 @@ context. This means adding the following lines at the top of your from synnefo.lib.db.psyco_gevent import make_psycopg_green make_psycopg_green() -Furthermore, add the ``--worker-class=gevent`` (or ``--worker-class=sync`` as -mentioned above, depending on your setup) argument on your -``/etc/gunicorn.d/synnefo`` configuration file. The file should look something -like this: + +.. _conf-pithos-gunicorn: + +Pithos gunicorn configuration +----------------------------- + +We also need to adjust Pithos gunicorn configuration in order to integrate with +Archipelago. The file, as mentioned above, is located at +``/etc/gunicorn.d/synnefo``. + +As of version 0.16 Pithos is backed by Archipelago. Pithos integrates with +Archipelago via a shared memory segment that is used to communicate with the +various Archipelago components. For more information regarding the Archipelago +internal architecture consult with the `Archipelago administrator's guide +<https://www.synnefo.org/docs/archipelago/latest/admin-guide.html>`_ + +Furthermore, we have to set the ``--config=/etc/synnefo/gunicorn-hooks/gunicorn-archipelago.py`` option. + +.. Furthermore, add the ``--worker-class=gevent`` (or ``--worker-class=sync`` as + mentioned above, depending on your setup) argument on your + ``/etc/gunicorn.d/synnefo`` configuration file. + +The file should look something like this: .. code-block:: console @@ -1169,17 +1317,19 @@ like this: 'DJANGO_SETTINGS_MODULE': 'synnefo.settings', }, 'working_dir': '/etc/synnefo', - 'user': 'www-data', - 'group': 'www-data', + 'user': 'synnefo', + 'group': 'synnefo', 'args': ( '--bind=127.0.0.1:8080', '--workers=4', '--worker-class=gevent', + '--config=/etc/synnefo/gunicorn-hooks/gunicorn-archipelago.py', '--log-level=debug', '--timeout=43200' ), } + Stamp Database Revision ----------------------- @@ -1195,22 +1345,6 @@ the migration history. root@node2:~ # pithos-migrate stamp head -Mount the NFS directory ------------------------ - -First install the package nfs-common by running: - -.. code-block:: console - - root@node2:~ # apt-get install nfs-common - -now create the directory /srv/pithos/ and mount the remote directory to it: - -.. code-block:: console - - root@node2:~ # mkdir /srv/pithos/ - root@node2:~ # mount -t nfs 203.0.113.1:/srv/pithos/ /srv/pithos/ - Servers Initialization ---------------------- @@ -1294,7 +1428,7 @@ Cyclades Prerequisites Before proceeding with the Cyclades installation, make sure you have successfully set up Astakos and Pithos first, because Cyclades depends on them. If you don't have a working Astakos and Pithos installation yet, please -return to the :ref:`top <install-guide-centos>` of this guide. +return to the :ref:`top <install-guide-debian>` of this guide. Besides Astakos and Pithos, you will also need a number of additional working prerequisites, before you start the Cyclades installation. @@ -1409,37 +1543,44 @@ Ganeti nodes: # apt-get install qemu-kvm -It's time to install Ganeti. To be able to use hotplug (which will be part of -the official Ganeti 2.10), we recommend using our Ganeti package version: +It's time to install Ganeti. We recommend using our Ganeti package version: -``2.8.4+snap1+b64v1+kvm2+ext1+lockfix1+ipfix1+ifdown1+backports5-1~wheezy`` +``2.10.7+snap1+b64v1+ext1+lockfix1+ifdown1+qmp1+bpo1-1~wheezy`` Let's briefly explain each patch set: - * snap adds snapshot support for ext disk template + * snap extends snapshot support for the ext disk template (separate LU) * b64 saves networks' bitarrays in a more compact representation - * kvm adds migration_caps hypervisor param * ext - * exports logical id in hooks * allows arbitrary params to reach kvm command (i.e. cache overrides disk_cache hvparam, heads and secs define the disk's geometry) * lockfix is a workaround for Issue #621 - * ipfix does not require IP if mode is routed (needed for IPv6 only NICs) * ifdown cleans up node's configuration upon instance migration/shutdown - * backports is a set of patches backported from stable-2.10 + * qmp replace HMP with QMP commands during hotplug + * bpo is a set of patches backported from later branches + + * Make name and uuid Disk attributes reach bdev (2.11) + * IDiskParams fixes (2.11) + * Proper support for the --cdrom option (2.12) + * Add migration capabilities as an hvparam (2.13) + * Convert hv_kvm to a package (2.12) + * Extend QMP support (2.12) + * Add access to IDiskParams (2.13) + * Support userspace access for ExtStorage (2.13) + * Allow NICs with routed mode and no IP (2.13) + * Add support for KVM multiqueue virtio-net (2.12) + * Support Snapshot() for the ExtStorage interface (2.13) + * Support disk hotplug even with chroot or SM (2.13) + * Some refactor wrt NICs at the HV level (2.12) - * Hotplug support - * Better networking support (NIC configuration scripts) - * Change IP pool to support NAT instances - * Change RAPI to accept depends body argument and shutdown_timeout To install Ganeti run: .. code-block:: console - # apt-get install snf-ganeti ganeti-htools ganeti-haskell ganeti2 + # apt-get install snf-ganeti ganeti2 Ganeti will make use of drbd. To enable this and make the configuration permanent you have to do the following : @@ -1496,29 +1637,39 @@ to handle image files stored on Pithos. It also needs `python-psycopg2` to be able to access the Pithos database. This is why, we also install them on *all* VM-capable Ganeti nodes. + +You must set the the ``PITHCAT_UMASK`` setting of snf-image to ``007``. On the +file ``/etc/default/snf-image`` uncomment or create the relevant setting and set +its value to ``007``. + .. warning:: - snf-image uses ``curl`` for handling URLs. This means that it will - not work out of the box if you try to use URLs served by servers which do - not have a valid certificate. In case you haven't followed the guide's - directions about the certificates, in order to circumvent this you should edit the file - ``/etc/default/snf-image``. Change ``#CURL="curl"`` to ``CURL="curl -k"`` on every node. + snf-image uses ``curl`` for handling URLs. This means that it will + not work out of the box if you try to use URLs served by servers which do + not have a valid certificate. In case you haven't followed the guide's + directions about the certificates, in order to circumvent this you should + edit the file ``/etc/default/snf-image``. Change ``# CURL="curl"`` to + ``CURL="curl -k"`` on every node. + +.. warning:: + If you are using qemu-kvm from wheezy-backports, note that the official + 2.1.0 version has a ACPI regression bug (see + `here <https://lists.nongnu.org/archive/html/qemu-devel/2014-08/msg03536.html>`_). + This bug has reached the + `Debian qemu-kvm 2.1+dfsg-2~bpo70+2 package <https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=759522>`_ + found in wheezy-backports and is triggered by snf-image. Until a newer package is + out, you can workaround it by editing the file ``/etc/default/snf-image`` + and changing ``# KVM="kvm"`` to ``KVM="qemu-system-x86_64 -enable-kvm -machine pc-i440fx-2.0,accel=kvm"`` + on every node. Configuration ~~~~~~~~~~~~~ snf-image supports native access to Images stored on Pithos. This means that it can talk directly to the Pithos backend, without the need of providing a -public URL. More details, are described in the next section. For now, the only -thing we need to do, is configure snf-image to access our Pithos backend. - -To do this, we need to set the corresponding variable in -``/etc/default/snf-image``, to reflect our Pithos setup: - -.. code-block:: console - - PITHOS_DATA="/srv/pithos/data" +public URL. More details, are described in the next section. If you have installed your Ganeti cluster on different nodes than node1 and -node2 make sure that ``/srv/pithos/data`` is visible by all of them. +node2 make sure that ``/srv/archip/`` is visible by all of them and +Archipelago is installed and configured properly. If you would like to use Images that are also/only stored locally, you need to save them under ``IMAGE_DIR``, however this guide targets Images stored only on @@ -1603,27 +1754,42 @@ for this image. To spawn a VM from a Pithos file, we need to know: - 1) The hashmap of the file + 1) The mapfile name of the file 2) The size of the file -If you uploaded the file with kamaki as described above, run: +If you uploaded the file with kamaki as described above, run on the Astakos +node: .. code-block:: console - # kamaki file info pithos:debian_base-6.0-x86_64.diskdump + # snf-manage user-list -else, replace ``pithos`` and ``debian_base-6.0-x86_64.diskdump`` with the -container and filename you used, when uploading the file. +to get a list of users. Then run the following: -The hashmap is the field ``x-object-hash``, while the size of the file is the -``content-length`` field, that ``kamaki file info`` command returns. +.. code-block:: console + + # snf-manage user-show 1 + +where 1 is the id of the user that uploaded the image, as retrieved by the +previous command. This will output the user's uuid (among others). + +Then on the Pithos node run the following: + +.. code-block:: console + + # snf-manage file-show <user uuid> pithos debian_base-6.0-x86_64.diskdump + +Replace ``pithos`` and ``debian_base-6.0-x86_64.diskdump`` with the +container and filename you used, when uploading the file. +This will output the following info (among others): the name of the pithos +mapfile (``mapfile`` field) and the size of the image (``bytes`` field). Run on the :ref:`GANETI-MASTER's <GANETI_NODES>` (node1) command line: .. code-block:: console # gnt-instance add -o snf-image+default --os-parameters \ - img_passwd=my_vm_example_passw0rd,img_format=diskdump,img_id="pithosmap://<HashMap>/<Size>",img_properties='{"OSFAMILY":"linux"\,"ROOT_PARTITION":"1"}' \ + img_passwd=my_vm_example_passw0rd,img_format=diskdump,img_id="pithosmap://<mapfile>/<Size>",img_properties='{"OSFAMILY":"linux"\,"ROOT_PARTITION":"1"}' \ -t plain --disk 0:size=2G --no-name-check --no-ip-check \ testvm1 @@ -1958,7 +2124,7 @@ package by running on node1: .. code-block:: console - # apt-get install snf-cyclades-app memcached python-memcache + # apt-get install snf-cyclades-app memcached python-memcache snf-pithos-backend If all packages install successfully, then Cyclades are installed and we proceed with their configuration. @@ -2033,12 +2199,10 @@ Edit ``/etc/synnefo/20-snf-cyclades-app-plankton.conf``: .. code-block:: console BACKEND_DB_CONNECTION = 'postgresql://synnefo:example_passw0rd@node1.example.com:5432/snf_pithos' - BACKEND_BLOCK_PATH = '/srv/pithos/data/' In this file we configure the Image Service. ``BACKEND_DB_CONNECTION`` denotes the Pithos database (where the Image files are stored). So we set that -to point to our Pithos database. ``BACKEND_BLOCK_PATH`` denotes the actual -Pithos data location. +to point to our Pithos database. Edit ``/etc/synnefo/20-snf-cyclades-app-queues.conf``: @@ -2068,7 +2232,7 @@ Configure the vncauthproxy settings in .. code-block:: console - CYCLADES_VNCAUTHPROXY_OPTS = { + CYCLADES_VNCAUTHPROXY_OPTS = [{ 'auth_user': 'synnefo', 'auth_password': 'secret_password', 'server_address': '127.0.0.1', @@ -2076,15 +2240,49 @@ Configure the vncauthproxy settings in 'enable_ssl': False, 'ca_cert': None, 'strict': False, - } + }] + Depending on your snf-vncauthproxy setup, you might want to tweak the above settings. Check the `documentation <http://www.synnefo.org/docs/snf-vncauthproxy/latest/index.html>`_ of snf-vncauthproxy for more information. +You should also provide snf-vncauthproxy with SSL certificates signed by a +trusted CA. You can either copy them to `/var/lib/vncauthproxy/{cert,key}.pem` +or inform vncauthproxy about the location of the certificates (via the +`DAEMON_OPTS` setting in `/etc/default/vncauthproxy`). + +:: + + DAEMON_OPTS="--pid-file=$PIDFILE --cert-file=<path_to_cert> --key-file=<path_to_key>" + +Both files should be readable by the `vncauthproxy` user or group. + +.. note:: + + When installing snf-vncauthproxy on the same node as Cyclades and using the + default settings for snf-vncauthproxy, the certificates should be issued to + the FQDN of the Cyclades worker. Refer to the :ref:`admin guide + <admin-guide-vnc>`, for more information on how to setup vncauthproxy on a + different host / interface. + We have now finished with the basic Cyclades configuration. +Gunicorn configuration +---------------------- + +Cyclades uses Pithos backend library to access and store system and +user-provided images and snapshots. + +We need to adjust gunicorn configuration in order to integrate with +Archipelago. Set the +``--config=/etc/synnefo/gunicorn-hooks/gunicorn-archipelago.py`` option +in the gunicorn configuration file located at +``/etc/gunicorn.d/synnefo``. + + + Database Initialization ----------------------- @@ -2123,7 +2321,7 @@ You can see everything has been setup correctly by running: Enable the new backend by running: -.. code-block:: +.. code-block:: console $ snf-manage backend-modify --drained False 1 @@ -2291,7 +2489,7 @@ skipped. .. code-block:: console - node1 # snf-manage quota-verify --sync + node1 # snf-manage quota-verify --fix node1 # snf-manage reconcile-resources-astakos --fix node2 # snf-manage reconcile-resources-pithos --fix node1 # snf-manage reconcile-resources-cyclades --fix @@ -2483,5 +2681,51 @@ to state 'Running' and you will be able to use it. Click 'Console' to connect through VNC out of band, or click on the machine's icon to connect directly via SSH or RDP (for windows machines). + +Installation of Admin on node1 +============================== + +This section describes the installation of Admin. Admin is a Synnefo component +that provides to trusted users the ability to manage and view various different +Synnefo entities such as users, VMs, projects etc. + +We will install Admin on node1. To do so, we install the corresponding +package by running on node1 the following command: + +.. code-block:: console + + # apt-get install snf-admin-app + +Once the package is installed, we must configure the ``ADMIN_BASE_URL`` +setting. This setting is located in the ``20-snf-admin-app-general.conf`` +settings file. Uncomment it and assign the following URL to it: + + ``https://node1.example.com/admin`` + +Now, we can proceed with testing Admin. + +Testing of Admin +================ + +In order to test the Admin Dashboard, we need a user that belongs to the +`admin` group. We will use the user that was created in `Testing of Astakos`_ +section: + +.. code-block:: console + + root@node1:~ # snf-manage group-add admin + root@node1:~ # snf-manage user-modify 1 --add-group=admin + +Then, you need to login to the Astakos node by visiting the following URL: + + ``https://node1.example.com/astakos`` + +Once you login successfully, you can access the Admin Dashboard from this URL: + + ``https://node1.example.com/admin`` + +This should redirect you to the **Users** table, where there should be an entry +with this user. + Congratulations. You have successfully installed the whole Synnefo stack and connected all components. diff --git a/docs/network-api-guide.rst b/docs/network-api-guide.rst index 5074f003c1589d61e3f2cc5bd475d14af4a8a73f..05adf20fb85f866a677dc4673c6c613b670dbff2 100644 --- a/docs/network-api-guide.rst +++ b/docs/network-api-guide.rst @@ -21,16 +21,17 @@ API Operations ============== .. rubric:: Networks -===================================== ========================== ====== ======== ======= ========== -Description URI Method Cyclades/Network OS/Neutron -===================================== ========================== ====== ================ ========== -`List <#list-networks>`_ ``/networks`` GET ✔ ✔ -`Get details <#get-network-details>`_ ``/networks/<network-id>`` GET ✔ ✔ -`Create <#create-network>`_ ``/networks`` POST ✔ ✔ -Bulk creation ``/networks`` POST **✘** ✔ -`Update <#update-network>`_ ``/networks/<network-id>`` PUT ✔ ✔ -`Delete <#delete-network>`_ ``/networks/<network id>`` DELETE ✔ ✔ -===================================== ========================== ====== ================ ========== +===================================== ================================= ====== ======== ======= ========== +Description URI Method Cyclades/Network OS/Neutron +===================================== ================================= ====== ================ ========== +`List <#list-networks>`_ ``/networks`` GET ✔ ✔ +`Get details <#get-network-details>`_ ``/networks/<network-id>`` GET ✔ ✔ +`Create <#create-network>`_ ``/networks`` POST ✔ ✔ +Bulk creation ``/networks`` POST **✘** ✔ +`Update <#update-network>`_ ``/networks/<network-id>`` PUT ✔ ✔ +`Delete <#delete-network>`_ ``/networks/<network id>`` DELETE ✔ ✔ +`Reassign <#reassign-network>`_ ``/networks/<network-id>/action`` POST ✔ **✘** +===================================== ================================= ====== ================ ========== .. rubric:: Subnets ==================================== ======================== ====== ======== ======= ========== @@ -57,15 +58,16 @@ Bulk creation ``/ports`` POST **✘** ================================== ==================== ====== ================ ========== .. rubric:: Floating IPs -========================================= ================================ ====== ================ ========== -Description URI Method Cyclades/Network OS/Neutron Extensions -========================================= ================================ ====== ================ ========== -`List <#list-floating-ips>`_ ``/floatingips`` GET ✔ ✔ -`Get details <#get-floating-ip-details>`_ ``/floatingips/<floatingip-id>`` GET ✔ ✔ -`Create <#create-floating-ip>`_ ``/floatingips`` POST ✔ ✔ -Update ``/floatingips/<floatingip-id>`` PUT **✘** ✔ -`Delete <#delete-floating-ip>`_ ``/floatingips/<floatingip id>`` DELETE ✔ ✔ -========================================= ================================ ====== ================ ========== +========================================= ======================================= ====== ================ ========== +Description URI Method Cyclades/Network OS/Neutron Extensions +========================================= ======================================= ====== ================ ========== +`List <#list-floating-ips>`_ ``/floatingips`` GET ✔ ✔ +`Get details <#get-floating-ip-details>`_ ``/floatingips/<floatingip-id>`` GET ✔ ✔ +`Create <#create-floating-ip>`_ ``/floatingips`` POST ✔ ✔ +Update ``/floatingips/<floatingip-id>`` PUT **✘** ✔ +`Delete <#delete-floating-ip>`_ ``/floatingips/<floatingip id>`` DELETE ✔ ✔ +`Reassign <#reassign-floating-ip>`_ ``/floatingips/<floatingip-id>/action`` POST ✔ **✘** +========================================= ======================================= ====== ================ ========== List networks ------------- @@ -392,6 +394,48 @@ Return Code Description network or floating IPs reserved from its pool. The subnets that are connected to it, though, are automatically deleted upon network deletion. +Reassign Network +---------------- + +Assign a network to a different project. + +.. rubric:: Request + +================================= ====== ================ ========== +URI Method Cyclades/Network OS/Neutron +================================= ====== ================ ========== +``/networks/<network-id>/action`` POST ✔ **✘** +================================= ====== ================ ========== + +| + +============== ========================= +Request Header Value +============== ========================= +X-Auth-Token User authentication token +============== ========================= + +Request body contents:: + + reassign: { + project: <project-id> + } + +.. rubric:: Response + +=========================== ===================== +Return Code Description +=========================== ===================== +200 (OK) Request succeeded +400 (Bad Request) Malformed request +401 (Unauthorized) Missing or expired user token +403 (Forbidden) Not allowed to modify this network (e.g. public) +404 (Not Found) Network not found +500 (Internal Server Error) The request cannot be completed because of an +\ internal error +503 (Service Unavailable) The service is not currently available +=========================== ===================== + List subnets ------------ @@ -1248,6 +1292,49 @@ Return Code Description 404 (itemNoFound) Floating IP not found =========================== ===================== +Reassign floating ip +-------------------- + +Assign a floating IP to a different project. + +.. rubric:: Request + +======================================= ====== ================ ========== +URI Method Cyclades/Network OS/Neutron +======================================= ====== ================ ========== +``/floatingips/<floatingip-id>/action`` POST ✔ **✘** +======================================= ====== ================ ========== + +| + +============== ========================= +Request Header Value +============== ========================= +X-Auth-Token User authentication token +============== ========================= + +Request body contents:: + + reassign: { + project: <project-id> + } + +.. rubric:: Response + +=========================== ===================== +Return Code Description +=========================== ===================== +200 (OK) Request succeeded +400 (Bad Request) Malformed request +401 (Unauthorized) Missing or expired user token +403 (Forbidden) Not allowed to modify this network (e.g. public) +404 (Not Found) Network not found +500 (Internal Server Error) The request cannot be completed because of an +\ internal error +503 (Service Unavailable) The service is not currently available +=========================== ===================== + + Index of Attributes ------------------- diff --git a/docs/networks.rst b/docs/networks.rst index 55c146d15ba601cf76886034575f696937e71ca3..8590e04b9c8c1b3385861a3868f1b06d7267a228 100644 --- a/docs/networks.rst +++ b/docs/networks.rst @@ -453,7 +453,7 @@ that he can access the VPN. Then we run in Cyclades: .. code-block:: console - # snf-manage network-create --subnet=192.168.1.0/24 --gateway=192.168.1.0/24 --dhcp=True --flavor=CUSTOM --mode=bridged --link=br200 --mac-prefix=bb:00:44 --owner=user@grnet.gr --tags=nfdhcpd,vpn --name=vpn --backend-id=1 + # snf-manage network-create --subnet=192.168.1.0/24 --gateway=192.168.1.0/24 --dhcp=True --flavor=CUSTOM --mode=bridged --link=br200 --mac-prefix=bb:00:44 --user=user@grnet.gr --tags=nfdhcpd,vpn --name=vpn --backend-id=1 # snf-manage network-list id name flavor owner mac_prefix dhcp state link vms public IPv4 Subnet IPv4 Gateway diff --git a/docs/object-api-guide.rst b/docs/object-api-guide.rst index f00814da22d057e394d78f3e6f578a9b77a61ff7..89d9e613a61e3662b8009eeddf1a785558dc1bd5 100644 --- a/docs/object-api-guide.rst +++ b/docs/object-api-guide.rst @@ -19,7 +19,7 @@ The present document is meant to be read alongside the OOS API documentation. Th Whatever marked as to be determined (**TBD**), should not be considered by implementors. -More info about Pithos can be found here: https://code.grnet.gr/projects/pithos +More info about Pithos can be found `here <pithos.html>`_. Document Revisions ^^^^^^^^^^^^^^^^^^ @@ -27,6 +27,7 @@ Document Revisions ========================= ================================ Revision Description ========================= ================================ +0.16 (Aug 06, 2014) Enforce resource and group limitations 0.15 (Apr 03, 2014) Allow only JSON format in uploads using hashmaps. 0.15 (Feb 01, 2014) Optionally enforce a specific content disposition type. 0.14 (Jun 18, 2013) Forbidden response for public listing by non path owners. @@ -94,27 +95,13 @@ Revision Description Pithos Users and Authentication ------------------------------- -In Pithos, each user is uniquely identified by a token. All API requests require a token and each token is internally resolved to an account string. The API uses the account string to identify the user's own files, thus whether a request is local or cross-account. +In Pithos, all API requests require a token and each token is internally resolved to an account identifier. The API uses the account identifier to decide the files the user is eligible to access. -Pithos does not keep a user database. For development and testing purposes, user identifiers and their corresponding tokens can be defined in the settings file. However, Pithos is designed with an external authentication service in mind. This service must handle the details of validating user credentials and communicate with Pithos via a middleware software component that, given a token, fills in the internal request account variable. +Pithos is designed with an external authentication service in mind. This service must handle the details of validating user credentials and communicate with Pithos via a middleware software component that, given a token, fills in the internal request account variable. -Client software using Pithos, if not already knowing a user's identifier and token, should forward to the ``/login`` URI. The Pithos server, depending on its configuration will redirect to the appropriate login page. +Client software using Pithos, if not already knowing a user's identifier and authentication token, should forward to the appropriate login service. -The login URI accepts the following parameters: - -====================== ========================= -Request Parameter Name Value -====================== ========================= -next The URI to redirect to when the process is finished -renew Force token renewal (no value parameter) -force Force logout current user (no value parameter) -====================== ========================= - -When done with logging in, the service's login URI should redirect to the URI provided with ``next``, adding the ``token`` parameters which contains authentication token. - -If ``next`` request parameter is missing the call fails with BadRequest (400) response status. - -A user management service that implements a login URI according to these conventions is Astakos (https://code.grnet.gr/projects/astakos), by GRNET. +Such a login API call following to above conventions is the `Weblogin <weblogin-api-guide.html>`_, implemented inside `Astakos <astakos.html>`_. User feedback ------------- @@ -334,17 +321,18 @@ until Optional timestamp Cross-user requests are not allowed to use ``until`` and only include the account modification date in the reply. -========================== ===================== -Reply Header Name Value -========================== ===================== -X-Account-Container-Count The total number of containers -X-Account-Bytes-Used The total number of bytes stored -X-Account-Until-Timestamp The last account modification date until the timestamp provided -X-Account-Group-* Optional user defined groups -X-Account-Policy-Quota Account quota limit -X-Account-Meta-* Optional user defined metadata -Last-Modified The last account modification date (regardless of ``until``) -========================== ===================== +===================================== ===================== +Reply Header Name Value +===================================== ===================== +X-Account-Container-Count The total number of containers +X-Account-Bytes-Used The total number of bytes stored +X-Account-Until-Timestamp The last account modification date until the timestamp provided +X-Account-Group-* Optional user defined groups +X-Account-Policy-Quota-<project_uuid> Project quota limit (This header is repeated for each project the user is enrolled and allocates Pithos disk space. All the accounts have a default project whose UUID is the account's UUID. More details about the quota allocation can be found in `Resource-pool projects <design/resource-pool-projects.html>`_ section) +X-Account-Policy-Quota The summary of all the project quota limits +X-Account-Meta-* Optional user defined metadata +Last-Modified The last account modification date (regardless of ``until``) +===================================== ===================== | @@ -462,11 +450,12 @@ No reply content/headers. The operation will overwrite all user defined metadata, except if ``update`` is defined. To create a group, include an ``X-Account-Group-*`` header with the name in the key and a comma separated list of user identifiers in the value. If no ``X-Account-Group-*`` header is present, no changes will be applied to groups. The ``update`` parameter also applies to groups. To delete a specific group, use ``update`` and an empty header value. -================ =============================== -Return Code Description -================ =============================== -202 (Accepted) The request has been accepted -================ =============================== +================= =============================== +Return Code Description +================= =============================== +202 (Accepted) The request has been accepted +400 (Bad Request) The metadata exceed in number the allowed account metadata or the groups exceed in number the allowed groups or the group members exceed in number the allowed group members +================= =============================== Container Level @@ -667,15 +656,17 @@ Available policy directives: * ``versioning``: Set to ``auto`` or ``none`` (default is ``auto``) * ``quota``: Size limit in KB (default is ``0`` - unlimited) +* ``project``: The project origin of the container quota If the container already exists, the operation is equal to a ``POST`` with ``update`` defined. -================ =============================== -Return Code Description -================ =============================== -201 (Created) The container has been created -202 (Accepted) The request has been accepted -================ =============================== +============================== =============================== +Return Code Description +============================== =============================== +201 (Created) The container has been created +202 (Accepted) The request has been accepted +413 (Request Entity Too Large) Insufficient quota to complete the request +============================== =============================== POST @@ -707,11 +698,13 @@ To change policy, include an ``X-Container-Policy-*`` header with the name in th To upload blocks of data to the container, set ``Content-Type`` to ``application/octet-stream`` and ``Content-Length`` to a valid value (except if using ``chunked`` as the ``Transfer-Encoding``). -================ =============================== -Return Code Description -================ =============================== -202 (Accepted) The request has been accepted -================ =============================== +============================== =============================== +Return Code Description +============================== =============================== +202 (Accepted) The request has been accepted +400 (Bad Request) The metadata exceed in number the allowed container metadata +413 (Request Entity Too Large) Insufficient quota to complete the request +============================== =============================== DELETE @@ -961,6 +954,7 @@ The ``X-Object-Sharing`` header may include either a ``read=...`` comma-separate Return Code Description ============================== ============================== 201 (Created) The object has been created +403 (Forbidden) If ``X-Copy-From`` and the source object is not available in the storage backend. 409 (Conflict) The object can not be created from the provided hashmap (a list of missing hashes will be included in the reply) 411 (Length Required) Missing ``Content-Length`` or ``Content-Type`` in the request 413 (Request Entity Too Large) Insufficient quota to complete the request @@ -1012,6 +1006,8 @@ X-Object-Version The object's new version Return Code Description ============================== ============================== 201 (Created) The object has been created +400 (Bad Request) The metadata exceed in number the allowed object metadata +403 (Forbidden) If the source object is not available in the storage backend. 413 (Request Entity Too Large) Insufficient quota to complete the request ============================== ============================== @@ -1089,7 +1085,7 @@ Return Code Description ============================== ============================== 202 (Accepted) The request has been accepted (not a data update) 204 (No Content) The request succeeded (data updated) -400 (Bad Request) Invalid ``X-Object-Sharing`` or ``X-Object-Bytes`` header or missing ``Content-Range`` header or invalid source object or source object length is smaller than range length or ``Content-Length`` does not match range length +400 (Bad Request) Invalid ``X-Object-Sharing`` or ``X-Object-Bytes`` header or missing ``Content-Range`` header or invalid source object or source object length is smaller than range length or ``Content-Length`` does not match range length or the metadata exceed in number the allowed object metadata 411 (Length Required) Missing ``Content-Length`` in the request 413 (Request Entity Too Large) Insufficient quota to complete the request 416 (Range Not Satisfiable) The supplied range is invalid diff --git a/docs/pithos.rst b/docs/pithos.rst index b3e34a21208451f4d862789109f0327f1febbe36..fdf36b06f7feeb0a0f0796469af35cb2bece4236 100644 --- a/docs/pithos.rst +++ b/docs/pithos.rst @@ -1,19 +1,19 @@ .. _pithos: -File/Object Storage Service (Pithos) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Object Storage Service (Pithos) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Overview ======== Pithos is the Object/File Storage component of Synnefo. Users upload files on Pithos using either the Web UI, the command-line client, or native syncing -clients. It is a thin layer mapping user-files to content-addressable blocks -which are then stored on a storage backend. Files are split in blocks of fixed -size, which are hashed independently to create a unique identifier for each -block, so each file is represented by a sequence of block names (a -hashmap). This way, Pithos provides deduplication of file data; blocks -shared among files are only stored once. +clients. It is a thin layer mapping user-files to underlying Archipelago +virtual resources. Files are split in blocks of fixed size, which are hashed +independently to create a unique identifier for each block, so each file is +represented by a sequence of block names (a hashmap, essentially an Archipelago +mapfile). This way, Pithos provides deduplication of file data; blocks shared +among files are only stored once. The current implementation uses 4MB blocks hashed with SHA256. Content-based addressing also enables efficient two-way file syncing that can be used by all @@ -28,16 +28,7 @@ download the modified ones. Pithos runs at the cloud layer and exposes the OpenStack Object Storage API to the outside world, with custom extensions for syncing. Any client speaking to -OpenStack Swift can also be used to store objects in a Pithos deployment. The -process of mapping user files to hashed objects is independent from the actual -storage backend, which is selectable by the administrator using pluggable -drivers. Currently, Pithos has drivers for two storage backends: - - * files on a shared filesystem, e.g., NFS, Lustre, GPFS or GlusterFS - * objects on a Ceph/RADOS cluster. - -Whatever the storage backend, it is responsible for storing objects reliably, -without any connection to the cloud APIs or to the hashing operations. +OpenStack Swift can also be used to store objects in a Pithos deployment. OpenStack extensions @@ -74,20 +65,15 @@ Pithos Design Pithos is built on a layered architecture. The Pithos server speaks HTTP with the outside world. The HTTP operations implement an extended OpenStack Object -Storage API. The back end is a library meant to be used by internal code and -other front ends. For instance, the back end library, apart from being used in -Pithos for implementing the OpenStack Object Storage API, is also used in our -implementation of the OpenStack Image Service API. Moreover, the back end -library allows specification of different namespaces for metadata, so that the -same object can be viewed by different front end APIs with different sets of -metadata. Hence the same object can be viewed as a file in Pithos, with one set -of metadata, or as an image with a different set of metadata, in our -implementation of the OpenStack Image Service. - -The data component provides storage of block and the information needed to -retrieve them, while the metadata component is a database of nodes and -permissions. At the current implementation, data is saved to the filesystem and -metadata in an SQL database. +Storage API. The backend is a library meant to be used by internal code and +other front ends. For instance, the backend library, apart from being used in +Pithos for implementing the OpenStack Object Storage API, is also used in +Cyclades for the implementation of the OpenStack Image Service API. Moreover, +the backend library allows specification of different namespaces for metadata, +so that the same object can be viewed by different front end APIs with +different sets of metadata. Hence the same object can be viewed as a file in +Pithos, with one set of metadata, or as an image with a different set of +metadata, in our implementation of the OpenStack Image Service. Block-based Storage for the Client ---------------------------------- @@ -154,7 +140,7 @@ the following operations: * If the preconditions are met, the API front end requests from the back end the object's hashmap (hashmaps are indexed by the full path). -* The back end will read and return to the API front end the +* The backend will read and return to the API front end the object's hashmap from the underlying storage. * Depending on the HTTP ``Range`` header, the API front end asks from the back end the required blocks, giving @@ -194,8 +180,8 @@ or create new blocks. At the end, the front end will save the updated hashmap. It is also possible to pass a parameter to HTTP ``POST`` to specify that the data will come from another object, instead of being uploaded by the client. -Pithos Back End Nodes ---------------------- +Pithos Backend Nodes +-------------------- Pithos organizes entities in a tree hierarchy, with one tree node per path entry (see Figure). Nodes can be accounts, containers, and objects. A user may @@ -216,8 +202,8 @@ node corresponds to a unique path, and we keep its parent in the account/container/object hierarchy (that is, all objects have a container as their parent). -Pithos Back End Versions ------------------------- +Pithos Backend Versions +----------------------- For each object version we keep the root Merkle hash of the object it refers to, the size of the object, the last modification time and the user that @@ -235,8 +221,8 @@ of their accounts. In effect, this also allows them to take their containers back in time. This is implemented conceptually by taking a vertical line in the Figure and presenting to the user the state on the left side of the line. -Pithos Back End Permissions ---------------------------- +Pithos Backend Permissions +-------------------------- Pithos recognizes read and write permissions, which can be granted to individual users or groups of users. Groups as collections of users created at diff --git a/docs/project-api-guide.rst b/docs/project-api-guide.rst index 789f6a47f8c8f45ee48f0608dd45dd8a8710e88e..063be5f03d4ce52a9dbad9f4556d16b665b85eee 100644 --- a/docs/project-api-guide.rst +++ b/docs/project-api-guide.rst @@ -19,18 +19,19 @@ Request Header Name Value X-Auth-Token User authentication token ==================== ========================= -Request can specify a filter. +The request can include the following filters as GET parameters: +``state``, ``owner``, ``name``. + +It also supports parameter ``mode`` with possible values: ``member``, +``default``. The former restricts the result to active projects where the +request user is an active member. By default it returns all accessible +projects; see below. **Example Request**: .. code-block:: javascript - { - "filter": { - "state": ["active", "suspended"], - "owner": [uuid] - } - } +GET /account/v1.0/projects?state=active&owner=uuid **Response Codes**: @@ -79,25 +80,23 @@ Status Description { "id": proj_id, - "application": app_id, - "state": "pending" | "active" | "denied" | "dismissed" | "cancelled" | "suspended" | "terminated", + "state": "uninitialized" | "active" | "suspended" | "terminated" | "deleted", "creation_date": "2013-06-26T11:48:06.579100+00:00", "name": "name", "owner": uuid, - "homepage": homepage or null, - "description": description or null, - "start_date": date, + "homepage": homepage, + "description": description, "end_date": date, "join_policy": "auto" | "moderated" | "closed", "leave_policy": "auto" | "moderated" | "closed", - "max_members": natural number - "resources": {"cyclades.vm": {"project_capacity": int or null, + "max_members": int, + "private": boolean, + "system_project": boolean, + "resources": {"cyclades.vm": {"project_capacity": int, "member_capacity": int } } - # only if request user is admin or project owner: - "comments": comments, - "pending_application": last pending app id or null, + "last_application": last application or null, "deactivation_date": date # if applicable } @@ -126,7 +125,9 @@ X-Auth-Token User authentication token "end_date": date, "join_policy": "auto" | "moderated" | "closed", # default: "moderated" "leave_policy": "auto" | "moderated" | "closed", # default: "auto" - "resources": {"cyclades.vm": {"project_capacity": int or null, + "max_members": int, # optional + "private": boolean, # default: false + "resources": {"cyclades.vm": {"project_capacity": int, "member_capacity": int } } @@ -158,7 +159,7 @@ Status Description Modify a Project ................ -**POST** /account/v1.0/projects/<proj_id> +**PUT** /account/v1.0/projects/<proj_id> ==================== ========================= Request Header Name Value @@ -205,132 +206,14 @@ X-Auth-Token User authentication token .. code-block:: javascript { - <action>: "reason" - } - -<action> can be: "suspend", "unsuspend", "terminate", "reinstate" - -**Response Codes**: - -====== ============================ -Status Description -====== ============================ -200 Success -400 Bad Request -401 Unauthorized (Missing token) -403 Forbidden -404 Not Found -409 Conflict -500 Internal Server Error -====== ============================ - -Retrieve List of Applications -............................. - -**GET** /account/v1.0/projects/apps - -==================== ========================= -Request Header Name Value -==================== ========================= -X-Auth-Token User authentication token -==================== ========================= - -Get all accessible applications. See below. - -**Example optional request** - -.. code-block:: javascript - - { - "project": <project_id> - } - -**Response Codes**: - -====== ============================ -Status Description -====== ============================ -200 Success -400 Bad Request -401 Unauthorized (Missing token) -500 Internal Server Error -====== ============================ - -**Example Successful Response**: - -List of application details. See below. - -Retrieve an Application -....................... - -**GET** /account/v1.0/projects/apps/<app_id> - -==================== ========================= -Request Header Name Value -==================== ========================= -X-Auth-Token User authentication token -==================== ========================= - -An application is accessible when the request user is admin or the -application owner/applicant. - -**Response Codes**: - -====== ============================ -Status Description -====== ============================ -200 Success -401 Unauthorized (Missing token) -403 Forbidden -404 Not Found -500 Internal Server Error -====== ============================ - -**Example Successful Response** - -.. code-block:: javascript - - { - "id": app_id, - "project": project_id, - "state": "pending" | "approved" | "replaced" | "denied" | "dismissed" | "cancelled", - "name": "name", - "owner": uuid, - "applicant": uuid, - "homepage": homepage or null, - "description": description or null, - "start_date": date, - "end_date": date, - "join_policy": "auto" | "moderated" | "closed", - "leave_policy": "auto" | "moderated" | "closed", - "max_members": int or null - "comments": comments, - "resources": {"cyclades.vm": {"project_capacity": int or null, - "member_capacity": int - } - } - } - -Take Action on an Application -............................. - -**POST** /account/v1.0/projects/apps/<app_id>/action - -==================== ============================ -Request Header Name Value -==================== ============================ -X-Auth-Token User authentication token -==================== ============================ - -**Example Request**: - -.. code-block:: javascript - - { - <action>: "reason" + <action>: {"reason": reason, + "app_id": app_id # only for app related actions + } } -<action> can be one of "approve", "deny", "dismiss", "cancel". +<action> can be: "suspend", "unsuspend", "terminate", "reinstate", +"approve", "deny", "dismiss", "cancel". The last four actions operate on the +project's last application and require its ``app_id``. **Response Codes**: @@ -357,15 +240,8 @@ Request Header Name Value X-Auth-Token User authentication token ==================== ============================ -Get all accessible memberships. See below. - -**Example Optional Request** - -.. code-block:: javascript - - { - "project": <proj_id> - } +Get all accessible memberships. Filtering by project is possible via the GET +parameter ``project``. **Response Codes**: diff --git a/docs/quick-install-guide.rst b/docs/quick-install-guide.rst index 449f0f397d5921b3412ee34371dac01490b6c8ff..5e3c7a41a8dde0edec013591b63eaa418426a9ce 100644 --- a/docs/quick-install-guide.rst +++ b/docs/quick-install-guide.rst @@ -62,7 +62,7 @@ To install the whole Synnefo stack run: .. code-block:: console - # snf-deploy all --autoconf + # snf-deploy synnefo --autoconf This might take a while depending on the physical host you are running on, since it will download everything that is necessary, install and configure the whole @@ -70,34 +70,107 @@ stack. If the following ends without errors, you have successfully installed Synnefo. +NOTE: All the passwords and secrets used during installation are +hardcoded in `/etc/snf-deploy/synnefo.conf`. You can change them before +starting the installation process. If you want snf-deploy create random +passwords use the ``--pass-gen`` option. The generated passwords will be +kept in the `/var/lib/snf-deploy/snf_deploy_status` file. + +.. _access-synnefo: + Accessing the Synnefo installation ================================== Remote access ------------- -If you want to access the Synnefo installation from a remote machine, please -first set your nameservers accordingly by adding the following line as your -first nameserver in ``/etc/resolv.conf``: +If you want to access the Synnefo installation from a remote machine: -.. code-block:: console +Method 1: Modify DNS name resolution +____________________________________ + +* Set your nameservers accordingly by adding the following line as your + first nameserver in ``/etc/resolv.conf``: + + .. code-block:: console + + nameserver <IP> + + The **<IP>** is the public IP of the machine that you deployed Synnefo on, + and want to access. Note that ``/etc/resolv.conf`` can be overwritten by + other programs (``Network Manager``, ``dhclient``) and you may therefore lose + this line. Depending on your system, you may need to disable writes to + ``/etc/resolv.conf`` or prepend the nameservers in the + ``/etc/dhclient.conf``. + +Method 2: Modify /etc/hosts +___________________________ + +* Add the IP of your Synnefo installation in your ``/etc/hosts`` file: + + .. code-block:: console + + <IP> synnefo.live + <IP> accounts.synnefo.live + <IP> compute.synnefo.live + <IP> pithos.synnefo.live + + If you're using Windows the same settings can be applied on + ``C:\WINDOWS\SYSTEM32\DRIVERS\ETC\HOSTS``. - nameserver <IP> +Method 3: Use a SOCKS proxy (easier) +____________________________________ -The <IP> is the public IP of the machine that you deployed Synnefo on, and want -to access. +* Alternatively, you can setup a SOCKS proxy using the ssh client and instruct + your browser to use it. To setup a SOCKS proxy run: -Then open a browser and point to: + .. code-block:: console -`https://synnefo.live/` + $ ssh -D localhost:9009 user@host + + Now, you can either instruct your browser to tunnel all the traffic through + the SOCKS proxy or even better install a plugin like `Foxy Proxy + <https://addons.mozilla.org/en-US/firefox/addon/foxyproxy-standard/>`_ to fine + tune when to use the proxy or not. + + In order to use the proxy globally in Firefox, go to + ``Edit->Preferences->Advanced->Network->Settings`` and set ``SOCKS host`` to + ``localhost`` and ``Port`` to ``9009``. Firefox by default doesn't use the + SOCKS proxy for domain name resolving. To enable this, type ``about:config`` in + the URL bar and change ``network.proxy.socks_remote_dns`` to ``true``. + + For better control on which sites you view over the proxy, download FoxyProxy + and set a ``URL_Pattern`` to whitelist the ``synnefo.live`` domain. To do this + use the URL_Pattern ``*synnefo.live*`` and set FoxyProxy to run in the + ``Use proxies based on their pre-defined patterns and priorities`` mode. + + FoxyProxy is also available for Chrome through the `Chrome Web Store + <https://chrome.google.com/webstore/detail/foxyproxy-standard/gcknhkkoolaabfmlnjonogaaifnjlfnp?hl=en>`_, + so a similar approach will work in Chrome also. + + .. note:: + + Internet Explorer doesn't support SOCKS5 proxies. + +Then open a browser and point it to: + +`https://astakos.synnefo.live/astakos/ui/login` Local access ------------ -If you want to access the installation from the same machine it runs on, just -open a browser and point to: +If you want to access the installation from the same machine it runs on, you +must connect graphically to the machine first. A simple way is to use SSH with +X-forwarding: + +.. code-block:: console + + $ ssh <user>@<hostname> -YC + +where **<user>** is your username and **<hostname>** is the IP/hostname of your +machine. Then, run ``iceweasel`` or ``chromium`` and in the address bar write: -`https://synnefo.live/` +`https://astakos.synnefo.live/astakos/ui/login` The default <domain> is set to ``synnefo.live``. A local BIND is already set up by `snf-deploy` to serve all FQDNs. diff --git a/docs/quick-install-intgrt-guide.rst b/docs/quick-install-intgrt-guide.rst deleted file mode 100644 index ec6bc546ad9cca75f254af1578e769e52f2afbed..0000000000000000000000000000000000000000 --- a/docs/quick-install-intgrt-guide.rst +++ /dev/null @@ -1,564 +0,0 @@ -.. _quick-install-intgrt-guide: - -Integrator's Quick Installation Guide -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -This is the Integrator's quick installation guide. - -It is intended for developers, wishing to implement new functionality -inside Synnefo. It assumes thorough familiarity with the -:ref:`Synnefo Administrator's Guide <admin-guide>`. - -It describes how to install the whole synnefo stack on two (2) physical nodes, -with minimum configuration. It installs synnefo in a ``virtualenv`` using ``pip -install``, and assumes the nodes run Debian Squeeze. After successful -installation, you will have the following services running: - - * Identity Management (Astakos) - * Object Storage Service (Pithos) - * Compute Service (Cyclades) - * Image Service (part of Cyclades) - * Network Service (part of Cyclades) - -and a single unified Web UI to manage them all. - -The Volume Storage Service (Archipelago) and the Billing Service (Aquarium) are -not released yet. - -If you just want to install the Object Storage Service (Pithos), follow the guide -and just stop after the "Testing of Pithos" section. - -Building a dev environment --------------------------- - -virtualenv -********** - -The easiest method to deploy a development environment is using -:command:`virtualenv`. Alternatively, you can use your system's package manager -to install any dependencies of synnefo components (e.g. Macports has them all). - - .. code-block:: console - - $ virtualenv ~/synnefo-env - $ source ~/synnefo-env/bin/activate - (synnefo-env)$ - -Virtualenv creates an isolated python environment to the path you pass as the -first argument of the command. That means that all packages you install using -:command:`pip` or :command:`easy_install` will be placed in -``ENV/lib/pythonX.X/site-packages`` and their console scripts in ``ENV/bin/``. - -This allows you to develop against multiple versions of packages that your -software depends on without messing with system python packages that may be -needed in specific versions for other software you have installed on your -system. - -* It is also recommended to install development helpers: - - .. code-block:: console - - (synnefo-env)$ pip install django_extensions fabric>=1.3 - -* Create a custom settings directory for :ref:`snf-common <snf-common>` and set - the ``SYNNEFO_SETTINGS_DIR`` environment variable to use development-specific - file:`*.conf` files inside this directory. - - (synnefo-env)$ mkdir ~/synnefo-settings-dir - (synnefo-env)$ export SYNNEFO_SETTINGS_DIR=~/synnefo-settings-dir - - Insert your custom settings in a file such as :file:`$SYNNEFO_SETTINGS_DIR/99-local.conf`: - - .. code-block:: python - - # uncomment this if have django-extensions installed (pip install django_extensions) - #INSTALLED_APPS = list(INSTALLED_APPS) + ['django_extensions'] - - DEV_PATH = os.path.abspath(os.path.dirname(__file__)) - DATABASES['default']['NAME'] = os.path.join(DEV_PATH, "synnefo.sqlite") - - # development rabitmq configuration - RABBIT_HOST = "<RabbitMQ_host>" - RABBIT_USERNAME = "<RabbitMQ_username>" - RABBIT_PASSWORD = "<RabbitMQ_password>" - RABBIT_VHOST = "/" - - # development ganeti settings - GANETI_MASTER_IP = "<Ganeti_master_IP>" - GANETI_CLUSTER_INFO = (GANETI_MASTER_IP, 5080, "<username>", "<password>") - GANETI_CREATEINSTANCE_KWARGS['disk_template'] = 'plain' - - # This prefix gets used when determining the instance names - # of Synnefo VMs at the Ganeti backend. - # The dash must always appear in the name! - BACKEND_PREFIX_ID = "<your_commit_name>-" - - IGNORE_FLAVOR_DISK_SIZES = True - - # do not actually send emails - # save them as files in /tmp/synnefo-mails - EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend' - EMAIL_FILE_PATH = '/tmp/synnefo-mails' - - # for UI developers - UI_HANDLE_WINDOW_EXCEPTIONS = False - - # allow login using /?test url - BYPASS_AUTHENTICATION = True - -synnefo source -************** - -* Clone the repository of the synnefo software components you wish - to work on, e.g.: - - .. code-block:: console - - (synnefo-env)$ git clone https://code.grnet.gr/git/synnefo synnefo - (synnefo-env)$ git clone https://code.grnet.gr/git/pithos pithos - -* Install the software components you wish to work on inside the - virtualenv, in development mode: - - .. code-block:: console - - (synnefo-env)$ cd snf-cyclades-app - (synnefo-env)$ python setup.py develop -N - -* Initialize database: - - .. code-block:: console - - (synnefo-env)$ snf-manage syndb - (synnefo-env)$ snf-manage migrate - (synnefo-env)$ snf-manage loaddata users flavors images - -Development tips -**************** - -* Running a development web server: - - .. code-block:: console - - (synnefo-env)$ snf-manage runserver - - or, if you have the ``django_extensions`` and ``werkzeug`` packages installed: - - .. code-block:: console - - (synnefo-env)$ snf-manage runserver_plus - -* Opening a python console with the synnefo environment initialized: - - .. code-block:: console - - (synnefo-env)$ snf-manage shell - - or, with the django_extensions package installed: - - .. code-block:: console - - (synnefo-env)$ snf-manage shell_plus - - -South Database Migrations -------------------------- - -.. _cyclades-dev-initialmigration: - -Initial Migration -***************** - -To initialize south migrations in your database the following commands must be -executed: - -.. code-block:: console - - $ snf-manage syncdb --all # Create / update the database with the south tables - $ snf-manage migrate --fake # Perform migration in the database - - -Note that ``--all`` and ``--fake`` arguments are only needed when you are -initializing your database. If you want to migrate a previously create databse -to the latest db scheme just run the same commands without those arguments. - -If you are trying to migrate a database that already contains the changes that -applied from a specific migration script, ``south`` will probably notify you for -inconsistent db scheme, a workaround for that issue is to use ``--fake`` option -for a specific migration. - -For example: - - -.. code-block:: console - - $ snf-manage migrate db 0001 --fake - -To be sure that all migrations are applied use: - -.. code-block:: console - - $ snf-manage migrate db --list - -All starred migrations are applied. - -Schema migrations -***************** - -Do not use the syncdb management command. It can only be used the first time -and/or if you drop the database and must recreate it from scratch. See -:ref:`cyclades-dev-initialmigration`. - - -Every time you make changes to the database and data migration is not required -(WARNING: always perform this with extreme care): - -.. code-block:: console - - $ snf-manage schemamigration db --auto - -The above will create the migration script. Now this must be applied to the live -database: - -.. code-block:: console - - $ snf-manage migrate db - -Consider this example (adding a field to the ``SynnefoUser`` model): - -.. code-block:: console - - $ ./bin/python manage.py schemamigration db --auto - + Added field new_south_test_field on db.SynnefoUser - - Created 0002_auto__add_field_synnefouser_new_south_test_field.py. - -You can now apply this migration with: - -.. code-block:: console - - $ ./manage.py migrate db - Running migrations for db: - - Migrating forwards to 0002_auto__add_field_synnefouser_new_south_test_field. - > db:0002_auto__add_field_synnefouser_new_south_test_field - - Loading initial data for db. - - Installing json fixture 'initial_data' from '/home/bkarak/devel/synnefo/../synnefo/db/fixtures'. - Installed 1 object(s) from 1 fixture(s) - -South needs some extra definitions to the model to preserve and migrate the -existing data, for example, if we add a field in a model, we should declare its -default value. If not, South will propably fail, after indicating the error: - -.. code-block:: console - - $ ./bin/python manage.py schemamigration db --auto - ? The field 'SynnefoUser.new_south_field_2' does not have a default specified, yet is NOT NULL. - ? Since you are adding or removing this field, you MUST specify a default - ? value to use for existing rows. Would you like to: - ? 1. Quit now, and add a default to the field in models.py - ? 2. Specify a one-off value to use for existing columns now - ? Please select a choice: 1 - -Data migrations -*************** - -To do data migration as well, for example rename a field, use the -``datamigration`` management command. - -In contrast with ``schemamigration``, to perform complex data migration, we -must write the script manually. The process is the following: - -1. Introduce the changes in the code and fixtures (initial data). -2. Execute: - - .. code-block:: console - - $ snf-manage datamigration <migration_name_here> - - For example: - - .. code-block:: console - - $ ./bin/python manage.py datamigration db rename_credit_wallet - Created 0003_rename_credit_wallet.py. - -3. Edit the generated script. It contains two methods, ``forwards`` and - ``backwards``. - - For database operations (column additions, alter tables etc), use the - South database API (http://south.aeracode.org/docs/databaseapi.html). - - To access the data, use the database reference (``orm``) provided as - parameter in ``forwards``, ``backwards`` method declarations in the - migration script. For example: - - .. code-block:: python - - class Migration(DataMigration): - - def forwards(self, orm): - orm.SynnefoUser.objects.all() - -4. To migrate the database to the latest version, run: - - .. code-block:: console - - $ snf-manage migrate db - - To see which migrations are applied: - - .. code-block:: console - - $ snf-manage migrate db --list - - db - (*) 0001_initial - (*) 0002_auto__add_field_synnefouser_new_south_test_field - (*) 0003_rename_credit_wallet - -.. seealso:: - More information and more thorough examples can be found in the South web site, - http://south.aeracode.org/ - -Test coverage -------------- - -.. warning:: This section may be out of date. - -In order to get code coverage reports you need to install django-test-coverage - -.. code-block:: console - - $ pip install django-test-coverage - -Then configure the test runner inside Django settings: - -.. code-block:: python - - TEST_RUNNER = 'django-test-coverage.runner.run_tests' - - -Internationalization --------------------- - -This section describes how to translate static strings in Django projects: - -0. From our project's base, we add directory locale - - .. code-block:: console - - $ mkdir locale - -then we add on the settings.py the language code e.g., - - .. code-block:: python - - LANGUAGES = ( - ('el', u'Greek'), - ('en', u'English'),) - -1. For each language we want to add, we run ``makemessages`` from the project's - base: - - .. code-block:: python - - $ ./bin/django-admin.py makemessages -l el -e html,txt,py - (./bin/django-admin.py makemessages -l el -e html,txt,py --ignore=lib/\*) - - This will add the Greek language, and we specify that :file:`*.html`, - :file:`*.txt` and :file:`*.py` files contain translatable strings - -2. We translate our strings: - - On :file:`.py` files, e.g., :file:`views.py`, first import ``gettext``: - - .. code-block:: python - - from django.utils.translation import gettext_lazy as _ - - Then every ``string`` to be translated becomes: ``_('string')`` - e.g.: - - .. code-block:: python - - help_text=_("letters and numbers only")) - 'title': _('Ubuntu 10.10 server 64bit'), - - On django templates (``html`` files), on the beggining of the file we add - ``{% load i18n %}`` then rewrite every string that needs to be translated, - as ``{% trans "string" %}``. For example: ``{% trans "Home" %}`` - -3. When all strings have been translated, run: - - .. code-block:: console - - $ django-admin.py makemessages -l el -e html,txt,py - - processing language ``el``. This creates (or updates) the :file:`po` file - for the Greek language. We run this command each time we add new strings to - be translated. After that, we can translate our strings in the :file:`po` - file (:file:`locale/el/LC_MESSAGES/django.po`) - -4. When the :file:`po` file is ready, run - - .. code-block:: console - - $ ./bin/django-admin.py compilemessages - - This compiles the ``po`` files to ``mo``. Our strings will appear translated - once we change the language (e.g., from a dropdown menu in the page) - -.. seealso:: - http://docs.djangoproject.com/en/dev/topics/i18n/internationalization/ - - -Building source packages ------------------------- - -.. warning:: This section may be out of date. - -To create a python package from the Synnefo source code run - -.. code-block:: bash - - $ cd snf-cyclades-app - $ python setup.py sdist - -this command will create a ``tar.gz`` python source package inside ``dist`` directory. - - -Building documentation ----------------------- - -Make sure you have ``sphinx`` installed. - -.. code-block:: bash - - $ cd snf-cyclades-app/docs - $ make html - -.. note:: - - The theme define in the Sphinx configuration file ``conf.py`` is ``nature``, - not available in the version of Sphinx shipped with Debian Squeeze. Replace - it with ``default`` to build with a Squeeze-provided Sphinx. - -html files are generated in the ``snf-cyclades-app/docs/_build/html`` directory. - - -Continuous integration with Jenkins ------------------------------------ -.. warning:: This section may be out of date. - -Preparing a GIT mirror -********************** - -Jenkins cannot currently work with Git over encrypted HTTP. To solve this -problem we currently mirror the central Git repository locally on the jenkins -installation machine. To setup such a mirror do the following: - -edit .netrc:: - - machine code.grnet.gr - login accountname - password accountpasswd - -Create the mirror:: - - git clone --mirror https://code.grnet.gr/git/synnefo synnefo - -Setup cron to pull from the mirror periodically. Ideally, Git mirror updates -should run just before Jenkins jobs check the mirror for changes:: - - 4,14,24,34,44,54 * * * * cd /path/to/mirror && git fetch && git remote prune origin - -Jenkins setup -************* - -The following instructions will setup Jenkins to run synnefo tests with the -SQLite database. To run the tests on MySQL and/or Postgres, step 5 must be -replicated. Also, the correct configuration file must be copied (line 6 of the -build script). - -1. Install and start Jenkins. On Debian Squeeze: - - wget -q -O - http://pkg.jenkins-ci.org/debian/jenkins-ci.org.key | apt-key add - - echo "deb http://pkg.jenkins-ci.org/debian binary/" >>/etc/apt/sources.list - echo "deb http://ppa.launchpad.net/chris-lea/zeromq/ubuntu lucid main" >> /etc/apt/sources.list - sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys C7917B12 - sudo apt-get update - sudo apt-get install jenkins - - Also install the following packages: - - apt-get install python-virtualenv libcurl3-gnutls libcurl3-gnutls-dev - uuid-dev libmysqlclient-dev libpq-dev libsqlite-dev - python-dev libzmq-dev - -2. After Jenkins starts, go to - - http://$HOST:8080/pluginManager/ - - and install the following plug-ins at - - -Jenkins Cobertura Plugin - -Jenkins Email Extension Plugin - -Jenkins GIT plugin - -Jenkins SLOCCount Plug-in - -Hudson/Jenkins Violations plugin - -3. Configure the Jenkins user's Git details: - su jenkins - git config --global user.email "buildbot@lists.grnet.gr" - git config --global user.name "Buildbot" - -4. Make sure that all system-level dependencies specified in README.develop - are correctly installed - -5. Create a new "free-style software" job and set the following values:: - - Project name: synnefo - Source Code Management: Git - URL of repository: Jenkins Git does not support HTTPS for checking out directly - from the repository. The temporary solution is to checkout - with a cron script in a directory and set the checkout path - in this field - Branches to build: master and perhaps others - Git->Advanced->Local subdirectory for repo (optional): synnefo - Git->Advanced->Prune remote branches before build: check - Repository browser: redmineweb, - URL: https://code.grnet.gr/projects/synnefo/repository/ - Build Triggers->Poll SCM: check - Schedule: # every five minutes - 0,5,10,15,20,25,30,35,40,45,50,55 * * * * - - Build -> Add build step-> Execute shell - - Command:: - - #!/bin/bash -ex - cd synnefo - mkdir -p reports - /usr/bin/sloccount --duplicates --wide --details api util ui logic auth > reports/sloccount.sc - cp conf/ci/manage.py . - if [ ! -e requirements.pip ]; then cp conf/ci/pip-1.2.conf requirements.pip; fi - cat settings.py.dist conf/ci/settings.py.sqlite > settings.py - python manage.py update_ve - python manage.py hudson api db logic - - Post-build Actions->Publish JUnit test result report: check - Test report XMLs: synnefo/reports/TEST-*.xml - - Post-build Actions->Publish Cobertura Coverage Report: check - Cobertura xml report pattern: synnefo/reports/coverage.xml - - Post-build Actions->Report Violations: check - pylint[XML filename pattern]: synnefo/reports/pylint.report - - Post-build Actions->Publish SLOCCount analysis results - SLOCCount reports: synnefo/reports/sloccount.sc - (also, remember to install sloccount at /usr/bin) - -.. seealso:: - http://sites.google.com/site/kmmbvnr/home/django-hudson-tutorial diff --git a/docs/quota-api-guide.rst b/docs/quota-api-guide.rst index e259a55de0a68e417ffa193ecd6732ce7a2bcb39..c0b40ad6628bb4285962f8dd412caeee956c184b 100644 --- a/docs/quota-api-guide.rst +++ b/docs/quota-api-guide.rst @@ -49,7 +49,8 @@ Quotas The system specifies user quotas for each available resource. Resources can be allocated from various sources. By default, users get resources -from a single source, called ``system``. For each combination of user, +from their user-specific `system projects', which are identified by the same +uuid as the users. For each combination of user, source, and resource, the quota system keeps track of the maximum allowed value (limit) and the current actual usage. The former is controlled by the policy defined in Astakos; the latter is updated by the services that @@ -69,10 +70,16 @@ X-Auth-Token User authentication token A user can query their resources with this call. It returns in a nested dictionary structure, for each source and resource, three indicators. -``limit`` and ``usage`` are as explained above. ``pending`` is related to the -commissioning system explained below. Roughly, if ``pending`` is non zero, -this indicates that some resource allocation process has started but not -finished properly. +``limit`` and ``usage`` are as explained above. ``pending`` is related to +the commissioning system explained below. Roughly, if ``pending`` is non +zero, this indicates that some resource allocation process has started but +not finished properly. The project-level indicators ``project_limit``, +``project_usage`` and ``project_pending`` are included in the resource +dictionaries, too. The project quota must be taken into account in order to +compute the `effective` quota limit:: + + taken_by_others = project_usage - usage + effective_limit = min(limit, project_limit - taken_by_others) **Response Codes**: @@ -89,29 +96,41 @@ Status Description .. code-block:: javascript { - "system": { + "system_project_id": { "cyclades.ram": { "usage": 536870912, "limit": 1073741824, - "pending": 0 + "pending": 0, + "project_usage": 536870912, + "project_limit": 1073741824, + "project_pending": 0 }, "cyclades.vm": { "usage": 2, "limit": 2, - "pending": 0 + "pending": 0, + "project_usage": 2, + "project_limit: 2, + "project_pending": 0 } }, "project:1": { "cyclades.ram": { "usage": 2147483648, "limit": 2147483648, - "pending": 0 + "pending": 0, + "project_usage": 4147483648, + "project_limit": 14147483648, + "project_pending": 0 }, "cyclades.vm": { "usage": 2, "limit": 5, - "pending": 1 + "pending": 1, + "project_usage": 4, + "project_limit": 10, + "project_pending": 1 } } } @@ -127,9 +146,9 @@ Request Header Name Value X-Auth-Token Service authentication token ==================== ============================ -A service can query the quotas for all resources related to it. By default, -it returns the quotas for all users, in the format explained above, indexed -by the user identifier (UUID). +A service can query the user quotas for all resources related to it. By +default, it returns the quotas for all users, in the format explained above, +indexed by the user identifier (UUID). Use the GET parameter ``?user=<uuid>`` to query for a single user. @@ -150,33 +169,113 @@ Status Description { "1a6165d0-5020-4b6d-a4ad-83476632a584": { - "system": { + "system_project_id": { "cyclades.ram": { "usage": 536870912, "limit": 1073741824, - "pending": 0 + "pending": 0, + "project_usage": 536870912, + "project_limit": 1073741824, + "project_pending": 0 }, "cyclades.vm": { "usage": 2, "limit": 2, - "pending": 0 + "pending": 0, + "project_usage": 2, + "project_limit: 2, + "project_pending": 0 } }, "project:1": { "cyclades.ram": { "usage": 2147483648, "limit": 2147483648, - "pending": 0 + "pending": 0, + "project_usage": 4147483648, + "project_limit": 14147483648, + "project_pending": 0 }, "cyclades.vm": { "usage": 2, "limit": 5, - "pending": 1 + "pending": 1, + "project_usage": 4, + "project_limit": 10, + "project_pending": 1 } } } } +**GET** /account/v1.0/service_project_quotas + +==================== ============================ +Request Header Name Value +==================== ============================ +X-Auth-Token Service authentication token +==================== ============================ + +A service can also query the project quotas for all resources related to it. +By default, it returns the quotas for all projects, in the format explained +above, indexed by the project identifier (UUID). + +Use the GET parameter ``?project=<uuid>`` to query for a single project. + + +**Response Codes**: + +====== ============================ +Status Description +====== ============================ +200 Success +401 Unauthorized (Missing token) +500 Internal Server Error +====== ============================ + +**Example Successful Response**: + +.. code-block:: javascript + + { + "system_project_id": { + "cyclades.ram": { + "project_usage": 536870912, + "project_limit": 1073741824, + "project_pending": 0 + }, + "cyclades.vm": { + "project_usage": 2, + "project_limit: 2, + "project_pending": 0 + } + }, + "system_project2_id": { + "cyclades.ram": { + "project_usage": 0, + "project_limit": 1073741824, + "project_pending": 0 + }, + "cyclades.vm": { + "project_usage": 0, + "project_limit: 2, + "project_pending": 0 + } + }, + "project:1": { + "cyclades.ram": { + "project_usage": 4147483648, + "project_limit": 14147483648, + "project_pending": 0 + }, + "cyclades.vm": { + "project_usage": 4, + "project_limit": 10, + "project_pending": 1 + } + } + } + Commissions ----------- @@ -203,9 +302,13 @@ Request Header Name Value X-Auth-Token Service authentication token ==================== ============================ -A service issues a commission by providing a list of *provisions*, i.e. -the intended allocation for a particular user (in general, ``holder``), -``source``, and ``resource`` combination. +A service issues a commission by providing a list of *provisions*, i.e. the +intended allocation for a particular user and project (in general, +``holder``), ``source``, and ``resource`` combination. Users must be +specified with ``user:<uuid>`` and projects with ``project:<uuid>``. When +charging a user/project pair for a given resource, the intended use is to +also charge the project separately (by including a provision with the +project as holder and ``null`` as source), as in the example below. The request body consists of a JSON dict (as in the example below), which apart from the provisions list can also contain the following optional @@ -225,14 +328,26 @@ fields: "name": "an optional description", "provisions": [ { - "holder": "c02f315b-7d84-45bc-a383-552a3f97d2ad", - "source": "system", + "holder": "user:c02f315b-7d84-45bc-a383-552a3f97d2ad", + "source": "project:c02f315b-7d84-45bc-a383-552a3f97d2ad", + "resource": "cyclades.vm", + "quantity": 1 + }, + { + "holder": "project:c02f315b-7d84-45bc-a383-552a3f97d2ad", + "source": null, "resource": "cyclades.vm", "quantity": 1 }, { - "holder": "c02f315b-7d84-45bc-a383-552a3f97d2ad", - "source": "system", + "holder": "user:c02f315b-7d84-45bc-a383-552a3f97d2ad", + "source": "project:c02f315b-7d84-45bc-a383-552a3f97d2ad", + "resource": "cyclades.ram", + "quantity": 536870912 + }, + { + "holder": "project:c02f315b-7d84-45bc-a383-552a3f97d2ad", + "source": null, "resource": "cyclades.ram", "quantity": 536870912 } @@ -279,8 +394,8 @@ also included. "code": 413, "data": { "provision": { - "holder": "c02f315b-7d84-45bc-a383-552a3f97d2ad", - "source": "system", + "holder": "user:c02f315b-7d84-45bc-a383-552a3f97d2ad", + "source": "project:c02f315b-7d84-45bc-a383-552a3f97d2ad", "resource": "cyclades.vm", "quantity": 1 }, @@ -357,14 +472,26 @@ Status Description "name": "an optional description", "provisions": [ { - "holder": "c02f315b-7d84-45bc-a383-552a3f97d2ad", - "source": "system", + "holder": "user:c02f315b-7d84-45bc-a383-552a3f97d2ad", + "source": "project:c02f315b-7d84-45bc-a383-552a3f97d2ad", "resource": "cyclades.vm", "quantity": 1 }, { - "holder": "c02f315b-7d84-45bc-a383-552a3f97d2ad", - "source": "system", + "holder": "project:c02f315b-7d84-45bc-a383-552a3f97d2ad", + "source": null, + "resource": "cyclades.vm", + "quantity": 1 + }, + { + "holder": "user:c02f315b-7d84-45bc-a383-552a3f97d2ad", + "source": "project:c02f315b-7d84-45bc-a383-552a3f97d2ad", + "resource": "cyclades.ram", + "quantity": 536870912 + }, + { + "holder": "project:c02f315b-7d84-45bc-a383-552a3f97d2ad", + "source": null, "resource": "cyclades.ram", "quantity": 536870912 } diff --git a/docs/snf-deploy.rst b/docs/snf-deploy.rst index c6ad69ee424e1e841112bce1cbec4863d315ebd6..03e59abf1c02cd19fecb5cea6b1324a0be358eb3 100644 --- a/docs/snf-deploy.rst +++ b/docs/snf-deploy.rst @@ -7,7 +7,7 @@ The `snf-deploy` tool allows you to automatically deploy Synnefo. You can use `snf-deploy` to deploy Synnefo, in two ways: 1. Create a virtual cluster on your local machine and then deploy on that cluster. -2. Deploy on a pre-existent cluster of physical nodes running Debian Squeeze. +2. Deploy on a pre-existent cluster of physical nodes running Debian Wheezy. Currently, `snf-deploy` is mostly useful for testing/demo installations and is not recommended for production environment Synnefo deployments. If you want to @@ -25,10 +25,9 @@ while reading the Admin guides to set up a production environment that will scale up and use all available features (e.g. RADOS, Archipelago, etc). `snf-deploy` is a debian package that should be installed locally and allows -you to install Synnefo on remote nodes (if you go for (2)), or spawn a cluster -of VMs on your local machine using KVM and then install Synnefo on this cluster -(if you go for (1)). To this end, here we will break down our description into -three sections: +you to install Synnefo locally, or on remote nodes, or spawn a cluster of VMs +on your local machine using KVM and then install Synnefo on this cluster. To +this end, here we will break down our description into three sections: a. :ref:`snf-deploy configuration <conf>` b. :ref:`Creating a virtual cluster <vcluster>` (needed for (1)) @@ -41,51 +40,106 @@ If you go for (1) you will need to walk through all the sections. If you go for Before getting any further we should mention the roles that `snf-deploy` refers to. The Synnefo roles are described in detail :ref:`here -<physical-node-roles>`. Note that multiple roles can co-exist in the same node +<physical-node-roles>`. Those nodes consist of certain components. +Note that multiple roles can co-exist in the same node (virtual or physical). -Currently, `snf-deploy` recognizes the following combined roles: +Currently, `snf-deploy` defines the following roles under each setup: + +* ns: bind server (DNS) +* db: postgresql server (database) +* mq: rabbitmq server (message queue) +* nfs: nfs server +* astakos: identity service +* pithos: storage service +* cyclades: compute service +* cms: cms service +* stats: stats service +* clusters: the ganeti clusters + +For each cluster we have + +* vmc: VM container node +* master: master node + + +The previous roles are combinations of the following software components: + +* HW: IP and internet access +* SSH: ssh keys and config +* DDNS: ddns keys and ddns client config +* NS: nameserver with ddns config +* DNS: resolver config +* APT: apt sources config +* DB: database server with postgresql +* MQ: message queue server with rabbitmq +* NFS: nfs server +* Mount: nfs mount point +* Apache: web server with Apache +* Gunicorn: gunicorn server +* Common: synnefo common +* WEB: synnefo webclient +* Astakos: astakos webapp +* Pithos: pithos webapp +* Cyclades: cyclades webapp +* CMS: cms webapp +* VNC: vnc authentication proxy +* Collectd: collectd config +* Stats: stats webapp +* Kamaki: kamaki client +* Burnin: qa software +* Ganeti: ganeti node +* Master: ganeti master node +* Image: synnefo image os provider +* Network: synnefo networking scripts +* GTools: synnefo tools for ganeti +* GanetiCollectd: collectd config for ganeti nodes +* PithosBackend: the pithos backend +* Archip: The archipelago core +* ArchipGaneti: The tools needed by ganeti for archipelago + +Each component defines the following things: + +* commands to execute on other components before setup +* commands to check prereqs +* commands to prepare installation +* list of packages to install +* specific configuration files (templates) +* restart/reload commands +* initialization commands +* test commands +* commands to execute on other components after setup + +All a components needs is the context that it gets installed to and the +snf-deploy configuration environment (available after parsing conf files). +The context is basically the target node, role, cluster (if any) and +setup. -* **accounts** = **WEBSERVER** + **ASTAKOS** -* **pithos** = **WEBSERVER** + **PITHOS** -* **cyclades** = **WEBSERVER** + **CYCLADES** -* **db** = **ASTAKOS_DB** + **PITHOS_DB** + **CYCLADES_DB** - -the following independent roles: - -* **qh** = **QHOLDER** -* **cms** = **CMS** -* **mq** = **MQ** -* **ns** = **NS** -* **client** = **CLIENT** -* **router**: The node to do any routing and NAT needed - -The above define the roles relative to the Synnefo components. However, in -order to have instances up-and-running, at least one backend must be associated -with Cyclades. Backends are Ganeti clusters, each with multiple **GANETI_NODE** -s. Please note that these nodes may be the same as the ones used for the -previous roles. To this end, `snf-deploy` also recognizes: +.. _conf: -* **cluster_nodes** = **G_BACKEND** = All available nodes of a specific backend -* **master_node** = **GANETI_MASTER** +Configuration (a) +================= -Finally, it recognizes the group role: +All configuration of `snf-deploy` happens by editting the following simple +ConfigParser files under ``/etc/snf-deploy``. -* **existing_nodes** = **SYNNEFO** + (N x **G_BACKEND**) +``setups.conf`` +--------------- -In the future, `snf-deploy` will recognize all the independent roles of a scale -out deployment as stated in the :ref:`scale up section <scale-up>`. When that's -done, it won't need to introduce its own roles (stated here with lowercase) but -rather use the scale out ones (stated with uppercase on the admin guide). +This file includes all coarse grain info for our available setups. +We assing each of the roles described in the :ref:`introduction +<snf-deploy>` to specific targets. The targets can be either nodes +defined at ``nodes.conf`` or clusters defined at ``ganeti.conf``. Note +that we refer to targets with their ID (node1, node2, ganeti1, etc). -.. _conf: - -Configuration (a) -================= - -All configuration of `snf-deploy` happens by editting the following files under -``/etc/snf-deploy``: +Each section refers to a generic setup (synnefo, qa, etc) or a specific +ganeti cluster (ganeti1, ganeti2, etc.) Each section includes the +corresponding role mappings. For example if the nameserver should be +installed in node1, the NFS on node2, etc. Each generic setup has also +the cluster meta-role. For example synnefo section can have clusters +ganeti1, ganeti2. Each of them has its own vmcs and master roles (which +map to nodes found in nodes.conf). ``nodes.conf`` -------------- @@ -94,11 +148,9 @@ This file reflects the hardware infrastucture on which Synnefo is going to be deployed and is the first to be set before running `snf-deploy`. Defines the nodes' hostnames and their IPs. Currently `snf-deploy` expects all -nodes to reside in the same network subnet and domain, and share the same -gateway and nameserver. Since Synnefo requires FQDNs to operate, a nameserver -is going to be automatically setup in the cluster by `snf-deploy`. Thus, the -nameserver's IP should appear among the defined node IPs. From now on, we will -refer to the nodes with their hostnames. This implies their FQDN and their IP. +nodes to reside under the same domain. Since Synnefo requires FQDNs to operate, +a nameserver is going to be automatically setup in the cluster by `snf-deploy` +and all nodes with use this node for resolver. Also, defines the nodes' authentication credentials (username, password). Furthermore, whether nodes have an extra disk (used for LVM/DRBD storage in @@ -116,6 +168,10 @@ As we will see in the next sections, one should first set up this file and then tell `snf-deploy` whether the nodes on this file should be created, or treated as pre-existing. +In case you deploy all-in-one you can install `snf-deploy` package in the +target node and use `--autoconf` option. By that you must change only +the passwords section and everything else will be automatically configured. + An example ``nodes.conf`` file looks like this: FIXME: example file here @@ -126,11 +182,6 @@ FIXME: example file here This file reflects the way Synnefo will be deployed on the nodes defined at ``nodes.conf``. -The important section here is the roles. In this file we assing each of the -roles described in the :ref:`introduction <snf-deploy>` to a specific node. The -node is one of the nodes defined at ``nodes.conf``. Note that we refer to nodes -with their short hostnames. - Here we also define all credentials related to users needed by the various Synnefo services (database, RAPI, RabbitMQ) and the credentials of a test end-user (`snf-deploy` simulates a user signing up). @@ -153,10 +204,8 @@ This file reflects the way Ganeti clusters will be deployed on the nodes defined at ``nodes.conf``. Here we include all info with regard to Ganeti backends. That is: the master -node, its floating IP, the volume group name (in case of LVM support) and the -VMs' public network associated to it. Please note that currently Synnefo -expects different public networks per backend but still can support multiple -public networks per backend. +node, its floating IP, the rest of the cluster nodes (if any) the volume group +name (in case of LVM support) and the VMs' public network associated to it. FIXME: example file here @@ -177,13 +226,13 @@ FIXME: example file here ``vcluster.conf`` ----------------- -This file defines options that are relevant to the virtual cluster creationi, if +This file defines options that are relevant to the virtual cluster creation, if one chooses to create one. -There is an option to define the URL of the Image that will be used as the host -OS for the VMs of the virtual cluster. Also, options for defining an LVM space -or a plain file to be used as a second disk. Finally, networking options to -define where to bridge the virtual cluster. +There is an option to define the disk size used for virtual cluster base +image along with networking options to define where to bridge the +virtual cluster and the network that the virtual hosts will reside. +Please note that the nodes' IPs are defined in ``nodes.conf``. .. _vcluster: @@ -200,59 +249,46 @@ will be deployed in the :ref:`next section <inst>`. If you want to deploy Synnefo on existing physical nodes, you should skip this section. The first thing you need to deploy a virtual cluster, is a Debian Base image, -which will be used to spawn the VMs. We already provide an 8GB Debian Squeeze -Base image with preinstalled keys and network-manager hostname hooks. This -resides on our production Pithos service. Please see the corresponding -``squeeze_image_url`` variable in ``vcluster.conf``. The image can be fetched -by running: +which will be used to spawn the VMs. To create one using debootstrap +use: .. code-block:: console - snf-deploy vcluster image + snf-deploy image -This will download the image from the URL defined at ``squeeez_image_url`` -(Pithos by default) and save it locally under ``/var/lib/snf-deploy/images``. +It will create one raw image file under `/var/lib/snf-deploy/vcluster` +and another one which will be used as an extra disk for LVM. Note that +for fast VM launching we use the snapshot feature of qemu and thus all +VMs will use the same base image to spawn and all changes on the +filesystem will not be saved. -TODO: mention related options: --img-dir, --extra-disk, --lvg, --os - -Once you have the image, then you need to setup the local machine's networking -appropriately. You can do this by running: +The virtual cluster can be created by running: .. code-block:: console - snf-deploy vcluster network + snf-deploy vcluster --setup vc --vnc + -This will add a bridge (defined with the ``bridge`` option inside +Afterwards it will add a bridge (defined with the ``bridge`` option inside ``vcluster.conf``), iptables to allow traffic from/to the cluster, and enable -forwarding and NAT for the selected network subnet (defined inside -``nodes.conf`` in the ``subnet`` option). +forwarding and NAT for the selected network subnet. To complete the preparation, you need a DHCP server that will provide the -selected hostnames and IPs to the cluster (defined under ``[ips]`` in -``nodes.conf``). To do so, run: +selected hostnames and IPs to the cluster (defined in ``nodes.conf``). -.. code-block:: console - - snf-deploy vcluster dhcp +It will launch a dnsmasq instance, acting only as DHCP server and listening +only on the cluster's bridge. -This will launch a dnsmasq instance, acting only as DHCP server and listening -only on the cluster's bridge. Every time you make changes inside ``nodes.conf`` -you should re-create the dnsmasq related files (under ``/etc/snf-deploy``) by -passing --save-config option. +Finally it will launch all the needed KVM virtual machines, snapshotting the +image we created before. Their taps will be connected with the already created +bridge and their primary interface will get the given address. -After running all the above preparation tasks we can finally create the cluster -defined in ``nodes.conf`` by running: +Now that we have the nodes ready, we can move on and deploy Synnefo on them +by running: .. code-block:: console - snf-deploy vcluster create - -This will launch all the needed KVM virtual machines, snapshotting the image we -fetched before. Their taps will be connected with the already created bridge -and their primary interface will get the given address. - -Now that we have the nodes ready, we can move on and deploy Synnefo on them. - + snf-deploy synnefo --setup vc .. _inst: @@ -270,10 +306,9 @@ will reside in which node. Node Requirements ----------------- - - OS: Debian Squeeze - - authentication: `root` with same password for all nodes + - OS: Debian Wheezy + - authentication: `root` user with corresponding for each node password - primary network interface: `eth0` - - primary IP in the same IPv4 subnet and network domain - spare network interfaces: `eth1`, `eth2` (or vlans on `eth0`) In case you have created a virtual cluster as described in the :ref:`section @@ -281,174 +316,110 @@ In case you have created a virtual cluster as described in the :ref:`section physical cluster, you need to set them up manually by yourself, before proceeding with the Synnefo installation. -Preparing the Synnefo deployment --------------------------------- - -The following actions are mandatory and must run before the actual deployment. -In the following we refer to the sub commands of ``snf-deploy prepare`` and -what they actually do. - -Synnefo expects FQDNs and therefore a nameserver (BIND) should be setup in a -node inside the cluster. All nodes along with your local machine should use -this nameserver and search in the corresponding network domain. To this end, -add to your local ``resolv.conf`` (please change the default values with the -ones of your custom configuration): - -.. code-block:: console - - search <your_domain> synnefo.deploy.local - nameserver 192.168.0.1 - -WARNING: In case you are running the installation on physical nodes please -ensure that they have the same `resolv.conf` and it does not change during -and after installation (because of NetworkManager hooks or something..) - -To actually setup the nameserver in the node specified as ``ns`` in -``synnefo.conf`` run: - -.. code-block:: console - - snf-deploy prepare ns -To do some node tweaking and install correct `id_rsa/dsa` keys and `authorized_keys` -needed for password-less intra-node communication run: - -.. code-block:: console - - snf-deploy prepare hosts - -At this point you should have a cluster with FQDNs and reverse DNS lookups -ready for the Synnefo deployment. To sum up, we mention all the node -requirements for a successful Synnefo installation, before proceeding. +Synnefo deployment +------------------ -To check the network configuration (FQDNs, connectivity): +To install the Synnefo stack in the same node (running snf-deploy) run: .. code-block:: console - snf-deploy prepare check - -WARNING: In case ping fails check ``/etc/nsswitch.conf`` hosts entry and put dns -after files!!! - -To setup the apt repository and update each nodes' package index files: + snf-deloy synnefo --autoconf -.. code-block:: console - - snf-deploy prepare apt +This does not require any tweak of the configuration files. -Finally Synnefo needs a shared file system, so we need to setup the NFS server -on node ``pithos`` defined in ``synnefo.conf``: +To install the Synnefo stack on an existing setup/infra (e.g. defined on synnefo +section in `setups.conf`) run: .. code-block:: console - snf-deploy prepare nfs - -If everything is setup correctly and all prerequisites are met, we can start -the Synnefo deployment. - -Synnefo deployment ------------------- - -To install the Synnefo stack on the existing cluster run: - -.. code-block:: console + snf-deploy synnefo --setup synnefo - snf-deploy synnefo -vvv +Please note that this requires valid configuration files with regard to +existing nodes (IP, hostnames, passwords, etc). -This might take a while. +The whole deployment might take a while. If this finishes without errors, check for successful installation by visiting from your local machine (make sure you have already setup your local ``resolv.conf`` to point at the cluster's DNS): -| https://accounts.synnefo.deploy.local/im/ +| https://astakos.synnefo.live/astakos/ui/ and login with: -| username: dimara@grnet.gr password: lala +| username: user@synnefo.org password: 12345 or the ``user_name`` and ``user_passwd`` defined in your ``synnefo.conf``. Take a small tour checking out Pithos and the rest of the Web UI. You can -upload a sample file on Pithos to see that Pithos is working. Do not try to -create a VM yet, since we have not yet added a Ganeti backend. - -If everything seems to work, we go ahead to the last step which is adding a -Ganeti backend. +upload a sample file on Pithos to see that Pithos is working. To test +everything went as expected, visit from your local machine: -Adding a Ganeti Backend ------------------------ +.. code-block:: console -Assuming that everything works as expected, you must have Astakos, Pithos, CMS, -DB and RabbitMQ up and running. Cyclades should work too, but partially. That's -because no backend is registered yet. Let's setup one. Currently, Synnefo -supports only Ganeti clusters as valid backends. They have to be created -independently with `snf-deploy` and once they are up and running, we register -them to Cyclades. From version 0.12, Synnefo supports multiple Ganeti backends. -`snf-deploy` defines them in ``ganeti.conf``. + https://cyclades.synnefo.live/cyclades/ui/ -After setting up ``ganeti.conf``, run: +and try to create a VM. Also create a Private Network and try to connect it. If +everything works, you have setup Synnefo successfully. Enjoy! -.. code-block:: console - snf-deploy backend create --backend-name ganeti1 -vvv +Adding another Ganeti Backend +----------------------------- -where ``ganeti1`` should have previously been defined as a section in -``ganeti.conf``. This will create the ``ganeti1`` backend on the corresponding -nodes (``cluster_nodes``, ``master_node``) defined in the ``ganeti1`` section -of the ``ganeti.conf`` file. If you are an experienced user and want to deploy -more than one Ganeti backend you should create multiple sections in -``ganeti.conf`` and re-run the above command with the corresponding backend -names. +From version 0.12, Synnefo supports multiple Ganeti backends. +`snf-deploy` defines them in ``ganeti.conf``. -After creating and adding the Ganeti backend, we need to setup the backend -networking. To do so, we run: +After adding another section in ``ganeti.conf`` with synnefo setting +set True, run: .. code-block:: console - snf-deploy backend network --backend-name ganeti1 + snf-deploy setup --setup synnefo --cluster ganeti2 -vvv -And finally, we need to setup the backend storage: -.. code-block:: console +snf-deploy for Ganeti +===================== + +`snf-deploy` can be used to deploy a Ganeti cluster on pre-existing nodes +by issuing: - snf-deploy backend storage --backend-name ganeti1 +.. code-block:: console -This command will first check the ``extra_disk`` in ``nodes.conf`` and try to -find it on the nodes of the cluster. If the nodes indeed have that disk, -`snf-deploy` will create a PV and the corresponding VG and will enable LVM and -DRBD storage in the Ganeti cluster. + snf-deploy ganeti --setup ganeti -vvv -If the option is blank or `snf-deploy` can't find the disk on the nodes, LVM -and DRBD will be disabled and only Ganeti's ``file`` disk template will be -enabled. -To test everything went as expected, visit from your local machine: +It will install a nameserver, nfs server and a Ganeti cluster. To +install a development node along with a Ganeti cluster ready for QA, run: .. code-block:: console - https://cyclades.synnefo.deploy.local/ui/ - -and try to create a VM. Also create a Private Network and try to connect it. If -everything works, you have setup Synnefo successfully. Enjoy! + snf-deploy ganeti-qa --setup qa -vvv snf-deploy as a DevTool ======================= For developers, a single node setup is highly recommended and `snf-deploy` is a -very helpful tool. `snf-deploy` also supports updating packages that are -locally generated. For this to work please add all \*.deb files in packages -directory (see ``deploy.conf``) and set the ``use_local_packages`` option to -``True``. Then run: +very helpful tool. `snf-deploy` also setting up components using packages that +are locally generated. For this to work please add all related \*.deb files in +packages directory (see ``deploy.conf``) and set the ``use_local_packages`` +option to ``True``. Then run: .. code-block:: console - snf-deploy synnefo update --use-local-packages - snf-deploy backend update --backend-name ganeti2 --use-local-packages + snf-deploy setup --setup SETUP --node nodeX \ + --role ROLE --cluster CLUSTER -For advanced users, `snf-deploy` gives the ability to run one or more times -independently some of the supported actions. To find out which are those, run: +to setup a specific role on a target node of a specific cluster and setup. + +For instance, to add another node to an existing ganeti backend run: .. code-block:: console - snf-deploy run --help + snf-deploy setup --node node5 --role vmc --cluster ganeti3 --setup synnefo + +`snf-deploy` keeps track of installed components per node in +``/var/lib/snf-deploy/snf_deploy_status``. If a deployment command +fails, the developer can make the required fix and then re-run the same +command; `snf-deploy` will not re-install components that have been +already setup and their status is ``ok``. diff --git a/docs/unify.rst b/docs/unify.rst new file mode 100644 index 0000000000000000000000000000000000000000..ca4a172c8b86f6a83f7731a897fbf44cd587d14a --- /dev/null +++ b/docs/unify.rst @@ -0,0 +1,110 @@ +.. _unify: + +Storage Unification (Objects/Images/Snapshots/Volumes) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +| Synnefo proposes a completely unified approach for cloud storage. +| It uses the Archipelago storage system to implement the above idea. + +In Synnefo: + +* Objects (as seen by the Object Storage Service and managed via the Swift API) +* Images (as seen by the Image Service and managed via the Glance API) +* Snapshots (as seen by the Image Service and managed via the Glance API) + +map to the `exact same` underlying virtual resource in the backend, which is an +Archipelago virtual resource. The only difference between them is a different +set of metadata that each service adds on the same underlying virtual resource. + +Specifically, the Object Storage service adds Swift-specific metadata, the +Image Service adds Glance-specific metadata and the Volume service adds +Cinder-specific metadata. This means that one can have a user uploaded file +which is an Object on the Object Storage service and by just changing metadata +can register it as an Image on the Image service. And by changing metadata +again, one can convert it to a Snapshot, or vice-versa. The underlying virtual +resource stays intact and Archipelago doesn't even learn about upper layer +metadata, meaning that no real data gets moved or copied around during +conversions and registrations, and there is only one gateway to upload, sync +and download data to and from the cloud and this is the Object Storage service. + + +Relation between Objects, Images and Snapshots +============================================== + +Objects, Images and Snapshots are the exact same things and map to the same, +single Archipelago virtual resource underneath. This Archipelago resource is a +read-only (RO) resource. + +An Object represents all kinds of files that users would like to upload on an +Object Storage service, e.g., documents, videos, music, etc. + +An Object that contains OS data (e.g., a raw disk dump), after getting uploaded +to the Object Storage service, it can be registered on the Image service, by +adding metadata to it, and then it becomes an Image, while still remaining an +Object for the Object Storage service. The metadata fall into two categories: + +* Generic metadata +* Customization metadata + +Generic metadata include name, creation-date, owner, size, id, checksum, etc. +and are mostly used for identification and presentation purposes by the Image +service. Customization metadata define the way this image will get customized +once its data finds their way on a bootable Volume (we describe how that +happens in the next section). Customization metadata configure: partitioning, +setting hostnames, setting passwords, resizing filesystem, injecting ssh keys, +etc. + +If a registered Object (an Image) contains only Generic metadata and no +Customization metadata, i.e. having the 'EXCLUDE_ALL_TASKS' metadata property +set, then it is a Snapshot. A Snapshot's data will find their way on a bootable +or non-bootable Volume and this Volume will get attached to a VM as is, without +any customization afterwards. + +So, one can convert a Snapshot to an Image by only changing its metadata and +vice-versa. Or convert it to a plain Object by just removing all metadata +(unregistering it from the Image service). + + +Volumes +======= + +Volumes (the actual disks that get attached to and accessed by VMs) are +respectively mapped to Archipelago virtual resources in the backend, the same +way Objects, Images and Snapshots do. The only difference is that Volumes map +to read-write (RW) Archipelago resources. + +Since Archipelago allows, among other things, thin cloning and snapshotting of +its virtual resources, and everything (Volumes/Objects/Images/Snapshots) is an +Archipelago resource in the backend, we can have workflows as the following: + +[Archipelago resource] --- clone --> [New Archipelago RW resource] +[Archipelago resource] --- snapshot --> [New Archipelago RO resource] + +which translate to: + +[Image] --- clone --> [Volume] --- disk customization --> [Customized Volume] +[Snapshot] --- clone --> [Volume] + +[Volume] --- snapshot --> [Snapshot] --- customization metadata addition --> [Image] +[Volume] --- snapshot --> [Snapshot] + + +Conclusion +========== + +In general we could say that Volumes are RW Archipelago resources exposed and +handled by the Volume service via the Cinder API and Images/Snapshots are RO +Archipelago resources exposed and handled by the Image service via the Glance +API. For Synnefo, Images and Snapshots are exact same things, with the only +difference that they have different customization metadata on the Image +service, where the first will get customized after they become Volumes, while +the latter will not get customized after they become Volumes. In the opposite +path, a Volume will always become a Snapshot first, and if added customization +metadata, will then become an Image. + +Objects are also RO Archipelago resources exposed and handled by the Object +Storage service via the Swift API. + +For a deeper dive in the internals of the above workflows please refer to the +:ref:`Snapshots <design/volume-snapshots>` and :ref:`Volumes <design/volumes>` +design documents. diff --git a/docs/upgrade/upgrade-0.16.rst b/docs/upgrade/upgrade-0.16.rst new file mode 100644 index 0000000000000000000000000000000000000000..eb64654b5ea734a83214b540d4bdb55dc83b7996 --- /dev/null +++ b/docs/upgrade/upgrade-0.16.rst @@ -0,0 +1,504 @@ +Upgrade to Synnefo v0.16 +^^^^^^^^^^^^^^^^^^^^^^^^ + +Introduction +============ + +Starting with version 0.16, we introduce Archipelago as the new storage backend +for the Pithos Service. Archipelago will act as a storage abstraction layer +between Pithos and NFS, RADOS or any other storage backend driver that +Archipelago supports. In order to use the Pithos Service you must install +Archipelago on the node that runs the Pithos and Cyclades workers. +Additionally, you must install Archipelago on the Ganeti nodes and upgrade +snf-image to version 0.16.2 since this is the first version that supports +Archipelago. + +Until now the Pithos mapfile was a simple file containing a list of hashes that +make up the stored file in a Pithos container. After this consolidation the +Pithos mapfile had to be converted to an Archipelago mapfile. An Archipelago +mapfile is an updated version of the Pithos mapfile, intended to supersede it. + +More info about the new mapfile you can find in Archipelago documentation. + + +Current state regarding ownerships and permissions +================================================== + +In Synnefo v0.15, all Synnefo components run as ``www-data:www-data``. Also, in +case Pithos is using the NFS backend store, the files in the shared directory +are owned by ``www-data:www-data`` with ``0640`` permissions. Finally, +Archipelago, if used, runs as ``root:root``. + +Synnefo v0.16 provides more flexibility regarding required users and +permissions. Synnefo introduces a dedicated system user and group: +``synnefo``. On the other hand, Archipelago v0.4 is able to run as an +arbitrary user/group (defaults to ``archipelago:archipelago``). + +As already mentioned, in Synnefo v0.16, Archipelago is becoming the +storage backend for Pithos, so we must guarantee that Pithos will have +the right permissions to communicate with Archipelago. For this reason +we run Archipelago with the ``synnefo`` group. + +Finally, in case NFS is used as a storage backend, we must also update +the permissions and ownerships of all directories and files on the +exported directory. Because this procedure may take a while in a production +setup with many TB of data, these upgrade notes provide a detailed procedure +in order to be able to perform the transition with minimum downtime. + +At the end of the day Synnefo (Cyclades, Pithos, Astakos, etc) will run +as ``synnefo:synnefo`` while Archipelago will run as +``archipelago:synnefo``. The NFS (if any) will be owned by +``archipelago:synnefo`` with 2660 permissions. + + +Upgrade Steps +============= + +The upgrade to v0.16 consists of the following steps: + +0. Upgrade snf-image on all Ganeti nodes + +1. Setup Archipelago on all nodes + +2. Ensure intermediate state + +3. Bring down services and backup databases + +4. Upgrade packages, migrate the databases and configure settings + +5. Inspect and adjust resource limits + +6. Tweak Gunicorn settings on Pithos and Cyclades node + +7. Bring up all services + +8. Finalize permissions + +9. Add unique names to disks of all Ganeti instances + + +.. warning:: + + It is strongly suggested that you keep separate database backups + for each service after the completion of each step. + +0. Upgrade snf-image on all Ganeti nodes +======================================== + +On all Ganeti VM-capable nodes install the latest snf-image package (v0.16.3). + +.. code-block:: console + + # apt-get install snf-image + + +1. Setup Archipelago on all nodes +================================== + +At this point, we will perform some intemediate migration steps in order to +perform the upgrade procedure with minimum downtime. To achieve this, we will +pass through an intermediate state where: + +* Pithos will run as ``www-data:synnefo``. +* Archipelago will run as ``archipelago:synnefo``. +* The NFS shared directory will be owned by ``www-data:synnefo`` with ``2660`` + permissions. + +To ensure seamless transition we do the following: + +* **Create system users and groups in advance** + + NFS expects the user and group ID of the owners of the exported directory + to be common across all nodes. So we need to guarantee that ID of ``archipelago`` + user/group and ``synnefo`` group will be the same to all nodes. + So we modify the ``archipelago`` user and group and create the ``synnefo`` + user (assuming that ids 200 and 300 are available everywhere), by running + the following commands to all nodes that have archipelago installed: + + .. code-block:: console + + # addgroup --system --gid 200 synnefo + # adduser --system --uid 200 --gid 200 --no-create-home \ + --gecos Synnefo synnefo + + # addgroup --system --gid 300 archipelago + # adduser --system --uid 300 --gid 300 --no-create-home \ + --gecos Archipelago archipelago + + Normally the ``snf-common`` and ``archipelago`` packages are responsible + for creating the required system users and groups. + +* **Upgrade/Install Archipelago** + + Up until now Archipelago was optional. So, your setup, either has no + Archipelago installation or has Archipelago v0.3.5 installed and + configured in all VM-capable nodes. Depending on your case refer to: + + * `Archipelago installation guide <https://www.synnefo.org/docs/archipelago/latest/install-guide.html>`_ + * `Archipelago upgrade notes <https://www.synnefo.org/docs/archipelago/latest/upgrades/upgrade-0.4.html>`_ + + Archipelago does not start automatically after installation. Do not start it + manually until it is configured properly. + +* **Adjust Pithos umask setting** + + On the Pithos node, edit the file + ``/etc/synnefo/20-snf-pithos-app-settings.conf`` and uncomment or add the + ``PITHOS_BACKEND_BLOCK_UMASK`` setting and set it to value ``0o007``. + + Then perform a gunicorn restart on both nodes: + + .. code-block:: console + + # service gunicorn restart + + This way, all files and directories created by Pithos will be writable by the + group that Pithos is running (i.e. ``www-data``). + +* **Change Pithos data group permissions** + + Ensure that every file and folder under Pithos data directory has correct + permissions. + + .. code-block:: console + + # find /srv/pithos/data -type d -exec chmod g+rwxs '{}' \; + # find /srv/pithos/data -type f -exec chmod g+rw '{}' \; + + This way, we prepare NFS to be fully accessible either via + the user or the group. + +* **Change gunicorn group** + + On the Pithos node, edit the file ``/etc/gunicorn.d/synnefo`` and set + ``group`` to ``synnefo``. Then change the ownership of all + configuration and log files: + + .. code-block:: console + + # chgrp -R synnefo /etc/synnefo + # chgrp -R synnefo /var/log/synnefo + # /etc/init.d/gunicorn restart + + This way, Pithos is able to access NFS via gunicorn user + (``www-data``). We prepare Pithos to be able to access the ``synnefo`` + group. + +* **Change Pithos data group owner** + + Make ``synnefo`` group the group owner of every file under the Pithos data + directory. + + .. code-block:: console + + # chgrp synnefo /srv/pithos/data + # find /srv/pithos/data -type d -exec chgrp synnefo '{}' \; + # find /srv/pithos/data -type f -exec chgrp synnefo '{}' \; + + From now on, every file or directory created under the Pithos data directory + will belong to the ``synnefo`` group because of the directory SET_GUID bit + that we set on a previous step. Plus the ``synnefo`` group will have + full read/write access because of the adjusted Pithos umask setting. + +* **Make archipelago run as synnefo group** + + Change the Archipelago configuration on all nodes, to run as + ``archipelago``:``synnefo``, since it no longer requires root + priviledges. For each Archipelago node: + + * Stop Archipelago + + .. code-block:: console + + # archipelago stop + + * Change the ``USER`` and ``GROUP`` configuration option to ``archipelago`` + and ``synnefo`` respectively. The configuration file is located under + ``/etc/archipelago/archipelago.conf`` + + * Change the ownership of Archipelago log files: + + .. code-block:: console + + # chown -R archipelago:synnefo /var/log/archipelago + + * Start Archipelago + + .. code-block:: console + + # archipelago start + + +2. Ensure intermediate state +============================ + +Please verify that Pithos runs as ``www-data:synnefo`` and any file +created in the exported directory will be owned by ``www-data:synnefo`` +with ``660`` permissions. Archipelago runs as ``archipelago:synnefo`` so it +can access NFS via the ``synnefo`` group. NFS (``blocks``, ``maps``, +``locks`` and all other subdirectories under ``/srv/pithos/data`` or +``/srv/archip``) will be owned by ``www-data:synnefo`` with 2770 +permissions. + + +3. Bring web services down, backup databases +============================================ + +1. All web services must be brought down so that the database maintains a + predictable and consistent state during the migration process:: + + $ service gunicorn stop + $ service snf-dispatcher stop + $ service snf-ganeti-eventd stop + +2. Backup databases for recovery to a pre-migration state. + +3. Keep the database servers running during the migration process. + + +4. Upgrade Synnefo and configure settings +========================================= + +4.1 Install the new versions of packages +---------------------------------------- + +:: + + astakos.host$ apt-get install \ + python-objpool \ + snf-common \ + python-astakosclient \ + snf-django-lib \ + snf-webproject \ + snf-branding \ + snf-astakos-app + + cyclades.host$ apt-get install \ + python-objpool \ + snf-common \ + python-astakosclient \ + snf-django-lib \ + snf-webproject \ + snf-branding \ + snf-pithos-backend \ + snf-cyclades-app + + pithos.host$ apt-get install \ + python-objpool \ + snf-common \ + python-astakosclient \ + snf-django-lib \ + snf-webproject \ + snf-branding \ + snf-pithos-backend \ + snf-pithos-app \ + snf-pithos-webclient + + ganeti.node$ apt-get install \ + python-objpool \ + snf-common \ + snf-cyclades-gtools \ + snf-pithos-backend \ + snf-network \ + snf-image + +.. note:: + + Make sure ``snf-webproject`` has the same version with snf-common + +.. note:: + + Installing the packages will cause services to start. Make sure you bring + them down again (at least ``gunicorn``, ``snf-dispatcher``) + +.. note:: + + If you are using qemu-kvm from wheezy-backports, note that qemu-kvm package + 2.1+dfsg-2~bpo70+2 has a bug that is triggered by snf-image. Check + `snf-image installation <https://www.synnefo.org/docs/synnefo/latest/install-guide-debian.html#installation>`_ for + a workaround. + + +4.2 Sync and migrate the database +--------------------------------- + +.. note:: + + If you are asked about stale content types during the migration process, + answer 'no' and let the migration finish. + +:: + + astakos-host$ snf-manage syncdb + astakos-host$ snf-manage migrate + + cyclades-host$ snf-manage syncdb + cyclades-host$ snf-manage migrate + + pithos-host$ pithos-migrate upgrade head + + +4.3 Configure snf-vncauthproxy +------------------------------ + +Synnefo 0.16 replaces the Java VNC client with an HTML5 Websocket client and +the Cyclades UI will always request secure Websocket connections. You should, +therefore, provide snf-vncauthproxy with SSL certificates signed by a trusted +CA. You can either copy them to `/var/lib/vncauthproxy/{cert,key}.pem` or +inform vncauthproxy about the location of the certificates (via the +`DAEMON_OPTS` setting in `/etc/default/vncauthproxy`). + +:: + + DAEMON_OPTS="--pid-file=$PIDFILE --cert-file=<path_to_cert> --key-file=<path_to_key>" + +Both files should be readable by the `vncauthproxy` user or group. + +.. note:: + + When installing snf-vncauthproxy on the same node as Cyclades and using the + default settings for snf-vncauthproxy, the certificates should be issued to + the FQDN of the Cyclades worker. Refer to the :ref:`admin guide + <admin-guide-vnc>`, for more information on how to setup vncauthproxy on a + different host / interface. + +For more information on how to setup snf-vncauthproxy check the +snf-vncauthproxy `documentation <https://www.synnefo.org/docs/snf-vncauthproxy/latest/index.html#usage-with-synnefo>`_ +and `upgrade notes <https://www.synnefo.org/docs/snf-vncauthproxy/latest/upgrade/upgrade-1.6.html>`_. + + + +5. Inspect and adjust resource limits +===================================== + +Synnefo 0.16 brings significant changes at the project mechanism. Projects +are now viewed as a source of finite resources, instead of a means to +accumulate quota. They are the single source of resources, and quota are now +managed at a project/member level. + +System-provided quota are now handled through special purpose +user-specific *system projects*, identified with the same UUID as the user. +These have been created during the database migration process. They are +included in the project listing with:: + + snf-manage project-list --system-projects + +All projects must specify quota limits for all registered resources. Default +values have been set for all resources, listed with:: + + astakos-host$ snf-manage resource-list + +Column `system_default` (previously known as `default_quota`) provides the +skeleton for the quota limits of user-specific system projects. Column +`project_default` is new and acts as skeleton for `applied` (non-system) +projects (i.e., for resources not specified in a project application). +Project defaults have been initialized during migration based on the system +default values: they have been set to `inf` if `system_default` is also `inf`, +otherwise set to zero. + +This default, affecting all future projects, can be modified with:: + + astakos-host$ snf-manage resource-modify <name> --project-default <value> + +Till now a project definition contained one quota limit per resource: the +maximum that a member can get from the project. A new limit is introduced: +the grand maximum a project can provide to its members. This new project +limit is initialized during migration as `max members * member limit` (if +`max members` is not set, the double of current active members is assumed). + +Existing projects can now be modified directly through the command line. In +order to change a project's resource limits, run:: + + astakos-host$ snf-manage project-modify <project_uuid> --limit <resource_name> <member_limit> <project_limit> + +With the new mechanism, when a new resource is allocated (e.g., a VM or a +Pithos container is created), it is also associated with a project besides +its owner. The migration process has associated existing resources with +their owner's system project. Note that users who had made use of projects to +increase their quota may end up overlimit on some resources of their system +projects and will need to *reassign* some of their reserved resources to +another project in order to overcome this restriction. + + +6. Tweak Gunicorn settings +========================== + +First we make Gunicorn run as ``synnefo:synnefo``, by setting the +``user`` and ``group`` option in Gunicorn configuration +file (``/etc/gunicorn.d/synnefo``). + +Also on the Pithos and Cyclades node you also have to set the following: + +* ``--config=/etc/synnefo/gunicorn-hooks/gunicorn-archipelago.py`` + + +.. warning:: + + If you have already installed Synnefo v0.16rc1 or v0.16rc2 you + should replace ``pithos.conf.py`` with ``gunicorn-archipelago.py`` located + under ``/etc/synnefo/gunicorn-hooks`` directory. Afterwards you + can freely delete ``pithos.conf.py`` conf file. + +After setting the user/group that Gunicorn will run as, we must also make +sure that configuration and log files are accessible: + +.. code-block:: console + + # chgrp -R synnefo /etc/synnefo/ + # chown -R synnefo:synnefo /var/log/synnefo/ + +On the Cyclades node, the ``snf-dispatcher`` must run as +``synnefo``:``synnefo``. In ``/etc/default/snf-dispatcher`` verify that +``SNF_USER`` and ``SNF_DSPTCH_OPTS`` settings are: + +.. code-block:: console + + SNF_USER="synnefo:synnefo" + SNF_DSPTCH_OPTS="" + +Finally, verify that snf-dispatcher can access its log file (e.g. +``/var/log/synnefo/synnefo.log``): + +.. code-block:: console + + # chown synnefo:synnefo /var/log/synnefo/dispatcher.log + + +7. Bring all services up +======================== + +After the upgrade is finished, we bring up all services: + +.. code-block:: console + + astakos.host # service gunicorn start + cyclades.host # service gunicorn start + + pithos.host # service gunicorn start + + cyclades.host # service snf-dispatcher start + +8. Finalize permissions +======================= + +At this point, and while the services are running, we will finalize the +permissions of existing directories and files in the NFS directory to match +the user/group that Archipelago is running: + +.. code-block:: console + + # chown -R archipelago:synnefo /srv/pithos/data + + +9. Add unique names to disks of all Ganeti instances +===================================================== + +Synnefo 0.16 introduces the Volume service which can handle multiple disks +per Ganeti instance. Synnefo assigns a unique name to each Ganeti disk and +refers to it by that unique name. After upgrading to v0.16, Synnefo must +assign names to all existing disks. This can be easily performed with a helper +script that is shipped with version 0.16: + +.. code-block:: console + + cyclades.host$ /usr/lib/synnefo/tools/add_unique_name_to_disks diff --git a/docs/weblogin-api-guide.rst b/docs/weblogin-api-guide.rst new file mode 100644 index 0000000000000000000000000000000000000000..09abf84485bc91354db552cfe2e6b1f655e4d412 --- /dev/null +++ b/docs/weblogin-api-guide.rst @@ -0,0 +1,45 @@ +Weblogin API +============ + +This is Weblogin API guide. + +Login +^^^^^ +This service is used by Okeanos services' clients in order to acquire the user authentication token. + +The login URI accepts the following parameters: + +========== ====== =============== +URI Method Description +========== ====== =============== +``/login`` GET Authenticate user and return authentication token +========== ====== ================== + +| + +====================== ========================= +Request Parameter Name Value +====================== ========================= +next The URI to redirect to when the process is finished +renew Force token renewal (no value parameter) +force Force session invalidation (no value parameter) +====================== ========================= + +If the request user is not authenticated, is sent to the login view and +after successful login, is redirected back to this view. + +If the request user has not signed the approval terms, is sent to the approval terms view and +after successfully signing the terms, is redirected to back to this view. + +Finally, if the request user is authenticated and has signed the approval terms, +is redirected to the `next` request parameter value. + +The resulted URI contains the user identifier and authentication token. + +=========================== ===================== +Return Code Description +=========================== ===================== +302 (Redirect) +400 (Bad Request) Missing ``next`` parameter +403 (Unauthorized) The ``next`` parameter is beyond the allowed schemes (set by ASTAKOS_REDIRECT_ALLOWED_SCHEMES setting) +=========================== ===================== diff --git a/snf-admin-app/MANIFEST.in b/snf-admin-app/MANIFEST.in new file mode 100644 index 0000000000000000000000000000000000000000..a31a5b4a7b7d40e5de74847ad6c9ab2326eaee20 --- /dev/null +++ b/snf-admin-app/MANIFEST.in @@ -0,0 +1,4 @@ +recursive-include synnefo_admin/admin/static * +recursive-include synnefo_admin/docs/ *.rst + +include distribute_setup.py diff --git a/snf-admin-app/distribute_setup.py b/snf-admin-app/distribute_setup.py new file mode 100644 index 0000000000000000000000000000000000000000..10d66840a7066c01875a3bfdd2545d0b779366a2 --- /dev/null +++ b/snf-admin-app/distribute_setup.py @@ -0,0 +1,485 @@ +#!python +"""Bootstrap distribute installation + +If you want to use setuptools in your package's setup.py, just include this +file in the same directory with it, and add this to the top of your setup.py:: + + from distribute_setup import use_setuptools + use_setuptools() + +If you want to require a specific version of setuptools, set a download +mirror, or use an alternate download directory, you can do so by supplying +the appropriate options to ``use_setuptools()``. + +This file can also be run as a script to install or upgrade setuptools. +""" +import os +import sys +import time +import fnmatch +import tempfile +import tarfile +from distutils import log + +try: + from site import USER_SITE +except ImportError: + USER_SITE = None + +try: + import subprocess + + def _python_cmd(*args): + args = (sys.executable,) + args + return subprocess.call(args) == 0 + +except ImportError: + # will be used for python 2.3 + def _python_cmd(*args): + args = (sys.executable,) + args + # quoting arguments if windows + if sys.platform == 'win32': + def quote(arg): + if ' ' in arg: + return '"%s"' % arg + return arg + args = [quote(arg) for arg in args] + return os.spawnl(os.P_WAIT, sys.executable, *args) == 0 + +DEFAULT_VERSION = "0.6.10" +DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/" +SETUPTOOLS_FAKED_VERSION = "0.6c11" + +SETUPTOOLS_PKG_INFO = """\ +Metadata-Version: 1.0 +Name: setuptools +Version: %s +Summary: xxxx +Home-page: xxx +Author: xxx +Author-email: xxx +License: xxx +Description: xxx +""" % SETUPTOOLS_FAKED_VERSION + + +def _install(tarball): + # extracting the tarball + tmpdir = tempfile.mkdtemp() + log.warn('Extracting in %s', tmpdir) + old_wd = os.getcwd() + try: + os.chdir(tmpdir) + tar = tarfile.open(tarball) + _extractall(tar) + tar.close() + + # going in the directory + subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) + os.chdir(subdir) + log.warn('Now working in %s', subdir) + + # installing + log.warn('Installing Distribute') + if not _python_cmd('setup.py', 'install'): + log.warn('Something went wrong during the installation.') + log.warn('See the error message above.') + finally: + os.chdir(old_wd) + + +def _build_egg(egg, tarball, to_dir): + # extracting the tarball + tmpdir = tempfile.mkdtemp() + log.warn('Extracting in %s', tmpdir) + old_wd = os.getcwd() + try: + os.chdir(tmpdir) + tar = tarfile.open(tarball) + _extractall(tar) + tar.close() + + # going in the directory + subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) + os.chdir(subdir) + log.warn('Now working in %s', subdir) + + # building an egg + log.warn('Building a Distribute egg in %s', to_dir) + _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) + + finally: + os.chdir(old_wd) + # returning the result + log.warn(egg) + if not os.path.exists(egg): + raise IOError('Could not build the egg.') + + +def _do_download(version, download_base, to_dir, download_delay): + egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg' + % (version, sys.version_info[0], sys.version_info[1])) + if not os.path.exists(egg): + tarball = download_setuptools(version, download_base, + to_dir, download_delay) + _build_egg(egg, tarball, to_dir) + sys.path.insert(0, egg) + import setuptools + setuptools.bootstrap_install_from = egg + + +def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, + to_dir=os.curdir, download_delay=15, no_fake=True): + # making sure we use the absolute path + to_dir = os.path.abspath(to_dir) + was_imported = 'pkg_resources' in sys.modules or \ + 'setuptools' in sys.modules + try: + try: + import pkg_resources + if not hasattr(pkg_resources, '_distribute'): + if not no_fake: + _fake_setuptools() + raise ImportError + except ImportError: + return _do_download(version, download_base, to_dir, download_delay) + try: + pkg_resources.require("distribute>="+version) + return + except pkg_resources.VersionConflict: + e = sys.exc_info()[1] + if was_imported: + sys.stderr.write( + "The required version of distribute (>=%s) is not available,\n" + "and can't be installed while this script is running. Please\n" + "install a more recent version first, using\n" + "'easy_install -U distribute'." + "\n\n(Currently using %r)\n" % (version, e.args[0])) + sys.exit(2) + else: + del pkg_resources, sys.modules['pkg_resources'] # reload ok + return _do_download(version, download_base, to_dir, + download_delay) + except pkg_resources.DistributionNotFound: + return _do_download(version, download_base, to_dir, + download_delay) + finally: + if not no_fake: + _create_fake_setuptools_pkg_info(to_dir) + +def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, + to_dir=os.curdir, delay=15): + """Download distribute from a specified location and return its filename + + `version` should be a valid distribute version number that is available + as an egg for download under the `download_base` URL (which should end + with a '/'). `to_dir` is the directory where the egg will be downloaded. + `delay` is the number of seconds to pause before an actual download + attempt. + """ + # making sure we use the absolute path + to_dir = os.path.abspath(to_dir) + try: + from urllib.request import urlopen + except ImportError: + from urllib2 import urlopen + tgz_name = "distribute-%s.tar.gz" % version + url = download_base + tgz_name + saveto = os.path.join(to_dir, tgz_name) + src = dst = None + if not os.path.exists(saveto): # Avoid repeated downloads + try: + log.warn("Downloading %s", url) + src = urlopen(url) + # Read/write all in one block, so we don't create a corrupt file + # if the download is interrupted. + data = src.read() + dst = open(saveto, "wb") + dst.write(data) + finally: + if src: + src.close() + if dst: + dst.close() + return os.path.realpath(saveto) + +def _no_sandbox(function): + def __no_sandbox(*args, **kw): + try: + from setuptools.sandbox import DirectorySandbox + if not hasattr(DirectorySandbox, '_old'): + def violation(*args): + pass + DirectorySandbox._old = DirectorySandbox._violation + DirectorySandbox._violation = violation + patched = True + else: + patched = False + except ImportError: + patched = False + + try: + return function(*args, **kw) + finally: + if patched: + DirectorySandbox._violation = DirectorySandbox._old + del DirectorySandbox._old + + return __no_sandbox + +def _patch_file(path, content): + """Will backup the file then patch it""" + existing_content = open(path).read() + if existing_content == content: + # already patched + log.warn('Already patched.') + return False + log.warn('Patching...') + _rename_path(path) + f = open(path, 'w') + try: + f.write(content) + finally: + f.close() + return True + +_patch_file = _no_sandbox(_patch_file) + +def _same_content(path, content): + return open(path).read() == content + +def _rename_path(path): + new_name = path + '.OLD.%s' % time.time() + log.warn('Renaming %s into %s', path, new_name) + os.rename(path, new_name) + return new_name + +def _remove_flat_installation(placeholder): + if not os.path.isdir(placeholder): + log.warn('Unkown installation at %s', placeholder) + return False + found = False + for file in os.listdir(placeholder): + if fnmatch.fnmatch(file, 'setuptools*.egg-info'): + found = True + break + if not found: + log.warn('Could not locate setuptools*.egg-info') + return + + log.warn('Removing elements out of the way...') + pkg_info = os.path.join(placeholder, file) + if os.path.isdir(pkg_info): + patched = _patch_egg_dir(pkg_info) + else: + patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO) + + if not patched: + log.warn('%s already patched.', pkg_info) + return False + # now let's move the files out of the way + for element in ('setuptools', 'pkg_resources.py', 'site.py'): + element = os.path.join(placeholder, element) + if os.path.exists(element): + _rename_path(element) + else: + log.warn('Could not find the %s element of the ' + 'Setuptools distribution', element) + return True + +_remove_flat_installation = _no_sandbox(_remove_flat_installation) + +def _after_install(dist): + log.warn('After install bootstrap.') + placeholder = dist.get_command_obj('install').install_purelib + _create_fake_setuptools_pkg_info(placeholder) + +def _create_fake_setuptools_pkg_info(placeholder): + if not placeholder or not os.path.exists(placeholder): + log.warn('Could not find the install location') + return + pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1]) + setuptools_file = 'setuptools-%s-py%s.egg-info' % \ + (SETUPTOOLS_FAKED_VERSION, pyver) + pkg_info = os.path.join(placeholder, setuptools_file) + if os.path.exists(pkg_info): + log.warn('%s already exists', pkg_info) + return + + log.warn('Creating %s', pkg_info) + f = open(pkg_info, 'w') + try: + f.write(SETUPTOOLS_PKG_INFO) + finally: + f.close() + + pth_file = os.path.join(placeholder, 'setuptools.pth') + log.warn('Creating %s', pth_file) + f = open(pth_file, 'w') + try: + f.write(os.path.join(os.curdir, setuptools_file)) + finally: + f.close() + +_create_fake_setuptools_pkg_info = _no_sandbox(_create_fake_setuptools_pkg_info) + +def _patch_egg_dir(path): + # let's check if it's already patched + pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') + if os.path.exists(pkg_info): + if _same_content(pkg_info, SETUPTOOLS_PKG_INFO): + log.warn('%s already patched.', pkg_info) + return False + _rename_path(path) + os.mkdir(path) + os.mkdir(os.path.join(path, 'EGG-INFO')) + pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') + f = open(pkg_info, 'w') + try: + f.write(SETUPTOOLS_PKG_INFO) + finally: + f.close() + return True + +_patch_egg_dir = _no_sandbox(_patch_egg_dir) + +def _before_install(): + log.warn('Before install bootstrap.') + _fake_setuptools() + + +def _under_prefix(location): + if 'install' not in sys.argv: + return True + args = sys.argv[sys.argv.index('install')+1:] + for index, arg in enumerate(args): + for option in ('--root', '--prefix'): + if arg.startswith('%s=' % option): + top_dir = arg.split('root=')[-1] + return location.startswith(top_dir) + elif arg == option: + if len(args) > index: + top_dir = args[index+1] + return location.startswith(top_dir) + if arg == '--user' and USER_SITE is not None: + return location.startswith(USER_SITE) + return True + + +def _fake_setuptools(): + log.warn('Scanning installed packages') + try: + import pkg_resources + except ImportError: + # we're cool + log.warn('Setuptools or Distribute does not seem to be installed.') + return + ws = pkg_resources.working_set + try: + setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools', + replacement=False)) + except TypeError: + # old distribute API + setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools')) + + if setuptools_dist is None: + log.warn('No setuptools distribution found') + return + # detecting if it was already faked + setuptools_location = setuptools_dist.location + log.warn('Setuptools installation detected at %s', setuptools_location) + + # if --root or --preix was provided, and if + # setuptools is not located in them, we don't patch it + if not _under_prefix(setuptools_location): + log.warn('Not patching, --root or --prefix is installing Distribute' + ' in another location') + return + + # let's see if its an egg + if not setuptools_location.endswith('.egg'): + log.warn('Non-egg installation') + res = _remove_flat_installation(setuptools_location) + if not res: + return + else: + log.warn('Egg installation') + pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO') + if (os.path.exists(pkg_info) and + _same_content(pkg_info, SETUPTOOLS_PKG_INFO)): + log.warn('Already patched.') + return + log.warn('Patching...') + # let's create a fake egg replacing setuptools one + res = _patch_egg_dir(setuptools_location) + if not res: + return + log.warn('Patched done.') + _relaunch() + + +def _relaunch(): + log.warn('Relaunching...') + # we have to relaunch the process + # pip marker to avoid a relaunch bug + if sys.argv[:3] == ['-c', 'install', '--single-version-externally-managed']: + sys.argv[0] = 'setup.py' + args = [sys.executable] + sys.argv + sys.exit(subprocess.call(args)) + + +def _extractall(self, path=".", members=None): + """Extract all members from the archive to the current working + directory and set owner, modification time and permissions on + directories afterwards. `path' specifies a different directory + to extract to. `members' is optional and must be a subset of the + list returned by getmembers(). + """ + import copy + import operator + from tarfile import ExtractError + directories = [] + + if members is None: + members = self + + for tarinfo in members: + if tarinfo.isdir(): + # Extract directories with a safe mode. + directories.append(tarinfo) + tarinfo = copy.copy(tarinfo) + tarinfo.mode = 448 # decimal for oct 0700 + self.extract(tarinfo, path) + + # Reverse sort directories. + if sys.version_info < (2, 4): + def sorter(dir1, dir2): + return cmp(dir1.name, dir2.name) + directories.sort(sorter) + directories.reverse() + else: + directories.sort(key=operator.attrgetter('name'), reverse=True) + + # Set correct owner, mtime and filemode on directories. + for tarinfo in directories: + dirpath = os.path.join(path, tarinfo.name) + try: + self.chown(tarinfo, dirpath) + self.utime(tarinfo, dirpath) + self.chmod(tarinfo, dirpath) + except ExtractError: + e = sys.exc_info()[1] + if self.errorlevel > 1: + raise + else: + self._dbg(1, "tarfile: %s" % e) + + +def main(argv, version=DEFAULT_VERSION): + """Install or upgrade setuptools and EasyInstall""" + tarball = download_setuptools() + _install(tarball) + + +if __name__ == '__main__': + main(sys.argv[1:]) diff --git a/snf-admin-app/setup.py b/snf-admin-app/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..fd2170412a1f2f1aa4ad784017f7c15c3d934623 --- /dev/null +++ b/snf-admin-app/setup.py @@ -0,0 +1,172 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import distribute_setup +distribute_setup.use_setuptools() + +import os +import sys + +from setuptools import setup, find_packages +from fnmatch import fnmatchcase +from distutils.util import convert_path + +HERE = os.path.abspath(os.path.normpath(os.path.dirname(__file__))) + +from synnefo_admin.version import __version__ + +# Package info +VERSION = __version__ +SHORT_DESCRIPTION = 'Synnefo Admin component' + +PACKAGES_ROOT = '.' +PACKAGES = find_packages(PACKAGES_ROOT) + +# Package meta +CLASSIFIERS = [] + +# Package requirements +INSTALL_REQUIRES = [ + 'Django>=1.4, <1.5', + 'snf-django-lib', + 'django-filter', + 'django-eztables', + 'pycrypto>=2.1.0', +] + +# Provided as an attribute, so you can append to these instead +# of replicating them: +standard_exclude = ["*.py", "*.pyc", "*$py.class", "*~", ".*", "*.bak"] +standard_exclude_directories = [ + ".*", "CVS", "_darcs", "./build", "./dist", "EGG-INFO", "*.egg-info", "snf-0.7" +] + +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php +# Note: you may want to copy this into your setup.py file verbatim, as +# you can't import this from another package, when you don't know if +# that package is installed yet. +def find_package_data( + where=".", + package="", + exclude=standard_exclude, + exclude_directories=standard_exclude_directories, + only_in_packages=True, + show_ignored=False): + """ + Return a dictionary suitable for use in ``package_data`` + in a distutils ``setup.py`` file. + + The dictionary looks like:: + + {"package": [files]} + + Where ``files`` is a list of all the files in that package that + don"t match anything in ``exclude``. + + If ``only_in_packages`` is true, then top-level directories that + are not packages won"t be included (but directories under packages + will). + + Directories matching any pattern in ``exclude_directories`` will + be ignored; by default directories with leading ``.``, ``CVS``, + and ``_darcs`` will be ignored. + + If ``show_ignored`` is true, then all the files that aren"t + included in package data are shown on stderr (for debugging + purposes). + + Note patterns use wildcards, or can be exact paths (including + leading ``./``), and all searching is case-insensitive. + """ + out = {} + stack = [(convert_path(where), "", package, only_in_packages)] + while stack: + where, prefix, package, only_in_packages = stack.pop(0) + for name in os.listdir(where): + fn = os.path.join(where, name) + if os.path.isdir(fn): + bad_name = False + for pattern in exclude_directories: + if (fnmatchcase(name, pattern) + or fn.lower() == pattern.lower()): + bad_name = True + if show_ignored: + print >> sys.stderr, ( + "Directory %s ignored by pattern %s" + % (fn, pattern)) + break + if bad_name: + continue + if (os.path.isfile(os.path.join(fn, "__init__.py")) + and not prefix): + if not package: + new_package = name + else: + new_package = package + "." + name + stack.append((fn, "", new_package, False)) + else: + stack.append((fn, prefix + name + "/", package, only_in_packages)) + elif package or not only_in_packages: + # is a file + bad_name = False + for pattern in exclude: + if (fnmatchcase(name, pattern) + or fn.lower() == pattern.lower()): + bad_name = True + if show_ignored: + print >> sys.stderr, ( + "File %s ignored by pattern %s" + % (fn, pattern)) + break + if bad_name: + continue + out.setdefault(package, []).append(prefix+name) + return out + +setup( + name='snf-admin-app', + version=VERSION, + license='GNU GPLv3', + url='http://www.synnefo.org/', + description=SHORT_DESCRIPTION, + classifiers=CLASSIFIERS, + + author='Synnefo development team', + author_email='synnefo-devel@googlegroups.com', + maintainer='Synnefo development team', + maintainer_email='synnefo-devel@googlegroups.com', + + packages=PACKAGES, + package_dir={'': PACKAGES_ROOT}, + package_data=find_package_data('.'), + include_package_data=True, + zip_safe=False, + + install_requires=INSTALL_REQUIRES, + + dependency_links=['http://www.synnefo.org/packages/pypi'], + + entry_points={ + 'synnefo': [ + 'default_settings = synnefo_admin.app_settings.default', + 'web_apps = synnefo_admin.app_settings:installed_apps', + 'web_middleware = synnefo_admin.app_settings:middleware_classes', + 'urls = synnefo_admin.urls:urlpatterns', + 'web_static = synnefo_admin.app_settings:static_files' + ] + }, +) diff --git a/snf-deploy/files/root/.ssh/.gitignore b/snf-admin-app/synnefo_admin/__init__.py similarity index 100% rename from snf-deploy/files/root/.ssh/.gitignore rename to snf-admin-app/synnefo_admin/__init__.py diff --git a/snf-admin-app/synnefo_admin/admin/__init__.py b/snf-admin-app/synnefo_admin/admin/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/snf-admin-app/synnefo_admin/admin/actions.py b/snf-admin-app/synnefo_admin/admin/actions.py new file mode 100644 index 0000000000000000000000000000000000000000..4b46eb51dc33e8a33d7b08fbfc5cc2063562e8e8 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/actions.py @@ -0,0 +1,188 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +import functools + +from snf_django.lib.api import faults + + +class AdminAction(object): + + """Generic class for actions on admin targets. + + Attributes: + name: The name of the action + target: The target group of the action + check: The function that will trigger once an action is + requested. + allowed_groups: The groups that are allowed to author this action. + karma: The impact of the action. + Accepted values: good, neutral, bad. + caution_level: Indication of how much careful the user should be: + Accepted values: none, warning, dangerous. + description: A short text that describes an action + + Methods: + f: The function that will trigger once an action is + requested. + can_apply: The function that checks if an action can be applied to + a user. + """ + + def __init__(self, name, target, f, c=None, allowed_groups='admin', + karma='neutral', caution_level='none', description=''): + """Initialize the AdminAction class.""" + self.name = name + self.description = description + self.target = target + self.karma = karma + self.caution_level = caution_level + self.allowed_groups = allowed_groups + self.f = f + if c: + self.check = c + + def can_apply(self, t): + """Check if an action can apply to a target. + + If no check function has been registered for this action, this method + will answer always "True". + """ + # Check if a check function has been registered + if not hasattr(self, 'check'): + return True + + try: + res = self.check(t) + # Cyclades raises BadRequest when an action is not supported for an + # instance. Also, if a server is in the process of being created, it + # throws BuildInProgress. + except (faults.BadRequest, faults.BuildInProgress): + return False + + # We accept "None" as correct value. + if res is None: + res = True + return res + + def apply(self, t, *args, **kwargs): + """Apply an action to a target. + + This function will ensure that the requested action can apply to a + target before actually applying it. + """ + if self.can_apply(t): + return self.f(t, *args, **kwargs) + else: + raise AdminActionCannotApply + + def is_user_allowed(self, user): + """Check if a user can author an action.""" + groups = get_user_groups(user) + return set(groups) & set(self.allowed_groups) + + +class AdminActionNotPermitted(Exception): + + """Exception when an action is not permitted.""" + + pass + + +class AdminActionUnknown(Exception): + + """Exception when an action is unknown.""" + + pass + + +class AdminActionNotImplemented(Exception): + + """Exception when an action is not implemented.""" + + pass + + +class AdminActionCannotApply(Exception): + + """Exception when an action cannot apply to a target.""" + + pass + + +def noop(*args, **kwargs): + """Placeholder function.""" + raise AdminActionNotImplemented + + +def get_user_groups(user): + """Extract user groups from request. + + This function requires that astakos client has already stored the user data + in the request. + """ + if not user: + return None + elif isinstance(user, dict): + groups = user['access']['user']['roles'] + return [g["name"] for g in groups] + else: + raise Exception + + +def has_permission_or_403(actions): + """API decorator for user permissions for actions. + + Check if a user (retrieved from Astakos client) can author an action. If + not, raise an AdminActionNotPermitted exception. + """ + def decorator(func): + @functools.wraps(func) + def wrapper(request, op, *args, **kwargs): + if not isinstance(actions, dict): + raise AdminActionNotImplemented + if not actions[op].is_user_allowed(request.user): + raise AdminActionNotPermitted + return func(request, op, *args, **kwargs) + return wrapper + return decorator + + +def get_permitted_actions(actions, user): + """Get a list of actions that a user is permitted to author.""" + permitted_actions = actions.copy() + for key, action in permitted_actions.iteritems(): + if not action.is_user_allowed(user): + permitted_actions.pop(key) + return permitted_actions + + +def get_allowed_actions(actions, inst, user=None): + """Get a list of actions that can apply to an instance. + + Optionally, if the `user` argument is passed, we return the intersection of + the permitted actions for the user and the allowed actions for the + instance. + """ + allowed_actions = [] + if user: + actions = get_permitted_actions(actions, user) + + for key, action in actions.iteritems(): + if action.can_apply(inst): + allowed_actions.append(key) + + return allowed_actions diff --git a/snf-admin-app/synnefo_admin/admin/associations.py b/snf-admin-app/synnefo_admin/admin/associations.py new file mode 100644 index 0000000000000000000000000000000000000000..163c57898809bd4501c90f144e08c5a671189b6b --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/associations.py @@ -0,0 +1,151 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from synnefo_admin.admin.utils import get_actions + + +class AdminAssociation(object): + + """Generic class for associated items. + + Association items have the following fields: + + * list: The associated items, + * type: The type of the items + + and the following optional fields: + + * actions: A dictionary with the permitted actions for *all* the items + * total: How many items are in total + * excluded: How many items are excluded + * showing: How many items are shown + """ + + def __init__(self, request, items, type, actions=None, total=0, + excluded=0, showing=0): + self.items = items + self.type = type + + if actions: + self.actions = actions + else: + self.actions = get_actions(type, request.user) + + if total != 0: + self.total = total + elif hasattr(self, 'count_total'): + self.total = self.count_total() + else: + self.total = self.count_items() + + self.excluded = excluded + self.showing = total + + def count_items(self): + return len(self.items) + + def __str__(self): + return self.__unicode__() + + def __unicode__(self): + return (u"<%s association, total: %s, excluded: %s, showing: %s, items: %s>" % + (self.type.capitalize(), self.total, self.excluded, + self.showing, self.items)) + + +class AdminQuerySetAssociation(AdminAssociation): + + def count_total(self): + return self.items.count() + + @property + def qs(self): + return self.items + + @qs.setter + def qs(self, value): + self.items = value + + def __unicode__(self): + return (u"<%s association, total: %s, excluded: %s, showing: %s, qs: %s>" % + (self.type.capitalize(), self.total, self.excluded, + self.showing, self.qs)) + + +class AdminSimpleAssociation(AdminAssociation): + pass + + +class UserAssociation(AdminQuerySetAssociation): + def __init__(self, request, items, **kwargs): + AdminAssociation.__init__(self, request, items, type='user', **kwargs) + + +class QuotaAssociation(AdminSimpleAssociation): + def __init__(self, request, items, **kwargs): + AdminAssociation.__init__(self, request, items, type='quota', **kwargs) + + +class VMAssociation(AdminQuerySetAssociation): + def __init__(self, request, items, **kwargs): + AdminAssociation.__init__(self, request, items, type='vm', **kwargs) + + +class SimpleVMAssociation(AdminSimpleAssociation): + def __init__(self, request, items, **kwargs): + AdminAssociation.__init__(self, request, items, type='vm', **kwargs) + + +class VolumeAssociation(AdminQuerySetAssociation): + def __init__(self, request, items, **kwargs): + AdminAssociation.__init__(self, request, items, type='volume', **kwargs) + + +class NetworkAssociation(AdminQuerySetAssociation): + def __init__(self, request, items, **kwargs): + AdminAssociation.__init__(self, request, items, type='network', **kwargs) + + +class SimpleNetworkAssociation(AdminSimpleAssociation): + def __init__(self, request, items, **kwargs): + AdminAssociation.__init__(self, request, items, type='network', **kwargs) + + +class NicAssociation(AdminQuerySetAssociation): + def __init__(self, request, items, **kwargs): + AdminAssociation.__init__(self, request, items, type='nic', **kwargs) + + +class SimpleNicAssociation(AdminSimpleAssociation): + def __init__(self, request, items, **kwargs): + AdminAssociation.__init__(self, request, items, type='nic', **kwargs) + + +class IPAssociation(AdminQuerySetAssociation): + def __init__(self, request, items, **kwargs): + AdminAssociation.__init__(self, request, items, type='ip', **kwargs) + + +class IPLogAssociation(AdminQuerySetAssociation): + + order_by = 'allocated_at' + + def __init__(self, request, items, **kwargs): + AdminAssociation.__init__(self, request, items, type='ip_log', **kwargs) + + +class ProjectAssociation(AdminQuerySetAssociation): + def __init__(self, request, items, **kwargs): + AdminAssociation.__init__(self, request, items, type='project', **kwargs) diff --git a/snf-admin-app/synnefo_admin/admin/exceptions.py b/snf-admin-app/synnefo_admin/admin/exceptions.py new file mode 100644 index 0000000000000000000000000000000000000000..57afc95ef3697c0377054e1c843549e2ba3a2e8c --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/exceptions.py @@ -0,0 +1,36 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from django import http +# Add the exceptions that are defined in actions.py in this file too, so that +# all exceptions can exist under the same namespace. +from synnefo_admin.admin.actions import (AdminActionNotPermitted, + AdminActionUnknown, + AdminActionNotImplemented, + AdminActionCannotApply) + + +class AdminHttp404(http.Http404): + + """404 Exception solely for admin pages.""" + + pass + + +class AdminHttp405(http.Http404): + + """405 Exception solely for admin pages.""" + + status = 405 diff --git a/snf-admin-app/synnefo_admin/admin/middleware/__init__.py b/snf-admin-app/synnefo_admin/admin/middleware/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f8b77fc1786199d2c5c5c16eae7b9c94a1d85066 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/middleware/__init__.py @@ -0,0 +1,57 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import logging +from django.template import RequestContext, loader +from django.http import HttpResponseNotFound +from synnefo_admin.admin.exceptions import AdminHttp404, AdminHttp405 +from synnefo_admin.admin.views import default_dict + +ADMIN_404_TEMPLATE = 'admin/admin_404.html' +ADMIN_405_TEMPLATE = 'admin/admin_405.html' + + +def update_request_context(request, extra_context={}, **kwargs): + """Update request context. + + Update request context as Django does internally in `direct_to_template` + generic view. + """ + for key, value in kwargs.items(): + if callable(value): + extra_context[key] = value() + else: + extra_context[key] = value + return RequestContext(request, extra_context) + + +class AdminMiddleware(object): + + """Middleware for the admin app.""" + + def process_exception(self, request, exception): + """Create a 404 page only for exceptions generated by the admin app.""" + if isinstance(exception, AdminHttp404): + template = ADMIN_404_TEMPLATE + elif isinstance(exception, AdminHttp405): + template = ADMIN_405_TEMPLATE + else: + return + + c = update_request_context(request, default_dict, + msg=exception.message) + t = loader.get_template(template) + response = t.render(c) + return HttpResponseNotFound(response) diff --git a/snf-admin-app/synnefo_admin/admin/models.py b/snf-admin-app/synnefo_admin/admin/models.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/snf-admin-app/synnefo_admin/admin/queries_common.py b/snf-admin-app/synnefo_admin/admin/queries_common.py new file mode 100644 index 0000000000000000000000000000000000000000..600438c82c566dc31d01f4a36df7b96e7c043bf0 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/queries_common.py @@ -0,0 +1,229 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import functools +import logging +from operator import or_, and_ + +from django.db.models import Q +from django.core.exceptions import FieldError +from django.conf import settings + +from synnefo_admin.admin.utils import model_dict +from synnefo_admin import admin_settings + +sign = admin_settings.ADMIN_FIELD_SIGN + + +def prefix_strip(query): + """Remove the prefix from the ID of Cyclades models. + + This function also returns a suggested lookup type for the stripped IDs. + Normally, the lookup type is "contains", but if the user has entered a + query like this: + + <prefix>4545 + + the lookup type should be "startswith". + """ + query = str(query) + lookup_type = 'contains' + prefix = settings.BACKEND_PREFIX_ID + + if query.startswith(prefix): + query = query.replace(prefix, '') + lookup_type = 'startswith' + + if not query.isdigit(): + return None, None + + return int(query), lookup_type + + +def get_model_field(model, query, field): + """Get query results for a specific model field. + + This function returns the results in a list format, which can be used in + an "IN" query easily, especially when that query will be executed in + another database. + """ + model = model_dict[model] + ids = model.objects.filter(query).values_list(field, flat=True) + return list(ids) + + +def model_filter(func): + """Decorator to format query before passing it to a filter function. + + The purpose of the decorator is to: + a) Split the queries into multiple keywords (space/tab separated). + b) Concatenate terms that have the ADMIN_FIELD_SIGN ("=") between them. + b) Ignore any empty queries. + """ + def process_terms(terms): + """Generic term processing. + + This function does the following: + * Concatenate terms that have the admin_settings.ADMIN_FIELD_SIGN ("=") + between them. E.g. the following list: + + ['first_name', '=', 'john', 'doe', 'last_name=', 'd'] + + becomes: + + ['first_name=john', 'doe', 'last_name=d'] + """ + new_terms = [] + cand = '' + for term in terms: + # Check if the current term can be concatenated with the previous + # (candidate) ones. + if term.startswith(sign) or cand.endswith(sign): + cand = cand + term + continue + # If the candidate cannot be concatenated with the current term, + # append it to the `new_terms` list. + if cand: + new_terms.append(cand) + cand = term + # Always append the last candidate, if valid + if cand: + new_terms.append(cand) + return new_terms + + @functools.wraps(func) + def wrapper(queryset, query, *args, **kwargs): + if isinstance(query, basestring): + query = query.split() + query = process_terms(query) + + if query: + try: + return func(queryset, query, *args, **kwargs) + except (FieldError, TypeError) as e: + logging.error("%s", e.message) + return queryset.none() + else: + return queryset + + return wrapper + + +def malicious(field): + """Check if query searches in private fields.""" + if 'token' in field or 'password' in field: + return True + else: + False + + +def update_queries(**queries): + """Extract nested queries from a single query. + + Check if the query is actually a nested query, by searching for the + admin_settings.ADMIN_FIELD_SIGN (commonly "="). + FIXME: This is not the best/cleaner/intuitive approach to do this. + """ + new_queries = queries.copy() + for key, value in queries.iteritems(): + if isinstance(value, str) or isinstance(value, unicode): + nested_query = value.split(sign, 1) + if len(nested_query) == 1: + continue + field = nested_query[0] + value = nested_query[1] + if value: + del new_queries[key] + # Do not filter sensitive data. + if not malicious(field): + new_queries[field] = value + return new_queries + + +def query_list(*qobjects, **default_queries): + """Return Q object list for the requested queries. + + This function can handle transparrently Q objects as well as simple + queries. + """ + queries = update_queries(**default_queries) + lookup_type = queries.pop("lookup_type", "icontains") + ql = list(qobjects) + ql += [Q(**{"%s__%s" % (field, lookup_type): value}) + for field, value in queries.iteritems()] + return ql if ql else [Q()] + + +def query_or(*qobjects, **queries): + """Return ORed Q object for the requested query.""" + ql = query_list(*qobjects, **queries) + return reduce(or_, ql) + + +def query_and(*qobjects, **queries): + """Return ANDed Q object for the requested query.""" + ql = query_list(*qobjects, **queries) + return reduce(and_, ql) + + +# ----------------- MODEL QUERIES --------------------# +# The following functions implement the query logic for each model + +def query(model, queries): + """Common entry point for getting a Q object for a model.""" + fun = globals().get('query_' + model) + if fun: + return fun(queries) + else: + raise Exception("Unknown model: %s" % model) + + +def query_user(queries): + qor = [query_or(first_name=q, last_name=q, email=q, uuid=q) + for q in queries] + return query_and(*qor) + + +def query_vm(queries): + qor = [] + for q in queries: + _qor = query_or(name=q, imageid=q) + id, lt = prefix_strip(q) + if id: + _qor = query_or(_qor, id=id, lookup_type=lt) + qor.append(_qor) + return query_and(*qor) + + +def query_volume(queries): + qor = [query_or(name=q, description=q, id=q) + for q in queries] + return query_and(*qor) + + +def query_network(queries): + qor = [query_or(name=q, id=q) for q in queries] + return query_and(*qor) + + +def query_ip(queries): + qor = [query_or(address=q) for q in queries] + return query_and(*qor) + + +def query_project(queries): + qor = [query_or(id=q, realname=q, description=q, uuid=q, homepage=q) + for q in queries] + return query_and(*qor) diff --git a/snf-admin-app/synnefo_admin/admin/resources/__init__.py b/snf-admin-app/synnefo_admin/admin/resources/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/snf-admin-app/synnefo_admin/admin/resources/auth_providers/__init__.py b/snf-admin-app/synnefo_admin/admin/resources/auth_providers/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/snf-admin-app/synnefo_admin/admin/resources/auth_providers/actions.py b/snf-admin-app/synnefo_admin/admin/resources/auth_providers/actions.py new file mode 100644 index 0000000000000000000000000000000000000000..d7c447bb9cf0be6e3aeb182c7d385fcd79881b5c --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/resources/auth_providers/actions.py @@ -0,0 +1,16 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +"""Auth-Providers have no actions.""" diff --git a/snf-admin-app/synnefo_admin/admin/resources/auth_providers/filters.py b/snf-admin-app/synnefo_admin/admin/resources/auth_providers/filters.py new file mode 100644 index 0000000000000000000000000000000000000000..16b971ec01051698fe1380e32fbe4d237525f7aa --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/resources/auth_providers/filters.py @@ -0,0 +1,15 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + diff --git a/snf-admin-app/synnefo_admin/admin/resources/auth_providers/utils.py b/snf-admin-app/synnefo_admin/admin/resources/auth_providers/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..16b971ec01051698fe1380e32fbe4d237525f7aa --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/resources/auth_providers/utils.py @@ -0,0 +1,15 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + diff --git a/snf-admin-app/synnefo_admin/admin/resources/auth_providers/views.py b/snf-admin-app/synnefo_admin/admin/resources/auth_providers/views.py new file mode 100644 index 0000000000000000000000000000000000000000..7770557701693875e53f612ef8abec487cbdfeb7 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/resources/auth_providers/views.py @@ -0,0 +1,85 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from django.core.urlresolvers import reverse + +from astakos.im.models import AstakosUserAuthProvider +from synnefo_admin.admin.tables import AdminJSONView + +templates = { + 'list': 'admin/auth_provider_list.html', + 'details': 'admin/auth_provider_details.html', +} + + +class AstakosUserAuthProviderJSONView(AdminJSONView): + model = AstakosUserAuthProvider + fields = ('id', 'module', 'identifier', 'active', 'created') + + extra = True + + def get_extra_data_row(self, inst): + extra_dict = { + 'allowed_actions': { + 'display_name': "", + 'value': [], + 'visible': False, + }, 'id': { + 'display_name': "ID", + 'value': inst.id, + 'visible': False, + }, 'item_name': { + 'display_name': "Name", + 'value': inst.module, + 'visible': False, + }, 'details_url': { + 'display_name': "Details", + 'value': (reverse('admin-details', args=['auth_provider', + inst.id])), + 'visible': True, + }, 'contact_email': { + 'display_name': "Contact email", + 'value': None, + 'visible': False, + }, 'contact_name': { + 'display_name': "Contact name", + 'value': None, + 'visible': False, + }, 'description': { + 'display_name': "Description", + 'value': inst.info_data, + 'visible': True, + }, 'auth_backend': { + 'display_name': "Auth backend", + 'value': inst.auth_backend, + 'visible': True, + } + } + + return extra_dict + + +JSON_CLASS = AstakosUserAuthProviderJSONView + + +def catalog(request): + """List view for Cyclades auth_providers.""" + context = {} + context['action_dict'] = {} + context['columns'] = ["Name", "Identifier", "Active", + "Creation date", ""] + context['item_type'] = 'auth_provider' + + return context diff --git a/snf-admin-app/synnefo_admin/admin/resources/groups/__init__.py b/snf-admin-app/synnefo_admin/admin/resources/groups/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/snf-admin-app/synnefo_admin/admin/resources/groups/actions.py b/snf-admin-app/synnefo_admin/admin/resources/groups/actions.py new file mode 100644 index 0000000000000000000000000000000000000000..4c91257a618481b0885140da14def59d02714f07 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/resources/groups/actions.py @@ -0,0 +1,16 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +"""Groups have no actions.""" diff --git a/snf-admin-app/synnefo_admin/admin/resources/groups/filters.py b/snf-admin-app/synnefo_admin/admin/resources/groups/filters.py new file mode 100644 index 0000000000000000000000000000000000000000..16b971ec01051698fe1380e32fbe4d237525f7aa --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/resources/groups/filters.py @@ -0,0 +1,15 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + diff --git a/snf-admin-app/synnefo_admin/admin/resources/groups/utils.py b/snf-admin-app/synnefo_admin/admin/resources/groups/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..16b971ec01051698fe1380e32fbe4d237525f7aa --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/resources/groups/utils.py @@ -0,0 +1,15 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + diff --git a/snf-admin-app/synnefo_admin/admin/resources/groups/views.py b/snf-admin-app/synnefo_admin/admin/resources/groups/views.py new file mode 100644 index 0000000000000000000000000000000000000000..279428697e647ab817a6b06526973a3168403010 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/resources/groups/views.py @@ -0,0 +1,73 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from django.contrib.auth.models import Group + +from synnefo_admin.admin.exceptions import AdminHttp404 +from synnefo_admin.admin.tables import AdminJSONView + + +templates = { + 'list': 'admin/group_list.html', + 'details': 'admin/group_details.html', +} + + +class GroupJSONView(AdminJSONView): + model = Group + fields = ('id', 'name') + + extra = True + + def get_extra_data_row(self, inst): + extra_dict = { + 'allowed_actions': { + 'display_name': "", + 'value': [], + 'visible': False, + }, 'id': { + 'display_name': "ID", + 'value': inst.id, + 'visible': False, + }, 'item_name': { + 'display_name': "Name", + 'value': inst.name, + 'visible': False, + }, + } + + return extra_dict + + +JSON_CLASS = GroupJSONView + + +def do_action(request, op, id): + raise AdminHttp404("There are no actions for Groups") + + +def catalog(request): + """List view for Cyclades groups.""" + context = {} + context['action_dict'] = {} + context['columns'] = ["ID", "Name", ""] + context['item_type'] = 'group' + + return context + + +def details(request, query): + """Details view for Cyclades groups.""" + raise AdminHttp404("There are no details for Groups") diff --git a/snf-admin-app/synnefo_admin/admin/resources/ip_logs/__init__.py b/snf-admin-app/synnefo_admin/admin/resources/ip_logs/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/snf-admin-app/synnefo_admin/admin/resources/ip_logs/filters.py b/snf-admin-app/synnefo_admin/admin/resources/ip_logs/filters.py new file mode 100644 index 0000000000000000000000000000000000000000..39472441e893191a81a0eabe120cf62dfe39ef7d --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/resources/ip_logs/filters.py @@ -0,0 +1,69 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +import logging + +from synnefo.db.models import IPAddressLog, VirtualMachine +import django_filters + +from synnefo_admin.admin.queries_common import (query, model_filter, + get_model_field) + + +@model_filter +def filter_user(queryset, queries): + q = query("user", queries) + user_ids = get_model_field("user", q, 'uuid') + vm_ids = VirtualMachine.objects.filter(userid__in=user_ids) + return queryset.filter(server_id__in=vm_ids) + + +@model_filter +def filter_vm(queryset, queries): + q = query("vm", queries) + ids = get_model_field("vm", q, 'id') + return queryset.filter(server_id__in=ids) + + +@model_filter +def filter_network(queryset, queries): + q = query("network", queries) + ids = get_model_field("network", q, 'id') + return queryset.filter(network_id__in=ids) + + +@model_filter +def filter_ip(queryset, queries): + q = query("ip", queries) + return queryset.filter(q) + + +class IPLogFilterSet(django_filters.FilterSet): + + """A collection of filters for ip log. + + This filter collection is based on django-filter's FilterSet. + """ + + user = django_filters.CharFilter(label='OF User', action=filter_user) + vm = django_filters.CharFilter(label='OF VM', action=filter_vm) + net = django_filters.CharFilter(label='OF Network', action=filter_network) + ip = django_filters.CharFilter(label='OF IP', action=filter_ip) + active = django_filters.BooleanFilter(label='Active') + + class Meta: + model = IPAddressLog + fields = ('user', 'vm', 'net', 'ip', 'active') diff --git a/snf-admin-app/synnefo_admin/admin/resources/ip_logs/utils.py b/snf-admin-app/synnefo_admin/admin/resources/ip_logs/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..c93b02bd2d7d84f09bfb402ed64ac8d8cfcd8ecb --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/resources/ip_logs/utils.py @@ -0,0 +1,49 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +import logging + +from django.core.exceptions import ObjectDoesNotExist + +from astakos.im.models import AstakosUser +from synnefo.db.models import IPAddress, VirtualMachine, Network + +from synnefo_admin.admin.utils import create_details_href + + +def get_ip_details_href(ip_log): + addr = ip_log.address + try: + ip = IPAddress.objects.get(address=addr, network__public=True) + return create_details_href('ip', addr, ip.id) + except ObjectDoesNotExist: + return addr + + +def get_vm_details_href(ip_log): + vm = VirtualMachine.objects.get(pk=ip_log.server_id) + return create_details_href('vm', vm.name, vm.pk) + + +def get_network_details_href(ip_log): + network = Network.objects.get(pk=ip_log.network_id) + return create_details_href('network', network.name, network.pk) + + +def get_user_details_href(ip_log): + vm = VirtualMachine.objects.get(pk=ip_log.server_id) + user = AstakosUser.objects.get(uuid=vm.userid) + return create_details_href('user', user.realname, user.email) diff --git a/snf-admin-app/synnefo_admin/admin/resources/ip_logs/views.py b/snf-admin-app/synnefo_admin/admin/resources/ip_logs/views.py new file mode 100644 index 0000000000000000000000000000000000000000..1772dc637d73aa28a820cbd9492b4356bdee156d --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/resources/ip_logs/views.py @@ -0,0 +1,106 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +import logging +from collections import OrderedDict + +from synnefo.db.models import IPAddressLog + +from synnefo_admin.admin.exceptions import AdminHttp404 +from synnefo_admin.admin.utils import _filter_public_ip_log +from synnefo_admin.admin.tables import AdminJSONView + +from .utils import (get_user_details_href, get_ip_details_href, + get_vm_details_href, get_network_details_href) +from .filters import IPLogFilterSet + + +templates = { + 'list': 'admin/ip_log_list.html', +} + + +class IPLogJSONView(AdminJSONView): + model = IPAddressLog + fields = ('address', 'server_id', 'network_id', 'allocated_at', + 'released_at', 'active',) + filters = IPLogFilterSet + + # This is a rather hackish method of plugging ourselves after + # get_queryset() + def set_object_list(self): + """Show logs only for public IPs.""" + self.qs = _filter_public_ip_log(self.qs) + return AdminJSONView.set_object_list(self) + + def format_data_row(self, row): + row = list(row) + row[3] = row[3].strftime("%Y-%m-%d %H:%M") + if row[4]: + row[4] = row[4].strftime("%Y-%m-%d %H:%M") + else: + row[4] = "-" + return row + + def get_extra_data_row(self, inst): + extra_dict = OrderedDict() + extra_dict['id'] = { + 'display_name': "ID", + 'value': inst.pk, + 'visible': False, + } + extra_dict['ip_info'] = { + 'display_name': "IP", + 'value': get_ip_details_href(inst), + 'visible': True, + } + extra_dict['vm_info'] = { + 'display_name': "VM", + 'value': get_vm_details_href(inst), + 'visible': True, + } + extra_dict['network_info'] = { + 'display_name': "Network", + 'value': get_network_details_href(inst), + 'visible': True, + } + extra_dict['user_info'] = { + 'display_name': "User", + 'value': get_user_details_href(inst), + 'visible': True, + } + + return extra_dict + + +JSON_CLASS = IPLogJSONView + + +def catalog(request): + """List view for Cyclades ip log.""" + context = {} + context['action_dict'] = None + context['filter_dict'] = IPLogFilterSet().filters.values() + context['columns'] = ["Address", "Server ID", "Network ID", + "Allocation date", "Release date", "Active", ""] + context['item_type'] = 'ip_log' + + return context + + +def details(request, query): + """Details view for Cyclades ip history.""" + raise AdminHttp404("There are no details for any entry of the IP History") diff --git a/snf-admin-app/synnefo_admin/admin/resources/ips/__init__.py b/snf-admin-app/synnefo_admin/admin/resources/ips/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/snf-admin-app/synnefo_admin/admin/resources/ips/actions.py b/snf-admin-app/synnefo_admin/admin/resources/ips/actions.py new file mode 100644 index 0000000000000000000000000000000000000000..13b234be2a61e3deb795489d29e921c506e6cae7 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/resources/ips/actions.py @@ -0,0 +1,68 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +import logging +from collections import OrderedDict + +from synnefo.logic import ips + +from synnefo_admin.admin.actions import AdminAction, noop +from synnefo_admin.admin.utils import update_actions_rbac, send_admin_email + + +class IPAction(AdminAction): + + """Class for actions on ips. Derived from AdminAction. + + Pre-determined Attributes: + target: ip + """ + + def __init__(self, name, f, **kwargs): + """Initialize the class with provided values.""" + AdminAction.__init__(self, name=name, target='ip', f=f, **kwargs) + + +def check_ip_action(action): + """Check if an action can apply to an IP. + + This is a wrapper for `validate_ip_action` of the ips module, that handles + the tupples returned by it. + """ + def check(ip, action): + res, _ = ips.validate_ip_action(ip, action) + return res + + return lambda ip: check(ip, action) + + +def generate_actions(): + """Create a list of actions on ips.""" + actions = OrderedDict() + + actions['destroy'] = IPAction(name='Destroy', c=check_ip_action("DELETE"), + f=ips.delete_floating_ip, karma='bad', + caution_level='dangerous',) + + actions['reassign'] = IPAction(name='Reassign to project', f=noop, + karma='neutral', caution_level='dangerous',) + + actions['contact'] = IPAction(name='Send e-mail', f=send_admin_email,) + + update_actions_rbac(actions) + + return actions +cached_actions = generate_actions() diff --git a/snf-admin-app/synnefo_admin/admin/resources/ips/filters.py b/snf-admin-app/synnefo_admin/admin/resources/ips/filters.py new file mode 100644 index 0000000000000000000000000000000000000000..66429ae1450fb0df90e6a39d4e2b814831f5cd7b --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/resources/ips/filters.py @@ -0,0 +1,75 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +import logging +import django_filters + +from synnefo.db.models import IPAddress + +from synnefo_admin.admin.queries_common import (query, model_filter, + get_model_field) + + +@model_filter +def filter_ip(queryset, queries): + q = query("ip", queries) + return queryset.filter(q) + + +@model_filter +def filter_user(queryset, queries): + q = query("user", queries) + ids = get_model_field("user", q, 'uuid') + return queryset.filter(userid__in=ids) + + +@model_filter +def filter_vm(queryset, queries): + q = query("vm", queries) + ids = get_model_field("vm", q, 'id') + return queryset.filter(nic__machine__id__in=ids) + + +@model_filter +def filter_network(queryset, queries): + q = query("network", queries) + ids = get_model_field("network", q, 'id') + return queryset.filter(network__id__in=ids) + + +@model_filter +def filter_project(queryset, queries): + q = query("project", queries) + ids = get_model_field("project", q, 'uuid') + return queryset.filter(project__in=ids) + + +class IPFilterSet(django_filters.FilterSet): + + """A collection of filters for ips. + + This filter collection is based on django-filter's FilterSet. + """ + + ip = django_filters.CharFilter(label='IP', action=filter_ip) + user = django_filters.CharFilter(label='OF User', action=filter_user) + vm = django_filters.CharFilter(label='OF VM', action=filter_vm) + net = django_filters.CharFilter(label='OF Network', action=filter_network) + proj = django_filters.CharFilter(label='OF Project', action=filter_project) + + class Meta: + model = IPAddress + fields = ('ip', 'floating_ip', 'user', 'vm', 'net', 'proj') diff --git a/snf-admin-app/synnefo_admin/admin/resources/ips/utils.py b/snf-admin-app/synnefo_admin/admin/resources/ips/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..c8146e7e13d15835f10120bd17c6786ba4a9ee98 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/resources/ips/utils.py @@ -0,0 +1,83 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +import logging + +from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned + +from astakos.im.models import AstakosUser +from synnefo.db.models import IPAddress, IPAddressLog + +from synnefo_admin.admin.exceptions import AdminHttp404 +from synnefo_admin.admin.utils import create_details_href + + +def get_ip_or_404(query): + try: + return IPAddress.objects.get(address=query) + except ObjectDoesNotExist: + pass + except MultipleObjectsReturned: + raise AdminHttp404("""Hm, that's interesting. There are more than one + entries for this address: %s""" % query) + + try: + return IPAddress.objects.get(pk=int(query)) + except (ObjectDoesNotExist, ValueError): + # Check the IPAddressLog and inform the user that the IP existed at + # sometime. + msg = "No IP was found that matches this query: %s" % query + try: + if IPAddressLog.objects.filter(address=query).exists(): + msg = """This IP was deleted. Check the "IP History" tab for + more details.""" + except ObjectDoesNotExist: + pass + raise AdminHttp404(msg) + + +def get_contact_email(inst): + if inst.userid: + return AstakosUser.objects.get(uuid=inst.userid).email + + +def get_contact_name(inst): + if inst.userid: + return AstakosUser.objects.get(uuid=inst.userid).realname + + +def get_user_details_href(ip): + if ip.userid: + user = AstakosUser.objects.get(uuid=ip.userid) + return create_details_href('user', user.realname, user.email) + else: + return "-" + + +def get_vm_details_href(ip): + if ip.in_use(): + vm = ip.nic.machine + return create_details_href('vm', vm.name, vm.pk) + else: + return "-" + + +def get_network_details_href(ip): + if ip.in_use(): + network = ip.nic.network + return create_details_href('network', network.name, network.pk) + else: + return "-" diff --git a/snf-admin-app/synnefo_admin/admin/resources/ips/views.py b/snf-admin-app/synnefo_admin/admin/resources/ips/views.py new file mode 100644 index 0000000000000000000000000000000000000000..2dbad5e4ae5a29f48c8d277f46f271896c203222 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/resources/ips/views.py @@ -0,0 +1,212 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +import logging +from collections import OrderedDict + +from django.core.urlresolvers import reverse +from django.utils.html import escape + +from synnefo.db.models import IPAddress, IPAddressLog +from astakos.im.models import AstakosUser, Project + +from synnefo_admin.admin.actions import (has_permission_or_403, + get_allowed_actions, + get_permitted_actions,) +from synnefo_admin.admin.resources.users.utils import get_user_or_404 +from synnefo_admin.admin.tables import AdminJSONView +from synnefo_admin.admin.associations import ( + UserAssociation, QuotaAssociation, VMAssociation, VolumeAssociation, + NetworkAssociation, NicAssociation, IPAssociation, IPLogAssociation, + ProjectAssociation, SimpleVMAssociation, SimpleNetworkAssociation, + SimpleNicAssociation) + +from .utils import (get_contact_email, get_contact_name, get_user_details_href, + get_ip_or_404, get_network_details_href, + get_vm_details_href) +from .actions import cached_actions +from .filters import IPFilterSet + + +templates = { + 'list': 'admin/ip_list.html', + 'details': 'admin/ip_details.html', +} + + +class IPJSONView(AdminJSONView): + model = IPAddress + fields = ('pk', 'address', 'floating_ip', 'created', 'userid',) + filters = IPFilterSet + + def format_data_row(self, row): + row = list(row) + row[3] = row[3].strftime("%Y-%m-%d %H:%M") + return row + + def get_extra_data(self, qs): + # FIXME: The `contact_name`, `contact_email` fields will cripple our db + if self.form.cleaned_data['iDisplayLength'] < 0: + qs = qs.only('pk', 'address', 'floating_ip', 'created', 'userid',) + return [self.get_extra_data_row(row) for row in qs] + + def get_extra_data_row(self, inst): + if self.dt_data['iDisplayLength'] < 0: + extra_dict = {} + else: + extra_dict = OrderedDict() + + extra_dict['allowed_actions'] = { + 'display_name': "", + 'value': get_allowed_actions(cached_actions, inst, + self.request.user), + 'visible': False, + } + extra_dict['id'] = { + 'display_name': "ID", + 'value': inst.pk, + 'visible': False, + } + extra_dict['item_name'] = { + 'display_name': "Name", + 'value': inst.address, + 'visible': False, + } + extra_dict['details_url'] = { + 'display_name': "Details", + 'value': reverse('admin-details', args=['ip', inst.pk]), + 'visible': True, + } + extra_dict['contact_id'] = { + 'display_name': "Contact ID", + 'value': inst.userid, + 'visible': False, + } + extra_dict['contact_email'] = { + 'display_name': "Contact email", + 'value': escape(get_contact_email(inst)), + 'visible': False, + } + extra_dict['contact_name'] = { + 'display_name': "Contact name", + 'value': escape(get_contact_name(inst)), + 'visible': False, + } + + if self.form.cleaned_data['iDisplayLength'] < 0: + extra_dict['minimal'] = { + 'display_name': "No summary available", + 'value': "Have you per chance pressed 'Select All'?", + 'visible': True, + } + else: + extra_dict.update(self.add_verbose_data(inst)) + + return extra_dict + + def add_verbose_data(self, inst): + extra_dict = OrderedDict() + extra_dict['user_info'] = { + 'display_name': "User", + 'value': get_user_details_href(inst), + 'visible': True, + } + extra_dict['vm_info'] = { + 'display_name': "VM", + 'value': get_vm_details_href(inst), + 'visible': True, + } + extra_dict['network_info'] = { + 'display_name': "Network info", + 'value': get_network_details_href(inst), + 'visible': True, + } + extra_dict['updated'] = { + 'display_name': "Update date", + 'value': inst.updated.strftime("%Y-%m-%d %H:%M"), + 'visible': True, + } + extra_dict['in_use'] = { + 'display_name': "Currently in Use", + 'value': inst.in_use(), + 'visible': True, + } + + return extra_dict + + +JSON_CLASS = IPJSONView + + +@has_permission_or_403(cached_actions) +def do_action(request, op, id): + """Apply the requested action on the specified ip.""" + if op == "contact": + user = get_user_or_404(id) + else: + ip = IPAddress.objects.get(id=id) + actions = get_permitted_actions(cached_actions, request.user) + + if op == 'contact': + actions[op].apply(user, request) + else: + actions[op].apply(ip) + + +def catalog(request): + """List view for Cyclades ips.""" + context = {} + context['action_dict'] = get_permitted_actions(cached_actions, + request.user) + context['filter_dict'] = IPFilterSet().filters.values() + context['columns'] = ["ID", "Address", "Floating", + "Creation date", "User ID", ""] + context['item_type'] = 'ip' + + return context + + +def details(request, query): + """Details view for Astakos users.""" + ip = get_ip_or_404(query) + associations = [] + + vm_list = [ip.nic.machine] if ip.in_use() else [] + associations.append(SimpleVMAssociation(request, vm_list,)) + + network_list = [ip.nic.network] if ip.in_use() else [] + associations.append(SimpleNetworkAssociation(request, network_list,)) + + nic_list = [ip.nic] if ip.in_use() else [] + associations.append(SimpleNicAssociation(request, nic_list,)) + + user_list = AstakosUser.objects.filter(uuid=ip.userid) + associations.append(UserAssociation(request, user_list,)) + + project_list = Project.objects.filter(uuid=ip.project) + associations.append(ProjectAssociation(request, project_list,)) + + ip_log_list = IPAddressLog.objects.filter(address=ip.address) + associations.append(IPLogAssociation(request, ip_log_list)) + + context = { + 'main_item': ip, + 'main_type': 'ip', + 'action_dict': get_permitted_actions(cached_actions, request.user), + 'associations_list': associations, + } + + return context diff --git a/snf-admin-app/synnefo_admin/admin/resources/networks/__init__.py b/snf-admin-app/synnefo_admin/admin/resources/networks/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/snf-admin-app/synnefo_admin/admin/resources/networks/actions.py b/snf-admin-app/synnefo_admin/admin/resources/networks/actions.py new file mode 100644 index 0000000000000000000000000000000000000000..cf40bfb92ecbdb643571d58f486aa3adb933c2bb --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/resources/networks/actions.py @@ -0,0 +1,94 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +import logging +from collections import OrderedDict + +from synnefo.logic.networks import validate_network_action +from synnefo.logic import networks + +from synnefo_admin.admin.actions import AdminAction, noop +from synnefo_admin.admin.utils import update_actions_rbac, send_admin_email + + +class NetworkAction(AdminAction): + + """Class for actions on networks. Derived from AdminAction. + + Pre-determined Attributes: + target: network + """ + + def __init__(self, name, f, **kwargs): + """Initialize the class with provided values.""" + AdminAction.__init__(self, name=name, target='network', f=f, **kwargs) + + +def drain_network(network): + network.drained = True + network.save() + + +def undrain_network(network): + network.drained = False + network.save() + + +def check_network_action(action): + if action == "CONTACT": + # Contact action is allowed only on private networks. However, this + # function may get called with an AstakosUser as a target. In this + # case, we always confirm the action. + return lambda n: not getattr(n, 'public', False) + elif action == "DRAIN": + return lambda n: not n.drained and not n.deleted and n.public + elif action == "UNDRAIN": + return lambda n: n.drained and not n.deleted and n.public + else: + return lambda n: validate_network_action(n, action) + + +def generate_actions(): + """Create a list of actions on networks.""" + actions = OrderedDict() + + actions['drain'] = NetworkAction(name='Drain', f=drain_network, + c=check_network_action('DRAIN'), + caution_level=True,) + + actions['undrain'] = NetworkAction(name='Undrain', f=undrain_network, + c=check_network_action('UNDRAIN'), + karma='neutral',) + + actions['destroy'] = NetworkAction(name='Destroy', f=networks.delete, + c=check_network_action('DESTROY'), + karma='bad', caution_level='dangerous',) + + actions['reassign'] = NetworkAction(name='Reassign to project', f=noop, + karma='neutral', + caution_level='dangerous',) + + actions['change_owner'] = NetworkAction(name='Change owner', f=noop, + karma='neutral', + caution_level='dangerous',) + + actions['contact'] = NetworkAction(name='Send e-mail', f=send_admin_email, + c=check_network_action("CONTACT"),) + + update_actions_rbac(actions) + + return actions +cached_actions = generate_actions() diff --git a/snf-admin-app/synnefo_admin/admin/resources/networks/filters.py b/snf-admin-app/synnefo_admin/admin/resources/networks/filters.py new file mode 100644 index 0000000000000000000000000000000000000000..8f36c934cc2fcaa295c6dfacdf4180f171333877 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/resources/networks/filters.py @@ -0,0 +1,78 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +import logging + +from synnefo.db.models import Network +import django_filters + +from synnefo_admin.admin.queries_common import (query, model_filter, + get_model_field) + + +@model_filter +def filter_network(queryset, queries): + q = query("network", queries) + return queryset.filter(q) + + +@model_filter +def filter_user(queryset, queries): + q = query("user", queries) + ids = get_model_field("user", q, 'uuid') + return queryset.filter(userid__in=ids) + + +@model_filter +def filter_vm(queryset, queries): + q = query("vm", queries) + ids = get_model_field("vm", q, 'id') + return queryset.filter(machines__id__in=ids) + + +@model_filter +def filter_ip(queryset, queries): + q = query("ip", queries) + ids = get_model_field("ip", q, 'nic__network__id') + return queryset.filter(id__in=ids) + + +@model_filter +def filter_project(queryset, queries): + q = query("project", queries) + ids = get_model_field("project", q, 'uuid') + return queryset.filter(project__in=ids) + + +class NetworkFilterSet(django_filters.FilterSet): + + """A collection of filters for networks. + + This filter collection is based on django-filter's FilterSet. + """ + + net = django_filters.CharFilter(label='Network', action=filter_network) + user = django_filters.CharFilter(label='OF User', action=filter_user) + vm = django_filters.CharFilter(label='HAS VM', action=filter_vm) + ip = django_filters.CharFilter(label='HAS IP', action=filter_ip) + proj = django_filters.CharFilter(label='OF Project', action=filter_project) + state = django_filters.MultipleChoiceFilter( + label='Status', name='state', choices=Network.OPER_STATES) + + class Meta: + model = Network + fields = ('net', 'state', 'public', 'drained', 'user', 'vm', 'ip', + 'proj') diff --git a/snf-admin-app/synnefo_admin/admin/resources/networks/utils.py b/snf-admin-app/synnefo_admin/admin/resources/networks/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..f58f23be9db81335d3d1672b2c3b658a7991b152 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/resources/networks/utils.py @@ -0,0 +1,55 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +import logging + +from django.core.exceptions import ObjectDoesNotExist + +from astakos.im.models import AstakosUser +from synnefo.db.models import Network + +from synnefo_admin.admin.exceptions import AdminHttp404 +from synnefo_admin.admin.utils import create_details_href + + +def get_network_or_404(query): + try: + return Network.objects.get(pk=int(query)) + except (ObjectDoesNotExist, ValueError): + raise AdminHttp404( + "No Network was found that matches this query: %s\n" % query) + + +def get_contact_email(inst): + if inst.userid: + return AstakosUser.objects.get(uuid=inst.userid).email + else: + return "-" + + +def get_contact_name(inst): + if inst.userid: + return AstakosUser.objects.get(uuid=inst.userid).realname + else: + return "-" + + +def get_user_details_href(inst): + if inst.userid: + user = AstakosUser.objects.get(uuid=inst.userid) + return create_details_href('user', user.realname, user.email) + else: + return "-" diff --git a/snf-admin-app/synnefo_admin/admin/resources/networks/views.py b/snf-admin-app/synnefo_admin/admin/resources/networks/views.py new file mode 100644 index 0000000000000000000000000000000000000000..33173afd4880b6eb9ceda10c3bb7282ec1646ff4 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/resources/networks/views.py @@ -0,0 +1,204 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +import logging +from collections import OrderedDict + +from django.core.urlresolvers import reverse +from django.utils.html import escape + +from synnefo.db.models import (Network, NetworkInterface, IPAddress, + IPAddressLog) +from astakos.im.models import AstakosUser, Project + +from synnefo_admin.admin.actions import (has_permission_or_403, + get_allowed_actions, + get_permitted_actions,) +from synnefo_admin.admin.resources.users.utils import get_user_or_404 +from synnefo_admin.admin.tables import AdminJSONView +from synnefo_admin.admin.associations import ( + UserAssociation, QuotaAssociation, VMAssociation, VolumeAssociation, + NetworkAssociation, NicAssociation, IPAssociation, IPLogAssociation, + ProjectAssociation) + +from .filters import NetworkFilterSet +from .actions import cached_actions +from .utils import (get_contact_name, get_contact_email, get_network_or_404, + get_user_details_href) + + +templates = { + 'list': 'admin/network_list.html', + 'details': 'admin/network_details.html', +} + + +class NetworkJSONView(AdminJSONView): + model = Network + fields = ('pk', 'name', 'state', 'public', 'drained',) + filters = NetworkFilterSet + + def format_data_row(self, row): + if not row[1]: + row = list(row) + row[1] = "(not set)" + return row + + def get_extra_data(self, qs): + # FIXME: The `contact_name`, `contact_email` fields will cripple our db + if self.form.cleaned_data['iDisplayLength'] < 0: + qs = qs.only('pk', 'name', 'state', 'public', 'drained', 'userid', + 'deleted') + return [self.get_extra_data_row(row) for row in qs] + + def get_extra_data_row(self, inst): + if self.dt_data['iDisplayLength'] < 0: + extra_dict = {} + else: + extra_dict = OrderedDict() + + extra_dict['allowed_actions'] = { + 'display_name': "", + 'value': get_allowed_actions(cached_actions, inst, + self.request.user), + 'visible': False, + } + extra_dict['id'] = { + 'display_name': "ID", + 'value': inst.pk, + 'visible': False, + } + extra_dict['item_name'] = { + 'display_name': "Name", + 'value': escape(inst.name), + 'visible': False, + } + extra_dict['details_url'] = { + 'display_name': "Details", + 'value': reverse('admin-details', args=['network', inst.pk]), + 'visible': True, + } + extra_dict['contact_id'] = { + 'display_name': "Contact ID", + 'value': inst.userid, + 'visible': False, + } + extra_dict['contact_email'] = { + 'display_name': "Contact email", + 'value': escape(get_contact_email(inst)), + 'visible': False, + } + extra_dict['contact_name'] = { + 'display_name': "Contact name", + 'value': escape(get_contact_name(inst)), + 'visible': False, + } + + extra_dict['user_info'] = { + 'display_name': "User", + 'value': get_user_details_href(inst), + 'visible': True, + } + + if self.form.cleaned_data['iDisplayLength'] < 0: + extra_dict['minimal'] = { + 'display_name': "No summary available", + 'value': "Have you per chance pressed 'Select All'?", + 'visible': True, + } + else: + extra_dict.update(self.add_verbose_data(inst)) + + return extra_dict + + def add_verbose_data(self, inst): + extra_dict = OrderedDict() + extra_dict['public'] = { + 'display_name': "Public", + 'value': inst.public, + 'visible': True, + } + extra_dict['updated'] = { + 'display_name': "Update time", + 'value': inst.updated.strftime("%Y-%m-%d %H:%M"), + 'visible': True, + } + + return extra_dict + + +JSON_CLASS = NetworkJSONView + + +@has_permission_or_403(cached_actions) +def do_action(request, op, id): + """Apply the requested action on the specified network.""" + if op == "contact": + user = get_user_or_404(id) + else: + network = Network.objects.get(pk=id) + actions = get_permitted_actions(cached_actions, request.user) + + if op == 'contact': + actions[op].apply(user, request) + else: + actions[op].apply(network) + + +def catalog(request): + """List view for Cyclades networks.""" + context = {} + context['action_dict'] = get_permitted_actions(cached_actions, + request.user) + context['filter_dict'] = NetworkFilterSet().filters.values() + context['columns'] = ["ID", "Name", "Status", "Public", + "Drained", ""] + context['item_type'] = 'network' + + return context + + +def details(request, query): + """Details view for Astakos users.""" + network = get_network_or_404(query) + associations = [] + + vm_list = network.machines.all() + associations.append(VMAssociation(request, vm_list,)) + + nic_list = NetworkInterface.objects.filter(network=network) + associations.append(NicAssociation(request, nic_list,)) + + ip_list = IPAddress.objects.filter(network=network) + associations.append(IPAssociation(request, ip_list,)) + + user_list = AstakosUser.objects.filter(uuid=network.userid) + associations.append(UserAssociation(request, user_list,)) + + project_list = Project.objects.filter(uuid=network.project) + associations.append(ProjectAssociation(request, project_list,)) + + ip_log_list = IPAddressLog.objects.filter(network_id=network.pk) + associations.append(IPLogAssociation(request, ip_log_list)) + + context = { + 'main_item': network, + 'main_type': 'network', + 'action_dict': get_permitted_actions(cached_actions, request.user), + 'associations_list': associations, + } + + return context diff --git a/snf-admin-app/synnefo_admin/admin/resources/projects/__init__.py b/snf-admin-app/synnefo_admin/admin/resources/projects/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/snf-admin-app/synnefo_admin/admin/resources/projects/actions.py b/snf-admin-app/synnefo_admin/admin/resources/projects/actions.py new file mode 100644 index 0000000000000000000000000000000000000000..ae576ec74a55f7df2e49950a5a0f035b0904b6c7 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/resources/projects/actions.py @@ -0,0 +1,115 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +import logging +from collections import OrderedDict + +from synnefo_admin.admin.actions import AdminAction +from astakos.im import functions as pactions + +from synnefo_admin.admin.utils import update_actions_rbac, send_admin_email + + +class ProjectAction(AdminAction): + + """Class for actions on projects. Derived from AdminAction. + + Pre-determined Attributes: + target: project + """ + + def __init__(self, name, f, **kwargs): + """Initialize the class with provided values.""" + AdminAction.__init__(self, name=name, target='project', f=f, **kwargs) + + +def do_project_action(action): + + if action == 'approve': + return lambda p: pactions.approve_application(p.last_application.id) + elif action == 'deny': + return lambda p: pactions.deny_application(p.last_application.id) + else: + # The action name should be the same as the function name in + # astakos.im.functions. + func = getattr(pactions, action) + return lambda p: func(p.id) + + +def check_project_action(action): + def check(p, action): + res, _ = pactions.validate_project_action(p, action) + return res + + return lambda p: check(p, action) + + +def check_approve(project): + if project.is_base: + return False + return project.last_application.can_approve() + + +def check_deny(project): + if project.is_base: + return False + return project.last_application.can_deny() + + +def generate_actions(): + """Create a list of actions on projects. + + The actions are: approve/deny, suspend/unsuspend, terminate/reinstate, + contact + """ + actions = OrderedDict() + + actions['approve'] = ProjectAction(name='Approve', + f=do_project_action("approve"), + c=check_approve, karma='good',) + + actions['deny'] = ProjectAction(name='Deny', + f=do_project_action("deny"), c=check_deny, + karma='bad', caution_level='warning',) + + actions['suspend'] = ProjectAction(name='Suspend', + f=do_project_action("suspend"), + c=check_project_action("SUSPEND"), + karma='bad', caution_level='warning',) + + actions['unsuspend'] = ProjectAction(name='Unsuspend', + f=do_project_action("unsuspend"), + c=check_project_action("UNSUSPEND"), + karma='good', caution_level='warning') + + actions['terminate'] = ProjectAction(name='Terminate', + f=do_project_action("terminate"), + c=check_project_action("TERMINATE"), + karma='bad', + caution_level='dangerous',) + + actions['reinstate'] = ProjectAction(name='Reinstate', + f=do_project_action("reinstate"), + c=check_project_action("REINSTATE"), + karma='good', + caution_level='warning',) + + actions['contact'] = ProjectAction(name='Send e-mail', f=send_admin_email,) + + update_actions_rbac(actions) + + return actions +cached_actions = generate_actions() diff --git a/snf-admin-app/synnefo_admin/admin/resources/projects/filters.py b/snf-admin-app/synnefo_admin/admin/resources/projects/filters.py new file mode 100644 index 0000000000000000000000000000000000000000..5c2302cdc148b81d46aba64ac2f4475d96979cc8 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/resources/projects/filters.py @@ -0,0 +1,140 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import django_filters + +from django.db.models import Q + +from astakos.im.models import Project, ProjectApplication +from synnefo_admin.admin.queries_common import (query, model_filter, + get_model_field) + + +@model_filter +def filter_project(queryset, queries): + q = query("project", queries) + return queryset.filter(q) + + +@model_filter +def filter_user(queryset, queries): + q = query("user", queries) + ids = get_model_field("user", q, 'uuid') + qor = Q(members__uuid__in=ids) | Q(owner__uuid__in=ids) + # BIG FAT FIXME: The below two lines in theory should not be necessary, but + # if they don't exist, the queryset will produce weird results with the + # addition of values list. + qs = queryset.select_related("owner__uuid").filter(qor) + len(qs) + return qs + + +@model_filter +def filter_vm(queryset, queries): + q = query("vm", queries) + ids = get_model_field("vm", q, 'project') + return queryset.filter(uuid__in=ids) + + +@model_filter +def filter_volume(queryset, queries): + q = query("volume", queries) + ids = get_model_field("volume", q, 'project') + return queryset.filter(uuid__in=ids) + + +@model_filter +def filter_network(queryset, queries): + q = query("network", queries) + ids = get_model_field("network", q, 'project') + return queryset.filter(uuid__in=ids) + + +@model_filter +def filter_ip(queryset, queries): + q = query("ip", queries) + ids = get_model_field("ip", q, 'project') + return queryset.filter(uuid__in=ids) + + +def get_project_status_choices(): + """Get all possible project statuses from Project model.""" + project_states = Project.O_STATE_DISPLAY.itervalues() + return [(value.upper(), '_') for value in project_states] +project_status_choices = get_project_status_choices() + + +def get_application_status_choices(): + """Get all possible application statuses from ProjectApplication model.""" + app_states = ProjectApplication.APPLICATION_STATE_DISPLAY.itervalues() + # There is a status with the name "Pending review". We only want to keep + # the "Pending" part. + return [(value.split()[0].upper(), '_') for value in app_states] +application_status_choices = get_application_status_choices() + + +def filter_project_status(queryset, choices): + """Filter project status.""" + choices = choices or () + if not choices: + return queryset + if len(choices) == len(project_status_choices): + return queryset + q = Q() + for c in choices: + status = getattr(Project, 'O_%s' % c.upper()) + q |= Q(state=status) + return queryset.filter(q).distinct() + + +def filter_application_status(queryset, choices): + """Filter application status.""" + choices = choices or () + if not choices: + return queryset + if len(choices) == len(application_status_choices): + return queryset + q = Q() + for c in choices: + status = getattr(ProjectApplication, '%s' % c.upper()) + q |= Q(last_application__state=status) + return queryset.filter(q).distinct() + + +class ProjectFilterSet(django_filters.FilterSet): + + """A collection of filters for Projects. + + This filter collection is based on django-filter's FilterSet. + """ + + proj = django_filters.CharFilter(label='Project', action=filter_project) + user = django_filters.CharFilter(label='OF User', action=filter_user) + vm = django_filters.CharFilter(label='HAS VM', action=filter_vm) + vol = django_filters.CharFilter(label='HAS Volume', action=filter_volume) + net = django_filters.CharFilter(label='HAS Network', action=filter_network) + ip = django_filters.CharFilter(label='HAS IP', action=filter_ip) + project_status = django_filters.MultipleChoiceFilter( + label='Project Status', action=filter_project_status, + choices=project_status_choices) + application_status = django_filters.MultipleChoiceFilter( + label='Application Status', action=filter_application_status, + choices=application_status_choices) + is_base = django_filters.BooleanFilter(label='System') + + class Meta: + model = Project + fields = ('proj', 'project_status', 'application_status', 'is_base', + 'user', 'vm', 'vol', 'net', 'ip',) diff --git a/snf-admin-app/synnefo_admin/admin/resources/projects/utils.py b/snf-admin-app/synnefo_admin/admin/resources/projects/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..841f7b047d76296383dd6af54e1b34022d53e238 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/resources/projects/utils.py @@ -0,0 +1,143 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +import logging + +from django.core.exceptions import ObjectDoesNotExist + +from astakos.im.models import Project +from astakos.im.quotas import get_project_quota + +from synnefo.util import units +from synnefo_admin.admin.exceptions import AdminHttp404 +from synnefo_admin.admin.utils import is_resource_useful + + +def get_actual_owner(inst): + if inst.owner: + return inst.owner + try: + return inst.members.all()[0] + except IndexError: + return None + + +def get_project_or_404(query): + # Get by UUID + try: + return Project.objects.get(uuid=query) + except ObjectDoesNotExist: + pass + + # Get by ID + try: + return Project.objects.get(id=query) + except (ObjectDoesNotExist, ValueError): + raise AdminHttp404( + "No Project was found that matches this query: %s\n" % query) + + +def get_contact_email(inst): + owner = get_actual_owner(inst) + if owner: + return owner.email + + +def get_contact_name(inst): + owner = get_actual_owner(inst) + if owner: + return owner.realname + + +def get_contact_id(inst): + owner = get_actual_owner(inst) + if owner: + return owner.uuid + + +def get_policies(inst): + policies = inst.projectresourcequota_set.all().prefetch_related('resource') + policy_list = [] + + for p in policies: + r = p.resource + if not is_resource_useful(r, p.project_capacity): + continue + policy_list.append(p) + + return policy_list + + +def get_project_usage(inst): + """Return requested project quota type. + + Accepted stats are: 'project_limit', 'project_pending', 'project_usage'. + Note that the output is sanitized, meaning that stats that correspond + to infinite or zero limits will not be returned. + """ + resource_list = [] + quota_dict = get_project_quota(inst) + if not quota_dict: + return [] + + policies = get_policies(inst) + for p in policies: + r = p.resource + value = units.show(quota_dict[r.name]['project_usage'], r.unit) + resource_list.append((r.report_desc, value)) + + return resource_list + + +def get_project_quota_category(inst, category): + """Get the quota for project member""" + resource_list = [] + policies = get_policies(inst) + + for p in policies: + r = p.resource + # Get human-readable (resource name, member capacity) tuple + if category == "member": + resource_list.append((r.report_desc, p.display_member_capacity())) + elif category == "limit": + resource_list.append((r.report_desc, p.display_project_capacity())) + + return resource_list + + +def display_quota_horizontally(resource_list): + """Display resource lists in one line.""" + if not resource_list: + return "-" + return ', '.join((': '.join(pair) for pair in resource_list)) + + +def display_project_usage_horizontally(inst): + """Display the requested project stats in a one-line string.""" + resource_list = get_project_usage(inst) + return display_quota_horizontally(resource_list) + + +def display_member_quota_horizontally(inst): + """Display project resources (member or total) in one line.""" + resource_list = get_project_quota_category(inst, "member") + return display_quota_horizontally(resource_list) + + +def display_project_limit_horizontally(inst): + """Display project resources (member or total) in one line.""" + resource_list = get_project_quota_category(inst, "limit") + return display_quota_horizontally(resource_list) diff --git a/snf-admin-app/synnefo_admin/admin/resources/projects/views.py b/snf-admin-app/synnefo_admin/admin/resources/projects/views.py new file mode 100644 index 0000000000000000000000000000000000000000..8cd874fd32138b54fcda195baa5ccb418b28cefc --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/resources/projects/views.py @@ -0,0 +1,262 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +from collections import OrderedDict + +from django.core.urlresolvers import reverse +from django.utils.html import escape + +from synnefo.db.models import (VirtualMachine, Network, Volume, + IPAddress) +from astakos.im.models import AstakosUser, Project +from astakos.im import transaction + +from synnefo_admin import admin_settings +from synnefo_admin.admin.actions import (has_permission_or_403, + get_allowed_actions, + get_permitted_actions,) +from synnefo_admin.admin.resources.users.utils import get_user_or_404 +from synnefo_admin.admin.tables import AdminJSONView +from synnefo_admin.admin.associations import ( + UserAssociation, QuotaAssociation, VMAssociation, VolumeAssociation, + NetworkAssociation, NicAssociation, IPAssociation, IPLogAssociation, + ProjectAssociation) + +from .filters import ProjectFilterSet +from .actions import cached_actions +from .utils import (get_contact_id, get_contact_name, get_contact_email, + get_project_or_404, display_project_usage_horizontally, + display_member_quota_horizontally, + display_project_limit_horizontally) + + +templates = { + 'list': 'admin/project_list.html', + 'details': 'admin/project_details.html', +} + + +class ProjectJSONView(AdminJSONView): + model = Project + fields = ('id', 'realname', '{owner__first_name} {owner__last_name}', + 'state', 'last_application__state', 'creation_date', 'end_date') + filters = ProjectFilterSet + + def format_data_row(self, row): + if self.dt_data['iDisplayLength'] > 0: + row = list(row) + if row[2] == "None None": + row[2] = "(not set)" + + project = Project.objects.get(id=row[0]) + row[3] = (str(row[3]) + ' (' + project.state_display() + ')') + + app = Project.objects.get(id=row[0]).last_application + if app: + row[4] = (str(row[4]) + ' (' + app.state_display() + ')') + + row[5] = str(row[5].date()) + row[6] = str(row[6].date()) + return row + + def get_extra_data(self, qs): + if self.form.cleaned_data['iDisplayLength'] < 0: + qs = qs.only('id', 'realname', 'state', 'is_base', + 'uuid',).select_related('last_application__state', + 'last_application__id', + 'owner__id', 'owner__uuid') + return [self.get_extra_data_row(row) for row in qs] + + def get_extra_data_row(self, inst): + if self.dt_data['iDisplayLength'] < 0: + extra_dict = {} + else: + extra_dict = OrderedDict() + + extra_dict['allowed_actions'] = { + 'display_name': "", + 'value': get_allowed_actions(cached_actions, inst, + self.request.user), + 'visible': False, + } + extra_dict['id'] = { + 'display_name': "ID", + 'value': inst.id, + 'visible': False, + } + extra_dict['item_name'] = { + 'display_name': "Name", + 'value': escape(inst.realname), + 'visible': False, + } + extra_dict['details_url'] = { + 'display_name': "Details", + 'value': reverse('admin-details', args=['project', inst.id]), + 'visible': True, + } + extra_dict['contact_id'] = { + 'display_name': "Contact ID", + 'value': get_contact_id(inst), + 'visible': False, + } + extra_dict['contact_email'] = { + 'display_name': "Contact email", + 'value': escape(get_contact_email(inst)), + 'visible': False, + } + extra_dict['contact_name'] = { + 'display_name': "Contact name", + 'value': escape(get_contact_name(inst)), + 'visible': False, + } + + if self.form.cleaned_data['iDisplayLength'] < 0: + extra_dict['minimal'] = { + 'display_name': "No summary available", + 'value': "Have you per chance pressed 'Select All'?", + 'visible': True, + } + else: + extra_dict.update(self.add_verbose_data(inst)) + + return extra_dict + + def add_verbose_data(self, inst): + extra_dict = OrderedDict() + extra_dict['uuid'] = { + 'display_name': "UUID", + 'value': inst.uuid, + 'visible': True, + } + + if not inst.is_base: + extra_dict['homepage'] = { + 'display_name': "Homepage url", + 'value': escape(inst.homepage) or "(not set)", + 'visible': True, + } + + extra_dict['description'] = { + 'display_name': "Description", + 'value': escape(inst.description) or "(not set)", + 'visible': True, + } + extra_dict['members'] = { + 'display_name': "Members", + 'value': (str(inst.members_count()) + ' / ' + + str(inst.limit_on_members_number)), + 'visible': True, + } + + if inst.last_application.comments: + extra_dict['comments'] = { + 'display_name': "Comments for review", + 'value': escape(inst.last_application.comments) or "(not set)", + 'visible': True, + } + + extra_dict['member_resources'] = { + 'display_name': "Max resources per member", + 'value': display_member_quota_horizontally(inst), + 'visible': True + } + + extra_dict['limit'] = { + 'display_name': "Total resources", + 'value': display_project_limit_horizontally(inst), + 'visible': True, + } + extra_dict['usage'] = { + 'display_name': "Resource usage", + 'value': display_project_usage_horizontally(inst), + 'visible': True, + } + + return extra_dict + + +JSON_CLASS = ProjectJSONView + + +@has_permission_or_403(cached_actions) +@transaction.commit_on_success +def do_action(request, op, id): + """Apply the requested action on the specified user.""" + if op == "contact": + user = get_user_or_404(id) + else: + project = get_project_or_404(id) + actions = get_permitted_actions(cached_actions, request.user) + + if op == 'contact': + actions[op].apply(user, request) + else: + actions[op].apply(project) + + +def catalog(request): + """List view for Cyclades projects.""" + context = {} + context['action_dict'] = get_permitted_actions(cached_actions, + request.user) + context['filter_dict'] = ProjectFilterSet().filters.values() + context['columns'] = ["ID", "Name", "Owner Name", "Project Status", + "Application Status", "Creation date", "End date", + ""] + context['item_type'] = 'project' + + return context + + +def custom_user_association(request, project): + """Return either all associated project members or only the active ones.""" + if admin_settings.ADMIN_SHOW_ONLY_ACTIVE_PROJECT_MEMBERS: + total = project.members.all().count() + user_ids = project.projectmembership_set.actually_accepted().\ + values("person__uuid") + user_list = AstakosUser.objects.filter(uuid__in=user_ids) + return UserAssociation(request, user_list, total=total) + else: + return UserAssociation(request, project.members.all()) + + +def details(request, query): + """Details view for Astakos projects.""" + project = get_project_or_404(query) + associations = [] + + associations.append(custom_user_association(request, project)) + + vm_list = VirtualMachine.objects.filter(project=project.uuid) + associations.append(VMAssociation(request, vm_list,)) + + volume_list = Volume.objects.filter(project=project.uuid) + associations.append(VolumeAssociation(request, volume_list,)) + + network_list = Network.objects.filter(project=project.uuid) + associations.append(NetworkAssociation(request, network_list,)) + + ip_list = IPAddress.objects.filter(project=project.uuid) + associations.append(IPAssociation(request, ip_list,)) + + context = { + 'main_item': project, + 'main_type': 'project', + 'action_dict': get_permitted_actions(cached_actions, request.user), + 'associations_list': associations, + } + + return context diff --git a/snf-admin-app/synnefo_admin/admin/resources/users/__init__.py b/snf-admin-app/synnefo_admin/admin/resources/users/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/snf-admin-app/synnefo_admin/admin/resources/users/actions.py b/snf-admin-app/synnefo_admin/admin/resources/users/actions.py new file mode 100644 index 0000000000000000000000000000000000000000..7ffc276637e54ba3cd4f6e17e94b06d7485a9308 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/resources/users/actions.py @@ -0,0 +1,88 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +import logging +from collections import OrderedDict + +from astakos.im import user_logic as users + +from synnefo_admin.admin.actions import AdminAction +from synnefo_admin.admin.utils import update_actions_rbac, send_admin_email + + +class UserAction(AdminAction): + + """Class for actions on users. Derived from AdminAction. + + Pre-determined Attributes: + target: user + """ + + def __init__(self, name, f, **kwargs): + """Initialize the class with provided values.""" + AdminAction.__init__(self, name=name, target='user', f=f, **kwargs) + + +def check_user_action(action): + def check(u, action): + res, _ = users.validate_user_action( + u, action, verification_code=u.verification_code) + return res + + return lambda u: check(u, action) + + +def verify(user): + return users.verify(user, verification_code=user.verification_code) + + +def generate_actions(): + """Create a list of actions on users. + + The actions are: activate/deactivate, accept/reject, verify, contact. + """ + actions = OrderedDict() + + actions['activate'] = UserAction(name='Activate', f=users.activate, + c=check_user_action("ACTIVATE"), + karma='good',) + + actions['deactivate'] = UserAction(name='Deactivate', f=users.deactivate, + c=check_user_action("DEACTIVATE"), + karma='bad', caution_level='warning',) + + actions['accept'] = UserAction(name='Accept', f=users.accept, + c=check_user_action("ACCEPT"), + karma='good',) + + actions['reject'] = UserAction(name='Reject', f=users.reject, + c=check_user_action("REJECT"), + karma='bad', caution_level='dangerous',) + + actions['verify'] = UserAction(name='Verify', f=verify, + c=check_user_action("VERIFY"), + karma='good',) + + actions['resend_verification'] = UserAction( + name='Resend verification', f=users.send_verification_mail, + karma='good', c=check_user_action("SEND_VERIFICATION_MAIL"),) + + actions['contact'] = UserAction(name='Send e-mail', f=send_admin_email,) + + update_actions_rbac(actions) + + return actions +cached_actions = generate_actions() diff --git a/snf-admin-app/synnefo_admin/admin/resources/users/filters.py b/snf-admin-app/synnefo_admin/admin/resources/users/filters.py new file mode 100644 index 0000000000000000000000000000000000000000..493d49ee43fa87ae89a714f25c39fcd6a0dd1b7e --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/resources/users/filters.py @@ -0,0 +1,161 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import logging +import django_filters + +from django.db.models import Q + +from astakos.im.models import AstakosUser, Project +from astakos.im import auth_providers + +from synnefo_admin.admin.queries_common import (query, model_filter, + get_model_field) + +from .utils import get_groups + +choice2query = { + ('ACTIVE', ''): Q(is_active=True), + ('INACTIVE', ''): Q(is_active=False, moderated=True) | Q(is_rejected=True), + ('PENDING MODERATION', ''): Q(moderated=False, email_verified=True), + ('PENDING EMAIL VERIFICATION', ''): Q(email_verified=False), +} + + +auth_providers = [(key, '_') for key in auth_providers.PROVIDERS.iterkeys()] + + +@model_filter +def filter_user(queryset, queries): + q = query("user", queries) + return queryset.filter(q) + + +@model_filter +def filter_vm(queryset, queries): + q = query("vm", queries) + ids = get_model_field("vm", q, 'userid') + return queryset.filter(uuid__in=ids) + + +@model_filter +def filter_volume(queryset, queries): + q = query("volume", queries) + ids = get_model_field("volume", q, 'userid') + return queryset.filter(uuid__in=ids) + + +@model_filter +def filter_network(queryset, queries): + q = query("network", queries) + ids = get_model_field("network", q, 'userid') + return queryset.filter(uuid__in=ids) + + +@model_filter +def filter_ip(queryset, queries): + q = query("ip", queries) + ids = get_model_field("ip", q, 'userid') + return queryset.filter(uuid__in=ids) + + +@model_filter +def filter_project(queryset, queries): + q = query("project", queries) + member_ids = Project.objects.filter(q).values('members__uuid') + owner_ids = Project.objects.filter(q).values('owner__uuid') + qor = Q(uuid__in=member_ids) | Q(uuid__in=owner_ids) + return queryset.filter(qor) + + +def filter_has_auth_providers(queryset, choices): + if not choices: + return queryset + + q = Q() + for c in choices: + q |= Q(auth_providers__module=c) + return queryset.filter(q).distinct() + + +def filter_has_not_auth_providers(queryset, choices): + if not choices: + return queryset + + q = Q() + for c in choices: + q |= Q(auth_providers__module=c) + + # We cannot use `exclude` here as `exclude` does not play nicely with + # multi-valued fields (see https://code.djangoproject.com/ticket/14645) + # + # Instead, we create a subquery to filter all users that actually match the + # requested providers and then exclude them. Given that the subquery is in + # the same database, it probably has small overhead. + user_ids = AstakosUser.objects.filter(q).values('id') + return queryset.exclude(id__in=user_ids).distinct() + + +def filter_status(queryset, choices): + choices = choices or () + if len(choices) == len(choice2query.keys()): + return queryset + q = Q() + for c in choices: + q |= choice2query[(c, '')] + return queryset.filter(q).distinct() + + +def filter_group(queryset, choices): + """Filter by group name for user. + + Since not all users need to be in a group, we always process the request + given even if all choices are selected. + """ + choices = choices or () + q = Q() + for c in choices: + q |= Q(groups__name__exact=c) + return queryset.filter(q).distinct() + + +class UserFilterSet(django_filters.FilterSet): + + """A collection of filters for users. + + This filter collection is based on django-filter's FilterSet. + """ + + user = django_filters.CharFilter(label='User', action=filter_user) + vm = django_filters.CharFilter(label='HAS VM', action=filter_vm) + vol = django_filters.CharFilter(label='HAS Volume', action=filter_volume) + net = django_filters.CharFilter(label='HAS Network', action=filter_network) + ip = django_filters.CharFilter(label='HAS IP', action=filter_ip) + proj = django_filters.CharFilter(label='IN Project', action=filter_project) + status = django_filters.MultipleChoiceFilter( + label='Status', action=filter_status, choices=choice2query.keys()) + groups = django_filters.MultipleChoiceFilter( + label='Group', action=filter_group, choices=get_groups()) + has_auth_providers = django_filters.MultipleChoiceFilter( + label='HAS Auth Providers', action=filter_has_auth_providers, + choices=auth_providers) + has_not_auth_providers = django_filters.MultipleChoiceFilter( + label='HAS NOT Auth Providers', action=filter_has_not_auth_providers, + choices=auth_providers) + + class Meta: + model = AstakosUser + fields = ('user', 'status', 'groups', 'has_auth_providers', + 'has_not_auth_providers', 'vm', 'vol', 'net', 'ip', 'proj') diff --git a/snf-admin-app/synnefo_admin/admin/resources/users/utils.py b/snf-admin-app/synnefo_admin/admin/resources/users/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..aff5ae39aae1d94989541ac7fb1a5b6c229af252 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/resources/users/utils.py @@ -0,0 +1,133 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import logging + +from django.core.exceptions import ObjectDoesNotExist +from django.contrib.auth.models import Group + +from synnefo.db.models import VirtualMachine +from astakos.im.models import AstakosUser, Project + +from astakos.im.quotas import get_user_quotas + +from synnefo.util import units + +from synnefo_admin import admin_settings +from synnefo_admin.admin.exceptions import AdminHttp404 +from synnefo_admin.admin.utils import (get_resource, is_resource_useful, + create_details_href) + + +def get_groups(): + groups = Group.objects.all().values('name') + return [(group['name'], '') for group in groups] + + +def get_user_or_404(query): + """Get AstakosUser from query. + + The query can either be a user email, UUID or ID. + """ + # Get by UUID + try: + return AstakosUser.objects.get(uuid=query) + except ObjectDoesNotExist: + pass + + # Get by Email + try: + return AstakosUser.objects.get(email=query) + except ObjectDoesNotExist: + pass + + # Get by ID + try: + return AstakosUser.objects.get(id=int(query)) + except (ObjectDoesNotExist, ValueError): + raise AdminHttp404( + "No User was found that matches this query: %s\n" % query) + + +def get_quotas(user): + """Transform the resource usage dictionary of a user. + + Return a list of dictionaries that represent the quotas of the user. Each + dictionary has the following form: + + { + 'project': <Project instance>, + 'resources': [('Resource Name1', <Resource dict>), + ('Resource Name2', <Resource dict>),...] + } + + where 'Resource Name' is the name of the resource and <Resource dict> is + the dictionary that is returned by list_user_quotas and has the following + fields: + + pending, project_pending, project_limit, project_usage, usage. + + Note, the get_quota_usage function returns many dicts, but we only keep the + ones that have limit > 0 + """ + usage = get_user_quotas(user) + + quotas = [] + for project_id, resource_dict in usage.iteritems(): + source = {} + source['project'] = Project.objects.get(uuid=project_id) + q_res = source['resources'] = [] + + for resource_name, resource in resource_dict.iteritems(): + # Chech if the resource is useful to display + project_limit = resource['project_limit'] + r = get_resource(resource_name) + if not is_resource_useful(r, project_limit): + continue + + usage = units.show(resource['usage'], r.unit) + limit = units.show(resource['limit'], r.unit) + q_res.append((r.report_desc, usage, limit,)) + + quotas.append(source) + + return quotas + + +def get_enabled_providers(user): + """Get a comma-seperated string with the user's enabled providers.""" + ep = [prov.module for prov in user.get_enabled_auth_providers()] + return ", ".join(ep) + + +def get_user_groups(user): + groups = ', '.join([g.name for g in user.groups.all()]) + if groups == '': + return 'None' + return groups + + +def get_suspended_vms(user): + limit = admin_settings.ADMIN_LIMIT_SUSPENDED_VMS_IN_SUMMARY + vms = VirtualMachine.objects.filter(userid=user.uuid, suspended=True).\ + order_by('-id') + count = vms.count() + if count == 0: + return 'None' + + urls = [create_details_href('vm', vm.name, vm.pk) for vm in vms[:limit]] + if count > limit: + urls.append('...') + return ', '.join(urls) diff --git a/snf-admin-app/synnefo_admin/admin/resources/users/views.py b/snf-admin-app/synnefo_admin/admin/resources/users/views.py new file mode 100644 index 0000000000000000000000000000000000000000..5b025fc042d06ef910561748f0e5cbdf156e0e3f --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/resources/users/views.py @@ -0,0 +1,235 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import logging +from collections import OrderedDict + +from django.core.urlresolvers import reverse +from django.utils.html import escape + +from synnefo.db.models import (VirtualMachine, Network, IPAddressLog, Volume, + NetworkInterface, IPAddress) +from astakos.im.models import AstakosUser, Project +from astakos.im import user_logic as users +from astakos.im import transaction + +from django.db.models import Q + +from synnefo_admin import admin_settings +from synnefo_admin.admin.actions import (has_permission_or_403, + get_allowed_actions, + get_permitted_actions,) +from synnefo_admin.admin.tables import AdminJSONView +from synnefo_admin.admin.associations import ( + UserAssociation, QuotaAssociation, VMAssociation, VolumeAssociation, + NetworkAssociation, NicAssociation, IPAssociation, IPLogAssociation, + ProjectAssociation) + +from .utils import (get_user_or_404, get_quotas, get_user_groups, + get_enabled_providers, get_suspended_vms) +from .actions import cached_actions +from .filters import UserFilterSet + +templates = { + 'list': 'admin/user_list.html', + 'details': 'admin/user_details.html', +} + + +class UserJSONView(AdminJSONView): + model = AstakosUser + fields = ('id', 'email', 'first_name', 'last_name', 'is_active', + 'is_rejected', 'moderated', 'email_verified') + filters = UserFilterSet + + def get_extra_data(self, qs): + if self.form.cleaned_data['iDisplayLength'] < 0: + qs = qs.only('email', 'first_name', 'last_name', 'is_active', + 'is_rejected', 'moderated', 'email_verified', 'uuid') + return [self.get_extra_data_row(row) for row in qs] + + def get_extra_data_row(self, inst): + if self.dt_data['iDisplayLength'] < 0: + extra_dict = {} + else: + extra_dict = OrderedDict() + + extra_dict['allowed_actions'] = { + 'display_name': "", + 'value': get_allowed_actions(cached_actions, inst, + self.request.user), + 'visible': False, + } + extra_dict['id'] = { + 'display_name': "UUID", + 'value': inst.uuid, + 'visible': True, + } + extra_dict['item_name'] = { + 'display_name': "Name", + 'value': escape(inst.realname), + 'visible': False, + } + extra_dict['details_url'] = { + 'display_name': "Details", + 'value': reverse('admin-details', args=['user', inst.uuid]), + 'visible': True, + } + extra_dict['contact_id'] = { + 'display_name': "Contact ID", + 'value': inst.uuid, + 'visible': False, + } + extra_dict['contact_email'] = { + 'display_name': "Contact email", + 'value': escape(inst.email), + 'visible': False, + } + extra_dict['contact_name'] = { + 'display_name': "Contact name", + 'value': escape(inst.realname), + 'visible': False, + } + + if self.form.cleaned_data['iDisplayLength'] < 0: + extra_dict['minimal'] = { + 'display_name': "No summary available", + 'value': "Have you per chance pressed 'Select All'?", + 'visible': True, + } + else: + extra_dict.update(self.add_verbose_data(inst)) + + return extra_dict + + def add_verbose_data(self, inst): + extra_dict = OrderedDict() + extra_dict['status'] = { + 'display_name': "Status", + 'value': inst.status_display, + 'visible': True, + } + extra_dict['groups'] = { + 'display_name': "Groups", + 'value': escape(get_user_groups(inst)), + 'visible': True, + } + extra_dict['enabled_providers'] = { + 'display_name': "Enabled providers", + 'value': get_enabled_providers(inst), + 'visible': True, + } + + if (users.validate_user_action(inst, "ACCEPT") and + inst.verification_code): + extra_dict['activation_url'] = { + 'display_name': "Activation URL", + 'value': inst.get_activation_url(), + 'visible': True, + } + + if inst.accepted_policy: + extra_dict['moderation_policy'] = { + 'display_name': "Moderation policy", + 'value': inst.accepted_policy, + 'visible': True, + } + + suspended_vms = get_suspended_vms(inst) + + extra_dict['suspended_vms'] = { + 'display_name': "Suspended VMs", + 'value': suspended_vms, + 'visible': True, + } + + return extra_dict + + +JSON_CLASS = UserJSONView + + +@has_permission_or_403(cached_actions) +@transaction.commit_on_success +def do_action(request, op, id): + """Apply the requested action on the specified user.""" + user = get_user_or_404(id) + actions = get_permitted_actions(cached_actions, request.user) + + if op == 'reject': + actions[op].apply(user, 'Rejected by the admin') + elif op == 'contact': + actions[op].apply(user, request) + else: + actions[op].apply(user) + + +def catalog(request): + """List view for Astakos users.""" + + context = {} + context['action_dict'] = get_permitted_actions(cached_actions, + request.user) + context['filter_dict'] = UserFilterSet().filters.values() + context['columns'] = ["ID", "E-mail", "First Name", "Last Name", "Active", + "Rejected", "Moderated", "Verified", ""] + context['item_type'] = 'user' + + return context + + +def details(request, query): + """Details view for Astakos users.""" + user = get_user_or_404(query) + associations = [] + lim = admin_settings.ADMIN_LIMIT_ASSOCIATED_ITEMS_PER_CATEGORY + + quota_list = get_quotas(user) + total = len(quota_list) + quota_list = quota_list[:lim] + associations.append(QuotaAssociation(request, quota_list, total=total)) + + qor = Q(members=user) | Q(last_application__applicant=user) + project_list = Project.objects.filter(qor) + associations.append(ProjectAssociation(request, project_list)) + + vm_list = VirtualMachine.objects.filter(userid=user.uuid) + associations.append(VMAssociation(request, vm_list)) + + volume_list = Volume.objects.filter(userid=user.uuid) + associations.append(VolumeAssociation(request, volume_list)) + + qor = Q(public=True, nics__machine__userid=user.uuid) | Q(userid=user.uuid) + network_list = Network.objects.filter(qor) + associations.append(NetworkAssociation(request, network_list)) + + nic_list = NetworkInterface.objects.filter(userid=user.uuid) + associations.append(NicAssociation(request, nic_list)) + + ip_list = IPAddress.objects.filter(userid=user.uuid) + associations.append(IPAssociation(request, ip_list)) + + vm_ids = VirtualMachine.objects.filter(userid=user.uuid).values('id') + ip_log_list = IPAddressLog.objects.filter(server_id__in=vm_ids) + associations.append(IPLogAssociation(request, ip_log_list)) + + context = { + 'main_item': user, + 'main_type': 'user', + 'action_dict': get_permitted_actions(cached_actions, request.user), + 'associations_list': associations, + } + + return context diff --git a/snf-admin-app/synnefo_admin/admin/resources/vms/__init__.py b/snf-admin-app/synnefo_admin/admin/resources/vms/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/snf-admin-app/synnefo_admin/admin/resources/vms/actions.py b/snf-admin-app/synnefo_admin/admin/resources/vms/actions.py new file mode 100644 index 0000000000000000000000000000000000000000..296ef52fd48d5c3f4a97ceb5effc75b16e8ee9bd --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/resources/vms/actions.py @@ -0,0 +1,145 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import logging +from collections import OrderedDict + +from synnefo.logic import servers as servers_backend +from synnefo.logic.commands import validate_server_action + +from synnefo_admin.admin.actions import (AdminAction, noop) +from synnefo_admin.admin.utils import update_actions_rbac, send_admin_email + + +class VMAction(AdminAction): + + """Class for actions on VMs. Derived from AdminAction. + + Pre-determined Attributes: + target: vm + """ + + def __init__(self, name, f, **kwargs): + """Initialize the class with provided values.""" + AdminAction.__init__(self, name=name, target='vm', f=f, **kwargs) + + +def vm_suspend(vm): + """Suspend a VM.""" + vm.suspended = True + vm.save() + + +def vm_suspend_release(vm): + """Release previous VM suspension.""" + vm.suspended = False + vm.save() + + +def check_vm_action(action): + if action == 'SUSPEND': + return lambda vm: not vm.suspended and not vm.deleted + elif action == 'UNSUSPEND': + return lambda vm: vm.suspended and not vm.deleted + else: + return lambda vm: validate_server_action(vm, action) + + +def generate_actions(): + """Create a list of actions on users. + + The actions are: start/shutdown, restart, destroy, + suspend/release, reassign, contact + """ + actions = OrderedDict() + + actions['start'] = VMAction(name='Start', f=servers_backend.start, + c=check_vm_action('START'), + karma='good',) + + actions['shutdown'] = VMAction(name='Shutdown', f=servers_backend.stop, + c=check_vm_action('STOP'), karma='bad', + caution_level='warning',) + + actions['reboot'] = VMAction(name='Reboot', f=servers_backend.reboot, + c=check_vm_action('REBOOT'), karma='bad', + caution_level='warning',) + + actions['resize'] = VMAction(name='Resize', f=noop, + c=check_vm_action('RESIZE'), karma='neutral', + caution_level='dangerous',) + + actions['destroy'] = VMAction(name='Destroy', f=servers_backend.destroy, + c=check_vm_action('DESTROY'), karma='bad', + caution_level='dangerous',) + + actions['connect'] = VMAction(name='Connect to network', f=noop, + karma='good',) + + actions['disconnect'] = VMAction(name='Disconnect from network', f=noop, + karma='bad',) + + actions['attach'] = VMAction(name='Attach IP', f=noop, + c=check_vm_action('ADDFLOATINGIP'), + karma='good',) + + actions['detach'] = VMAction(name='Detach IP', f=noop, + c=check_vm_action('REMOVEFLOATINGIP'), + karma='bad',) + + actions['suspend'] = VMAction(name='Suspend', f=vm_suspend, + c=check_vm_action('SUSPEND'), + karma='bad', caution_level='warning',) + + actions['unsuspend'] = VMAction(name='Unsuspend', f=vm_suspend_release, + c=check_vm_action('UNSUSPEND'), + karma='good',) + + actions['reassign'] = VMAction(name='Reassign to project', f=noop, + karma='neutral', caution_level='dangerous',) + + actions['change_owner'] = VMAction(name='Change owner', f=noop, + karma='neutral', + caution_level='dangerous',) + + actions['contact'] = VMAction(name='Send e-mail', f=send_admin_email,) + + update_actions_rbac(actions) + + return actions +cached_actions = generate_actions() + + +def get_permitted_actions(user): + actions = cached_actions + for key, action in actions.iteritems(): + if not action.is_user_allowed(user): + actions.pop(key, None) + return actions + + +def get_allowed_actions(inst, user=None): + """Get a list of actions that can apply to an instance.""" + allowed_actions = [] + if user: + actions = get_permitted_actions(user) + else: + actions = cached_actions + + for key, action in actions.iteritems(): + if action.can_apply(inst): + allowed_actions.append(key) + + return allowed_actions diff --git a/snf-admin-app/synnefo_admin/admin/resources/vms/filters.py b/snf-admin-app/synnefo_admin/admin/resources/vms/filters.py new file mode 100644 index 0000000000000000000000000000000000000000..0611772dd9b611aa9adaf65e816f78052345414d --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/resources/vms/filters.py @@ -0,0 +1,91 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import logging +import django_filters + +from synnefo.db.models import VirtualMachine +from synnefo_admin.admin.queries_common import (query, model_filter, + get_model_field) + + +@model_filter +def filter_vm(queryset, queries): + q = query("vm", queries) + return queryset.filter(q) + + +@model_filter +def filter_user(queryset, queries): + q = query("user", queries) + ids = get_model_field("user", q, 'uuid') + return queryset.filter(userid__in=ids) + + +@model_filter +def filter_volume(queryset, queries): + q = query("volume", queries) + ids = get_model_field("volume", q, 'machine__id') + return queryset.filter(id__in=ids) + + +@model_filter +def filter_network(queryset, queries): + q = query("network", queries) + ids = get_model_field("network", q, 'machines__id') + return queryset.filter(id__in=ids) + + +@model_filter +def filter_ip(queryset, queries): + q = query("ip", queries) + ids = get_model_field("ip", q, 'machines__id') + return queryset.filter(id__in=ids) + + +@model_filter +def filter_project(queryset, queries): + q = query("project", queries) + ids = get_model_field("project", q, 'uuid') + return queryset.filter(project__in=ids) + + +def filter_id(field): + def _filter_id(qs, query): + if not query: + return qs + return qs.filter(**{"%s__icontains" % field: int(query)}) + + return _filter_id + + +class VMFilterSet(django_filters.FilterSet): + + """A collection of filters for VMs. + + This filter collection is based on django-filter's FilterSet. + """ + + vm = django_filters.CharFilter(label='VM', action=filter_vm) + user = django_filters.CharFilter(label='OF User', action=filter_user) + vol = django_filters.CharFilter(label='HAS Volume', action=filter_volume) + net = django_filters.CharFilter(label='IN Network', action=filter_network) + proj = django_filters.CharFilter(label='OF Project', action=filter_project) + operstate = django_filters.MultipleChoiceFilter( + label='Status', name='operstate', choices=VirtualMachine.OPER_STATES) + + class Meta: + model = VirtualMachine + fields = ('vm', 'operstate', 'suspended', 'user', 'vol', 'net', 'proj') diff --git a/snf-admin-app/synnefo_admin/admin/resources/vms/utils.py b/snf-admin-app/synnefo_admin/admin/resources/vms/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..c0b698deed310d8bce0916708cbca1b9c869de88 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/resources/vms/utils.py @@ -0,0 +1,43 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import logging + +from django.core.exceptions import ObjectDoesNotExist + +from astakos.im.models import AstakosUser +from synnefo.db.models import VirtualMachine + +from synnefo_admin.admin.exceptions import AdminHttp404 +from synnefo_admin.admin.utils import create_details_href + + +def get_vm_or_404(query): + try: + return VirtualMachine.objects.get(pk=int(query)) + except (ObjectDoesNotExist, ValueError): + raise AdminHttp404( + "No VM was found that matches this query: %s\n" % query) + + +def get_flavor_info(vm): + return ('CPU: x' + str(vm.flavor.cpu) + ', RAM: ' + str(vm.flavor.ram) + + 'MB, Disk size: ' + str(vm.flavor.disk) + 'GB, Disk template: ' + + str(vm.flavor.volume_type.disk_template)) + + +def get_user_details_href(vm): + user = AstakosUser.objects.get(uuid=vm.userid) + return create_details_href('user', user.realname, user.email) diff --git a/snf-admin-app/synnefo_admin/admin/resources/vms/views.py b/snf-admin-app/synnefo_admin/admin/resources/vms/views.py new file mode 100644 index 0000000000000000000000000000000000000000..a73faf5c067b3914eeff79c8d54239beb09c92ec --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/resources/vms/views.py @@ -0,0 +1,233 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import logging +from collections import OrderedDict +import time + + +from django.core.urlresolvers import reverse +from django.utils.html import escape + +from synnefo.db.models import (VirtualMachine, Network, IPAddressLog, + IPAddress) +from astakos.im.models import AstakosUser, Project + +from synnefo_admin.admin.actions import (has_permission_or_403, + get_allowed_actions, + get_permitted_actions,) +from synnefo_admin.admin.resources.users.utils import get_user_or_404 +from synnefo_admin.admin.tables import AdminJSONView +from synnefo_admin.admin.associations import ( + UserAssociation, QuotaAssociation, VMAssociation, VolumeAssociation, + NetworkAssociation, NicAssociation, IPAssociation, IPLogAssociation, + ProjectAssociation) + +from .utils import get_flavor_info, get_vm_or_404, get_user_details_href +from .filters import VMFilterSet +from .actions import cached_actions + +templates = { + 'list': 'admin/vm_list.html', + 'details': 'admin/vm_details.html', +} + + +class VMJSONView(AdminJSONView): + model = VirtualMachine + fields = ('pk', 'name', 'operstate', 'suspended',) + filters = VMFilterSet + + def get_extra_data(self, qs): + # FIXME: The `contact_name`, `contact_email` fields will cripple our db + if self.form.cleaned_data['iDisplayLength'] < 0: + qs = qs.only('pk', 'name', 'operstate', 'suspended', 'id', + 'deleted', 'task', 'userid') + return [self.get_extra_data_row(row) for row in qs] + + def get_extra_data_row(self, inst): + if self.dt_data['iDisplayLength'] < 0: + extra_dict = {} + else: + extra_dict = OrderedDict() + + extra_dict['allowed_actions'] = { + 'display_name': "", + 'value': get_allowed_actions(cached_actions, inst, + self.request.user), + 'visible': False, + } + extra_dict['id'] = { + 'display_name': "ID", + 'value': inst.pk, + 'visible': False, + } + extra_dict['item_name'] = { + 'display_name': "Name", + 'value': escape(inst.name), + 'visible': False, + } + extra_dict['details_url'] = { + 'display_name': "Details", + 'value': reverse('admin-details', args=['vm', inst.pk]), + 'visible': True, + } + extra_dict['contact_id'] = { + 'display_name': "Contact ID", + 'value': inst.userid, + 'visible': False, + } + extra_dict['contact_email'] = { + 'display_name': "Contact email", + 'value': escape(AstakosUser.objects.get(uuid=inst.userid).email), + 'visible': False, + } + extra_dict['contact_name'] = { + 'display_name': "Contact name", + 'value': escape(AstakosUser.objects.get(uuid=inst.userid).realname), + 'visible': False, + } + + if self.form.cleaned_data['iDisplayLength'] < 0: + extra_dict['minimal'] = { + 'display_name': "No summary available", + 'value': "Have you per chance pressed 'Select All'?", + 'visible': True, + } + else: + extra_dict.update(self.add_verbose_data(inst)) + + return extra_dict + + def add_verbose_data(self, inst): + extra_dict = OrderedDict() + extra_dict['user_info'] = { + 'display_name': "User", + 'value': get_user_details_href(inst), + 'visible': True, + } + extra_dict['image_id'] = { + 'display_name': "Image ID", + 'value': inst.imageid, + 'visible': True, + } + extra_dict['flavor_info'] = { + 'display_name': "Flavor info", + 'value': get_flavor_info(inst), + 'visible': True, + } + extra_dict['created'] = { + 'display_name': "Created", + 'value': inst.created.strftime("%Y-%m-%d %H:%M"), + 'visible': True, + } + extra_dict['updated'] = { + 'display_name': "Updated", + 'value': inst.updated.strftime("%Y-%m-%d %H:%M"), + 'visible': True, + } + + return extra_dict + + +JSON_CLASS = VMJSONView + + +@has_permission_or_403(cached_actions) +def do_action(request, op, id): + """Apply the requested action on the specified user.""" + if op == "contact": + user = get_user_or_404(id) + else: + vm = get_vm_or_404(id) + actions = get_permitted_actions(cached_actions, request.user) + + if op == 'reboot': + actions[op].apply(vm, "SOFT") + elif op == 'contact': + actions[op].apply(user, request) + else: + actions[op].apply(vm) + + +@has_permission_or_403(cached_actions) +def wait_action(request, op, id): + """Wait for the requested action to end.""" + if op == "contact" or op == "suspend" or op == "unsuspend": + return + + terminal_state = ["ERROR"] + if op == "start" or op == "reboot": + terminal_state.append("STARTED") + elif op == "shutdown": + terminal_state.append("STOPPED") + elif op == "destroy": + terminal_state.append("DESTROYED") + + while True: + vm = get_vm_or_404(id) + if vm.operstate in terminal_state: + break + time.sleep(1) + + return + + +def catalog(request): + """List view for Cyclades VMs.""" + context = {} + context['action_dict'] = get_permitted_actions(cached_actions, + request.user) + context['filter_dict'] = VMFilterSet().filters.values() + context['columns'] = ["ID", "Name", "State", "Suspended", ""] + context['item_type'] = 'vm' + + return context + + +def details(request, query): + """Details view for Astakos users.""" + vm = get_vm_or_404(query) + associations = [] + + user_list = AstakosUser.objects.filter(uuid=vm.userid) + associations.append(UserAssociation(request, user_list,)) + + project_list = Project.objects.filter(uuid=vm.project) + associations.append(ProjectAssociation(request, project_list,)) + + volume_list = vm.volumes.all() + associations.append(VolumeAssociation(request, volume_list,)) + + network_list = Network.objects.filter(machines__pk=vm.pk) + associations.append(NetworkAssociation(request, network_list,)) + + nic_list = vm.nics.all() + associations.append(NicAssociation(request, nic_list,)) + + ip_list = IPAddress.objects.filter(nic__in=vm.nics.all()) + associations.append(IPAssociation(request, ip_list,)) + + ip_log_list = IPAddressLog.objects.filter(server_id=vm.pk) + associations.append(IPLogAssociation(request, ip_log_list)) + + context = { + 'main_item': vm, + 'main_type': 'vm', + 'action_dict': get_permitted_actions(cached_actions, request.user), + 'associations_list': associations, + } + + return context diff --git a/snf-admin-app/synnefo_admin/admin/resources/volumes/__init__.py b/snf-admin-app/synnefo_admin/admin/resources/volumes/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/snf-admin-app/synnefo_admin/admin/resources/volumes/actions.py b/snf-admin-app/synnefo_admin/admin/resources/volumes/actions.py new file mode 100644 index 0000000000000000000000000000000000000000..01a07f6cdada34253f281b4c47ebd3747ef8a6b6 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/resources/volumes/actions.py @@ -0,0 +1,46 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +import logging +from collections import OrderedDict + +from synnefo_admin.admin.actions import AdminAction +from synnefo_admin.admin.utils import update_actions_rbac, send_admin_email + + +class VolumeAction(AdminAction): + + """Class for actions on volumes. Derived from AdminAction. + + Pre-determined Attributes: + target: volume + """ + + def __init__(self, name, f, **kwargs): + """Initialize the class with provided values.""" + AdminAction.__init__(self, name=name, target='volume', f=f, **kwargs) + + +def generate_actions(): + """Create a list of actions on volumes.""" + actions = OrderedDict() + + actions['contact'] = VolumeAction(name='Send e-mail', f=send_admin_email,) + + update_actions_rbac(actions) + + return actions +cached_actions = generate_actions() diff --git a/snf-admin-app/synnefo_admin/admin/resources/volumes/filters.py b/snf-admin-app/synnefo_admin/admin/resources/volumes/filters.py new file mode 100644 index 0000000000000000000000000000000000000000..81be0bd1fcef3dcf4a665f755fd6e7b99e57fac1 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/resources/volumes/filters.py @@ -0,0 +1,119 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +import logging + +from django.core.cache import cache +from django.db.models import Q + +from synnefo.db.models import Volume + +import django_filters + +from synnefo_admin.admin.queries_common import (query, model_filter, + get_model_field) + + +def get_disk_template_choices(): + # Check if the choices exist in the cache. + dt_choices = cache.get('dt_choices') + if dt_choices: + return dt_choices + + # Recreate them if they don't. + dt_choices = cache.get('dt_choices') + dt_field = "volume_type__disk_template" + dts = Volume.objects.order_by(dt_field).\ + values_list("{}".format(dt_field), flat=True).distinct() + dt_choices = [(dt, '_') for dt in dts] + + # Store them in cache for 5 minutes and return them to the caller. + cache.set('dt_choices', dt_choices, 300) + return dt_choices + + +@model_filter +def filter_volume(queryset, queries): + q = query("volume", queries) + return queryset.filter(q) + + +@model_filter +def filter_user(queryset, queries): + q = query("user", queries) + ids = get_model_field("user", q, 'uuid') + return queryset.filter(userid__in=ids) + + +@model_filter +def filter_vm(queryset, queries): + q = query("vm", queries) + ids = get_model_field("vm", q, 'volumes__id') + return queryset.filter(id__in=ids) + + +@model_filter +def filter_project(queryset, queries): + q = query("project", queries) + ids = get_model_field("project", q, 'uuid') + return queryset.filter(project__in=ids) + + +def filter_disk_template(queryset, choices): + if not query: + return queryset + choices = choices or () + dt_choices = get_disk_template_choices() + if len(choices) == len(dt_choices): + return queryset + q = Q() + for c in choices: + q |= Q(volume_type__disk_template=c) + return queryset.filter(q) + + +def filter_index(queryset, query): + if not query: + return queryset + elif not query.isdigit(): + return queryset.none() + return queryset.filter(index=query) + + +class VolumeFilterSet(django_filters.FilterSet): + + """A collection of filters for volumes. + + This filter collection is based on django-filter's FilterSet. + """ + + vol = django_filters.CharFilter(label='Volume', action=filter_volume) + user = django_filters.CharFilter(label='OF User', action=filter_user) + vm = django_filters.CharFilter(label='OF VM', action=filter_vm) + proj = django_filters.CharFilter(label='OF Project', action=filter_project) + status = django_filters.MultipleChoiceFilter( + label='Status', name='status', choices=Volume.STATUS_VALUES) + disk_template = django_filters.MultipleChoiceFilter( + label="Disk template", choices=get_disk_template_choices(), + action=filter_disk_template) + index = django_filters.CharFilter(label="Index", action=filter_index) + source = django_filters.CharFilter(label="Soure image", name="source", + lookup_type='icontains') + + class Meta: + model = Volume + fields = ('vol', 'status', 'disk_template', 'index', 'source', + 'user', 'vm', 'proj') diff --git a/snf-admin-app/synnefo_admin/admin/resources/volumes/utils.py b/snf-admin-app/synnefo_admin/admin/resources/volumes/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..afc7ae979e650877899682162a0ff7484232e1bb --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/resources/volumes/utils.py @@ -0,0 +1,48 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +import logging + +from django.core.exceptions import ObjectDoesNotExist + +from synnefo.db.models import Volume +from astakos.im.models import AstakosUser, Project + +from synnefo_admin.admin.exceptions import AdminHttp404 +from synnefo_admin.admin.utils import create_details_href + + +def get_volume_or_404(query): + try: + return Volume.objects.get(pk=int(query)) + except (ObjectDoesNotExist, ValueError): + raise AdminHttp404( + "No Volume was found that matches this query: %s\n" % query) + + +def get_user_details_href(volume): + user = AstakosUser.objects.get(uuid=volume.userid) + return create_details_href('user', user.realname, user.email) + + +def get_project_details_href(volume): + project = Project.objects.get(uuid=volume.project) + return create_details_href('project', project.realname, project.id) + + +def get_vm_details_href(volume): + vm = volume.machine + return create_details_href('vm', vm.name, vm.id) diff --git a/snf-admin-app/synnefo_admin/admin/resources/volumes/views.py b/snf-admin-app/synnefo_admin/admin/resources/volumes/views.py new file mode 100644 index 0000000000000000000000000000000000000000..0e263e93e63a0790e85c1e36c94536699b11cf38 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/resources/volumes/views.py @@ -0,0 +1,217 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +import logging +from collections import OrderedDict + +from django.core.urlresolvers import reverse +from django.utils.html import escape + +from synnefo.db.models import Volume, VirtualMachine +from astakos.im.models import AstakosUser, Project + +from synnefo_admin.admin.actions import (has_permission_or_403, + get_allowed_actions, + get_permitted_actions,) +from synnefo_admin.admin.resources.users.utils import get_user_or_404 +from synnefo_admin.admin.tables import AdminJSONView +from synnefo_admin.admin.associations import ( + UserAssociation, QuotaAssociation, VMAssociation, VolumeAssociation, + NetworkAssociation, NicAssociation, IPAssociation, IPLogAssociation, + ProjectAssociation) + +from .utils import (get_volume_or_404, get_user_details_href, + get_vm_details_href, get_project_details_href) +from .actions import cached_actions +from .filters import VolumeFilterSet + +templates = { + 'list': 'admin/volume_list.html', + 'details': 'admin/volume_details.html', +} + + +class VolumeJSONView(AdminJSONView): + model = Volume + fields = ('id', 'name', 'status', 'size', 'volume_type__disk_template', + 'machine__pk', 'created', 'updated') + filters = VolumeFilterSet + + def format_data_row(self, row): + row = list(row) + if not row[1]: + row[1] = "(not set)" + row[6] = row[6].strftime("%Y-%m-%d %H:%M") + row[7] = row[7].strftime("%Y-%m-%d %H:%M") + return row + + def get_extra_data(self, qs): + # FIXME: The `contact_name`, `contact_email` fields will cripple our db + if self.form.cleaned_data['iDisplayLength'] < 0: + qs = qs.only('id', 'name', 'status', 'created', 'userid') + return [self.get_extra_data_row(row) for row in qs] + + def get_extra_data_row(self, inst): + if self.dt_data['iDisplayLength'] < 0: + extra_dict = {} + else: + extra_dict = OrderedDict() + + extra_dict['allowed_actions'] = { + 'display_name': "", + 'value': get_allowed_actions(cached_actions, inst, + self.request.user), + 'visible': False, + } + extra_dict['id'] = { + 'display_name': "ID", + 'value': inst.id, + 'visible': False, + } + extra_dict['item_name'] = { + 'display_name': "Name", + 'value': escape(inst.name), + 'visible': False, + } + extra_dict['details_url'] = { + 'display_name': "Details", + 'value': reverse('admin-details', args=['volume', inst.id]), + 'visible': True, + } + extra_dict['contact_id'] = { + 'display_name': "Contact ID", + 'value': inst.userid, + 'visible': False, + } + extra_dict['contact_email'] = { + 'display_name': "Contact email", + 'value': escape(AstakosUser.objects.get(uuid=inst.userid).email), + 'visible': False, + } + extra_dict['contact_name'] = { + 'display_name': "Contact name", + 'value': escape(AstakosUser.objects.get(uuid=inst.userid).realname), + 'visible': False, + } + + if self.form.cleaned_data['iDisplayLength'] < 0: + extra_dict['minimal'] = { + 'display_name': "No summary available", + 'value': "Have you per chance pressed 'Select All'?", + 'visible': True, + } + else: + extra_dict.update(self.add_verbose_data(inst)) + + return extra_dict + + def add_verbose_data(self, inst): + extra_dict = OrderedDict() + extra_dict['description'] = { + 'display_name': "Description", + 'value': escape(inst.description) or "(not set)", + 'visible': True, + } + if inst.source: + sv = inst.source_version + source_version = " (v{})".format(sv) if sv else "" + extra_dict['source'] = { + 'display_name': "Source Image", + 'value': inst.source + source_version, + 'visible': True, + } + if inst.origin: + extra_dict['origin'] = { + 'display_name': "Origin", + 'value': inst.origin, + 'visible': True, + } + extra_dict['index'] = { + 'display_name': "Index", + 'value': inst.index, + 'visible': True, + } + extra_dict['user_info'] = { + 'display_name': "User", + 'value': get_user_details_href(inst), + 'visible': True, + } + extra_dict['project_info'] = { + 'display_name': "Project", + 'value': get_project_details_href(inst), + 'visible': True, + } + if inst.machine: + extra_dict['vm_info'] = { + 'display_name': "VM", + 'value': get_vm_details_href(inst), + 'visible': True, + } + return extra_dict + +JSON_CLASS = VolumeJSONView + + +@has_permission_or_403(cached_actions) +def do_action(request, op, id): + """Apply the requested action on the specified volume.""" + if op == "contact": + user = get_user_or_404(id) + else: + volume = Volume.objects.get(id=id) + actions = get_permitted_actions(cached_actions, request.user) + + if op == 'contact': + actions[op].apply(user, request) + else: + actions[op].apply(volume) + + +def catalog(request): + """List view for Cyclades volumes.""" + context = {} + context['action_dict'] = get_permitted_actions(cached_actions, + request.user) + context['filter_dict'] = VolumeFilterSet().filters.values() + context['columns'] = ["ID", "Name", "Status", "Size (GB)", "Disk template", + "VM ID", "Created at", "Updated at", ""] + context['item_type'] = 'volume' + + return context + + +def details(request, query): + """Details view for Astakos users.""" + volume = get_volume_or_404(query) + associations = [] + + vm_list = VirtualMachine.objects.filter(volumes=volume) + associations.append(VMAssociation(request, vm_list,)) + + user_list = AstakosUser.objects.filter(uuid=volume.userid) + associations.append(UserAssociation(request, user_list,)) + + project_list = Project.objects.filter(uuid=volume.project) + associations.append(ProjectAssociation(request, project_list,)) + + context = { + 'main_item': volume, + 'main_type': 'volume', + 'action_dict': get_permitted_actions(cached_actions, request.user), + 'associations_list': associations, + } + + return context diff --git a/snf-admin-app/synnefo_admin/admin/static/config.rb b/snf-admin-app/synnefo_admin/admin/static/config.rb new file mode 100644 index 0000000000000000000000000000000000000000..96d5b0060e233e0cb14ad3b545c6afdfde1f7d26 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/config.rb @@ -0,0 +1,34 @@ +# Require any additional compass plugins here. + +# Set this to the root of your project when deployed: +http_path = "/" +sass_dir = "sass" +images_dir = "images" +javascripts_dir = "javascripts" + +# You can select your preferred output style here (can be overridden via the command line): +output_style = :nested + +# To enable relative paths to assets via compass helper functions. Uncomment: +# relative_assets = true + +# To disable debugging comments that display the original location of your selectors. Uncomment: +# line_comments = false + + +# If you prefer the indented syntax, you might want to regenerate this +# project again passing --syntax sass, or you can uncomment this: +# preferred_syntax = :sass +# and then run: +# sass-convert -R --from scss --to sass sass scss && rm -rf sass && mv scss sass +if environment == :development + css_dir = "css" + line_comments = true + output_style = :nested +end + +if environment == :production + css_dir = "min-css" + line_comments = false + output_style = :compressed +end diff --git a/snf-admin-app/synnefo_admin/admin/static/css/c3.css b/snf-admin-app/synnefo_admin/admin/static/css/c3.css new file mode 100644 index 0000000000000000000000000000000000000000..568648f5e7db9ed7168775616733e6c3338cca5d --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/css/c3.css @@ -0,0 +1,203 @@ +/*-- Chart --*/ + +.c3 svg { + font: 10px sans-serif; +} +.c3 path, .c3 line { + fill: none; + stroke: #000; +} +.c3 text { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +.c3-legend-item-tile, +.c3-xgrid-focus, +.c3-ygrid, +.c3-event-rect, +.c3-bars path { + shape-rendering: crispEdges; +} + +.c3-chart-arc path { + stroke: #fff; + +} +.c3-chart-arc text { + fill: #fff; + font-size: 13px; +} + +/*-- Axis --*/ + +.c3-axis-x .tick { +} +.c3-axis-x-label { +} + +.c3-axis-y .tick { +} +.c3-axis-y-label { +} + +.c3-axis-y2 .tick { +} +.c3-axis-y2-label { +} + +/*-- Grid --*/ + +.c3-grid line { + stroke: #aaa; +} +.c3-grid text { + fill: #aaa; +} +.c3-xgrid, .c3-ygrid { + stroke-dasharray: 3 3; +} +.c3-xgrid-focus { +} + +/*-- Text on Chart --*/ + +.c3-text { +} + +.c3-text.c3-empty { + fill: #808080; + font-size: 2em; +} + +/*-- Line --*/ + +.c3-line { + stroke-width: 1px; +} +/*-- Point --*/ + +.c3-circle._expanded_ { + stroke-width: 1px; + stroke: white; +} +.c3-selected-circle { + fill: white; + stroke-width: 2px; +} + +/*-- Bar --*/ + +.c3-bar { + stroke-width: 0; +} +.c3-bar._expanded_ { + fill-opacity: 0.75; +} + +/*-- Arc --*/ + +.c3-chart-arcs-title { + font-size: 1.3em; +} + +/*-- Focus --*/ + +.c3-target.c3-focused path.c3-line, .c3-target.c3-focused path.c3-step { + stroke-width: 2px; +} + +/*-- Region --*/ + +.c3-region { + fill: steelblue; + fill-opacity: .1; +} + +/*-- Brush --*/ + +.c3-brush .extent { + fill-opacity: .1; +} + +/*-- Select - Drag --*/ + +.c3-dragarea { +} + +/*-- Legend --*/ + +.c3-legend-item { + font-size: 12px; +} + +.c3-legend-background { + opacity: 0.75; + fill: white; + stroke: lightgray; + stroke-width: 1 +} + +/*-- Tooltip --*/ + +.c3-tooltip { + border-collapse:collapse; + border-spacing:0; + background-color:#fff; + empty-cells:show; + -webkit-box-shadow: 7px 7px 12px -9px rgb(119,119,119); + -moz-box-shadow: 7px 7px 12px -9px rgb(119,119,119); + box-shadow: 7px 7px 12px -9px rgb(119,119,119); + opacity: 0.9; +} +.c3-tooltip tr { + border:1px solid #CCC; +} +.c3-tooltip th { + background-color: #aaa; + font-size:14px; + padding:2px 5px; + text-align:left; + color:#FFF; +} +.c3-tooltip td { + font-size:13px; + padding: 3px 6px; + background-color:#fff; + border-left:1px dotted #999; +} +.c3-tooltip td > span { + display: inline-block; + width: 10px; + height: 10px; + margin-right: 6px; +} +.c3-tooltip td.value{ + text-align: right; +} + +.c3-area { + stroke-width: 0; + opacity: 0.2; +} + +.c3-chart-arcs .c3-chart-arcs-background { + fill: #e0e0e0; + stroke: none; +} +.c3-chart-arcs .c3-chart-arcs-gauge-unit { + fill: #000; + font-size: 16px; +} +.c3-chart-arcs .c3-chart-arcs-gauge-max { + fill: #777; +} +.c3-chart-arcs .c3-chart-arcs-gauge-min { + fill: #777; +} + +.c3-chart-arc .c3-gauge-value { + fill: #000; + font-size: 28px; +} diff --git a/snf-admin-app/synnefo_admin/admin/static/css/icon-fonts.css b/snf-admin-app/synnefo_admin/admin/static/css/icon-fonts.css new file mode 100644 index 0000000000000000000000000000000000000000..d2667ba3d287541b7941d81801b9bf61133dc2e3 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/css/icon-fonts.css @@ -0,0 +1,1774 @@ +@font-face { + font-family: 'font-icons'; + src: url("../fonts/font-icons.eot?hm0cup"); + src: url("../fonts/font-icons.eot?#iefixhm0cup") format("embedded-opentype"), url("../fonts/font-icons.woff?hm0cup") format("woff"), url("../fonts/font-icons.ttf?hm0cup") format("truetype"), url("../fonts/font-icons.svg?hm0cup#font-icons") format("svg"); + font-weight: normal; + font-style: normal; } + +/* Font with kpal icons */ +@font-face { + font-family: "snf-font"; + src: url("../fonts/snf-font.eot"); + src: url("../fonts/snf-font.eot?#iefix") format("embedded-opentype"), url("../fonts/snf-font.woff") format("woff"), url("../fonts/snf-font.ttf") format("truetype"), url("../fonts/snf-font.svg#snf-font") format("svg"); + font-weight: normal; + font-style: normal; } + +/* line 47, ../sass/icon-fonts.scss */ +.snf-ok { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-ok:before { + content: "\61"; } + +/* line 50, ../sass/icon-fonts.scss */ +.snf-remove { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-remove:before { + content: "\62"; } + +/* line 53, ../sass/icon-fonts.scss */ +.snf-envelope { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-envelope:before { + content: "\63"; } + +/* line 56, ../sass/icon-fonts.scss */ +.snf-envelope-alt { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-envelope-alt:before { + content: "\64"; } + +/* line 59, ../sass/icon-fonts.scss */ +.snf-angle-up { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-angle-up:before { + content: "\65"; } + +/* line 62, ../sass/icon-fonts.scss */ +.snf-angle-down { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-angle-down:before { + content: "\66"; } + +/* line 65, ../sass/icon-fonts.scss */ +.snf-exclamation-sign { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-exclamation-sign:before { + content: "\67"; } + +/* line 68, ../sass/icon-fonts.scss */ +.snf-clipboard-h { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-clipboard-h:before { + content: "\68"; } + +/* line 71, ../sass/icon-fonts.scss */ +.snf-clipboard-i { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-clipboard-i:before { + content: "\69"; } + +/* line 74, ../sass/icon-fonts.scss */ +.snf-copy { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-copy:before { + content: "\6c"; } + +/* line 77, ../sass/icon-fonts.scss */ +.snf-search { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-search:before { + content: "\6d"; } + +/* line 80, ../sass/icon-fonts.scss */ +.snf-sign-out { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-sign-out:before { + content: "\6e"; } + +/* line 83, ../sass/icon-fonts.scss */ +.snf-archive { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-archive:before { + content: "\6b"; } + +/* line 86, ../sass/icon-fonts.scss */ +.snf-checkbox-checked { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-checkbox-checked:before { + content: "\6f"; } + +/* line 89, ../sass/icon-fonts.scss */ +.snf-checkbox-unchecked { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-checkbox-unchecked:before { + content: "\70"; } + +/* line 92, ../sass/icon-fonts.scss */ +.snf-radio-checked { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-radio-checked:before { + content: "\71"; } + +/* line 95, ../sass/icon-fonts.scss */ +.snf-radio-unchecked { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-radio-unchecked:before { + content: "\72"; } + +/* line 98, ../sass/icon-fonts.scss */ +.snf-info { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-info:before { + content: "\73"; } + +/* line 101, ../sass/icon-fonts.scss */ +.snf-user-outline { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-user-outline:before { + content: "\75"; } + +/* line 104, ../sass/icon-fonts.scss */ +.snf-user-full { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-user-full:before { + content: "\74"; } + +/* line 107, ../sass/icon-fonts.scss */ +.snf-wallet-full { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-wallet-full:before { + content: "\78"; } + +/* line 110, ../sass/icon-fonts.scss */ +.snf-wallet-outline { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-wallet-outline:before { + content: "\79"; } + +/* line 113, ../sass/icon-fonts.scss */ +.snf-keyboard { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-keyboard:before { + content: "\7a"; } + +/* line 116, ../sass/icon-fonts.scss */ +.snf-book-2 { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-book-2:before { + content: "\42"; } + +/* line 119, ../sass/icon-fonts.scss */ +.snf-bell-1 { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-bell-1:before { + content: "\43"; } + +/* line 122, ../sass/icon-fonts.scss */ +.snf-bulb { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-bulb:before { + content: "\46"; } + +/* line 125, ../sass/icon-fonts.scss */ +.snf-sun-1 { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-sun-1:before { + content: "\47"; } + +/* line 128, ../sass/icon-fonts.scss */ +.snf-moon-1 { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-moon-1:before { + content: "\76"; } + +/* line 131, ../sass/icon-fonts.scss */ +.snf-sun-2-full { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-sun-2-full:before { + content: "\77"; } + +/* line 134, ../sass/icon-fonts.scss */ +.snf-sun-2-outline { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-sun-2-outline:before { + content: "\6a"; } + +/* line 137, ../sass/icon-fonts.scss */ +.snf-moon-2-full:before { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-moon-2-full:before:before { + content: "\44"; } + +/* line 140, ../sass/icon-fonts.scss */ +.snf-moon-2-outline { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-moon-2-outline:before { + content: "\45"; } + +/* line 143, ../sass/icon-fonts.scss */ +.snf-sun-3 { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-sun-3:before { + content: "\41"; } + +/* line 146, ../sass/icon-fonts.scss */ +.snf-filter { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-filter:before { + content: "\7b"; } + +/* line 149, ../sass/icon-fonts.scss */ +.snf-eye { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-eye:before { + content: "\41"; } + +/* line 152, ../sass/icon-fonts.scss */ +.snf-radio-checked { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-radio-checked:before { + content: "\42"; } + +/* line 155, ../sass/icon-fonts.scss */ +.snf-radio-unchecked { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-radio-unchecked:before { + content: "\43"; } + +/* line 158, ../sass/icon-fonts.scss */ +.snf-close { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-close:before { + content: "\44"; } + +/* line 161, ../sass/icon-fonts.scss */ +.snf-www { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-www:before { + content: "\49"; } + +/* line 164, ../sass/icon-fonts.scss */ +.snf-arrow-up { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-arrow-up:before { + content: "\4c"; } + +/* line 167, ../sass/icon-fonts.scss */ +.snf-arrow-down { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-arrow-down:before { + content: "\4d"; } + +/* line 170, ../sass/icon-fonts.scss */ +.snf-checkbox-unchecked { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-checkbox-unchecked:before { + content: "\61"; } + +/* line 173, ../sass/icon-fonts.scss */ +.snf-checkbox-checked { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-checkbox-checked:before { + content: "\62"; } + +/* line 176, ../sass/icon-fonts.scss */ +.snf-cancel-circled { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-cancel-circled:before { + content: "\63"; } + +/* line 179, ../sass/icon-fonts.scss */ +.snf-search { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-search:before { + content: "\64"; } + +/* line 182, ../sass/icon-fonts.scss */ +.snf-twitter-logo { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-twitter-logo:before { + content: "\67"; } + +/* line 185, ../sass/icon-fonts.scss */ +.snf-ok { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-ok:before { + content: "\68"; } + +/* line 188, ../sass/icon-fonts.scss */ +.snf-switch { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-switch:before { + content: "\69"; } + +/* line 191, ../sass/icon-fonts.scss */ +.snf-ban-circle { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-ban-circle:before { + content: "\6a"; } + +/* line 194, ../sass/icon-fonts.scss */ +.snf-ok-sign { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-ok-sign:before { + content: "\6c"; } + +/* line 197, ../sass/icon-fonts.scss */ +.snf-minus-sign { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-minus-sign:before { + content: "\6e"; } + +/* line 200, ../sass/icon-fonts.scss */ +.snf-edit { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-edit:before { + content: "\71"; } + +/* line 203, ../sass/icon-fonts.scss */ +.snf-listview { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-listview:before { + content: "\73"; } + +/* line 206, ../sass/icon-fonts.scss */ +.snf-gridview { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-gridview:before { + content: "\74"; } + +/* line 209, ../sass/icon-fonts.scss */ +.snf-dashboard-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-dashboard-outline:before { + content: "\7a"; } + +/* line 212, ../sass/icon-fonts.scss */ +.snf-pithos-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-pithos-outline:before { + content: "\79"; } + +/* line 215, ../sass/icon-fonts.scss */ +.snf-info-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-info-full:before { + content: "\70"; } + +/* line 218, ../sass/icon-fonts.scss */ +.snf-volume-create-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-volume-create-full:before { + content: "\36"; } + +/* line 221, ../sass/icon-fonts.scss */ +.snf-image-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-image-full:before { + content: "\51"; } + +/* line 224, ../sass/icon-fonts.scss */ +.snf-pc-create-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-pc-create-full:before { + content: "\53"; } + +/* line 227, ../sass/icon-fonts.scss */ +.snf-network-create-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-network-create-outline:before { + content: "\54"; } + +/* line 230, ../sass/icon-fonts.scss */ +.snf-network-create-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-network-create-full:before { + content: "\55"; } + +/* line 233, ../sass/icon-fonts.scss */ +.snf-ram-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-ram-outline:before { + content: "\4a"; } + +/* line 236, ../sass/icon-fonts.scss */ +.snf-nic-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-nic-outline:before { + content: "\50"; } + +/* line 239, ../sass/icon-fonts.scss */ +.snf-ram-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-ram-full:before { + content: "\52"; } + +/* line 242, ../sass/icon-fonts.scss */ +.snf-nic-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-nic-full:before { + content: "\72"; } + +/* line 245, ../sass/icon-fonts.scss */ +.snf-network-broken-1-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-network-broken-1-full:before { + content: "\56"; } + +/* line 248, ../sass/icon-fonts.scss */ +.snf-network-broken-2-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-network-broken-2-full:before { + content: "\57"; } + +/* line 251, ../sass/icon-fonts.scss */ +.snf-pc-broken-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-pc-broken-full:before { + content: "\58"; } + +/* line 254, ../sass/icon-fonts.scss */ +.snf-pc-reboot-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-pc-reboot-full:before { + content: "\59"; } + +/* line 257, ../sass/icon-fonts.scss */ +.snf-pc-switch-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-pc-switch-full:before { + content: "\5a"; } + +/* line 260, ../sass/icon-fonts.scss */ +.snf-key-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-key-full:before { + content: "\31"; } + +/* line 263, ../sass/icon-fonts.scss */ +.snf-router-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-router-full:before { + content: "\32"; } + +/* line 266, ../sass/icon-fonts.scss */ +.snf-chip-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-chip-full:before { + content: "\33"; } + +/* line 269, ../sass/icon-fonts.scss */ +.snf-plus-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-plus-full:before { + content: "\34"; } + +/* line 272, ../sass/icon-fonts.scss */ +.snf-snapshot-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-snapshot-full:before { + content: "\4e"; } + +/* line 275, ../sass/icon-fonts.scss */ +.snf-pithos-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-pithos-full:before { + content: "\35"; } + +/* line 278, ../sass/icon-fonts.scss */ +.snf-volume-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-volume-full:before { + content: "\4f"; } + +/* line 281, ../sass/icon-fonts.scss */ +.snf-network-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-network-full:before { + content: "\4b"; } + +/* line 284, ../sass/icon-fonts.scss */ +.snf-pc-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-pc-full:before { + content: "\78"; } + +/* line 287, ../sass/icon-fonts.scss */ +.snf-network-broken-1-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-network-broken-1-outline:before { + content: "\37"; } + +/* line 290, ../sass/icon-fonts.scss */ +.snf-network-broken-2-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-network-broken-2-outline:before { + content: "\38"; } + +/* line 293, ../sass/icon-fonts.scss */ +.snf-pc-broken-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-pc-broken-outline:before { + content: "\39"; } + +/* line 296, ../sass/icon-fonts.scss */ +.snf-volume-broken-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-volume-broken-outline:before { + content: "\30"; } + +/* line 299, ../sass/icon-fonts.scss */ +.snf-pc-reboot-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-pc-reboot-outline:before { + content: "\21"; } + +/* line 302, ../sass/icon-fonts.scss */ +.snf-pc-switch-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-pc-switch-outline:before { + content: "\40"; } + +/* line 305, ../sass/icon-fonts.scss */ +.snf-key-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-key-outline:before { + content: "\23"; } + +/* line 308, ../sass/icon-fonts.scss */ +.snf-router-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-router-outline:before { + content: "\48"; } + +/* line 311, ../sass/icon-fonts.scss */ +.snf-chip-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-chip-outline:before { + content: "\45"; } + +/* line 314, ../sass/icon-fonts.scss */ +.snf-image-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-image-outline:before { + content: "\66"; } + +/* line 317, ../sass/icon-fonts.scss */ +.snf-plus-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-plus-outline:before { + content: "\6d"; } + +/* line 320, ../sass/icon-fonts.scss */ +.snf-snapshot-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-snapshot-outline:before { + content: "\65"; } + +/* line 323, ../sass/icon-fonts.scss */ +.snf-volume-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-volume-outline:before { + content: "\75"; } + +/* line 326, ../sass/icon-fonts.scss */ +.snf-network-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-network-outline:before { + content: "\76"; } + +/* line 329, ../sass/icon-fonts.scss */ +.snf-pc-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-pc-outline:before { + content: "\77"; } + +/* line 332, ../sass/icon-fonts.scss */ +.snf-info-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-info-outline:before { + content: "\6f"; } + +/* line 335, ../sass/icon-fonts.scss */ +.snf-thunder-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-thunder-full:before { + content: "\6b"; } + +/* line 338, ../sass/icon-fonts.scss */ +.snf-lock-closed-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-lock-closed-full:before { + content: "\46"; } + +/* line 341, ../sass/icon-fonts.scss */ +.snf-lock-open-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-lock-open-full:before { + content: "\47"; } + +/* line 345, ../sass/icon-fonts.scss */ +.snf-link-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-link-outline:before { + content: "\26"; } + +/* line 348, ../sass/icon-fonts.scss */ +.snf-refresh-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-refresh-outline:before { + content: "\29"; } + +/* line 351, ../sass/icon-fonts.scss */ +.snf-download-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-download-full:before { + content: "\25"; } + +/* line 354, ../sass/icon-fonts.scss */ +.snf-person-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-person-outline:before { + content: "\2a"; } + +/* line 357, ../sass/icon-fonts.scss */ +.snf-upload-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-upload-full:before { + content: "\28"; } + +/* line 360, ../sass/icon-fonts.scss */ +.snf-arrow-right-small-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-arrow-right-small-full:before { + content: "\2d"; } + +/* line 363, ../sass/icon-fonts.scss */ +.snf-copy-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-copy-outline:before { + content: "\3f"; } + +/* line 366, ../sass/icon-fonts.scss */ +.snf-copy-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-copy-full:before { + content: "\22"; } + +/* line 369, ../sass/icon-fonts.scss */ +.snf-arrow-left-small-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-arrow-left-small-full:before { + content: "\5f"; } + +/* line 372, ../sass/icon-fonts.scss */ +.snf-trash-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-trash-full:before { + content: "\3d"; } + +/* line 375, ../sass/icon-fonts.scss */ +.snf-trash-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-trash-outline:before { + content: "\24"; } diff --git a/snf-admin-app/synnefo_admin/admin/static/css/jquery.dataTables.css b/snf-admin-app/synnefo_admin/admin/static/css/jquery.dataTables.css new file mode 100644 index 0000000000000000000000000000000000000000..d57b597ec673de088f634fca0cf905cf09889aca --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/css/jquery.dataTables.css @@ -0,0 +1,399 @@ +/* + * Table styles + */ +table.dataTable { + width: 100%; + margin: 0 auto; + clear: both; + border-collapse: separate; + border-spacing: 0; + /* + * Header and footer styles + */ + /* + * Body styles + */ +} +table.dataTable thead th, +table.dataTable tfoot th { + font-weight: bold; +} +table.dataTable thead th, +table.dataTable thead td { + padding: 10px 18px; + border-bottom: 1px solid #111111; +} +table.dataTable thead th:active, +table.dataTable thead td:active { + outline: none; +} +table.dataTable tfoot th, +table.dataTable tfoot td { + padding: 10px 18px 6px 18px; + border-top: 1px solid #111111; +} +table.dataTable thead .sorting_asc, +table.dataTable thead .sorting_desc, +table.dataTable thead .sorting { + cursor: pointer; + *cursor: hand; +} +table.dataTable thead .sorting { + background: url("../images/sort_both.png") no-repeat center right; +} +table.dataTable thead .sorting_asc { + background: url("../images/sort_asc.png") no-repeat center right; +} +table.dataTable thead .sorting_desc { + background: url("../images/sort_desc.png") no-repeat center right; +} +table.dataTable thead .sorting_asc_disabled { + background: url("../images/sort_asc_disabled.png") no-repeat center right; +} +table.dataTable thead .sorting_desc_disabled { + background: url("../images/sort_desc_disabled.png") no-repeat center right; +} +table.dataTable tbody tr { + background-color: white; +} +table.dataTable tbody tr.selected { + background-color: #b0bed9; +} +table.dataTable tbody th, +table.dataTable tbody td { + padding: 8px 10px; +} +table.dataTable th.center, +table.dataTable td.center, +table.dataTable td.dataTables_empty { + text-align: center; +} +table.dataTable th.right, +table.dataTable td.right { + text-align: right; +} +table.dataTable.row-border tbody th, table.dataTable.row-border tbody td, table.dataTable.display tbody th, table.dataTable.display tbody td { + border-top: 1px solid #dddddd; +} +table.dataTable.row-border tbody tr:first-child th, +table.dataTable.row-border tbody tr:first-child td, table.dataTable.display tbody tr:first-child th, +table.dataTable.display tbody tr:first-child td { + border-top: none; +} +table.dataTable.cell-border tbody th, table.dataTable.cell-border tbody td { + border-top: 1px solid #dddddd; + border-right: 1px solid #dddddd; +} +table.dataTable.cell-border tbody tr th:first-child, +table.dataTable.cell-border tbody tr td:first-child { + border-left: 1px solid #dddddd; +} +table.dataTable.cell-border tbody tr:first-child th, +table.dataTable.cell-border tbody tr:first-child td { + border-top: none; +} +table.dataTable.stripe tbody tr.odd, table.dataTable.display tbody tr.odd { + background-color: #f9f9f9; +} +table.dataTable.stripe tbody tr.odd.selected, table.dataTable.display tbody tr.odd.selected { + background-color: #abb9d3; +} +table.dataTable.hover tbody tr:hover, +table.dataTable.hover tbody tr.odd:hover, +table.dataTable.hover tbody tr.even:hover, table.dataTable.display tbody tr:hover, +table.dataTable.display tbody tr.odd:hover, +table.dataTable.display tbody tr.even:hover { + background-color: whitesmoke; +} +table.dataTable.hover tbody tr:hover.selected, +table.dataTable.hover tbody tr.odd:hover.selected, +table.dataTable.hover tbody tr.even:hover.selected, table.dataTable.display tbody tr:hover.selected, +table.dataTable.display tbody tr.odd:hover.selected, +table.dataTable.display tbody tr.even:hover.selected { + background-color: #a9b7d1; +} +table.dataTable.order-column tbody tr > .sorting_1, +table.dataTable.order-column tbody tr > .sorting_2, +table.dataTable.order-column tbody tr > .sorting_3, table.dataTable.display tbody tr > .sorting_1, +table.dataTable.display tbody tr > .sorting_2, +table.dataTable.display tbody tr > .sorting_3 { + background-color: #f9f9f9; +} +table.dataTable.order-column tbody tr.selected > .sorting_1, +table.dataTable.order-column tbody tr.selected > .sorting_2, +table.dataTable.order-column tbody tr.selected > .sorting_3, table.dataTable.display tbody tr.selected > .sorting_1, +table.dataTable.display tbody tr.selected > .sorting_2, +table.dataTable.display tbody tr.selected > .sorting_3 { + background-color: #acbad4; +} +table.dataTable.display tbody tr.odd > .sorting_1, table.dataTable.order-column.stripe tbody tr.odd > .sorting_1 { + background-color: #f1f1f1; +} +table.dataTable.display tbody tr.odd > .sorting_2, table.dataTable.order-column.stripe tbody tr.odd > .sorting_2 { + background-color: #f3f3f3; +} +table.dataTable.display tbody tr.odd > .sorting_3, table.dataTable.order-column.stripe tbody tr.odd > .sorting_3 { + background-color: whitesmoke; +} +table.dataTable.display tbody tr.odd.selected > .sorting_1, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_1 { + background-color: #a6b3cd; +} +table.dataTable.display tbody tr.odd.selected > .sorting_2, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_2 { + background-color: #a7b5ce; +} +table.dataTable.display tbody tr.odd.selected > .sorting_3, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_3 { + background-color: #a9b6d0; +} +table.dataTable.display tbody tr.even > .sorting_1, table.dataTable.order-column.stripe tbody tr.even > .sorting_1 { + background-color: #f9f9f9; +} +table.dataTable.display tbody tr.even > .sorting_2, table.dataTable.order-column.stripe tbody tr.even > .sorting_2 { + background-color: #fbfbfb; +} +table.dataTable.display tbody tr.even > .sorting_3, table.dataTable.order-column.stripe tbody tr.even > .sorting_3 { + background-color: #fdfdfd; +} +table.dataTable.display tbody tr.even.selected > .sorting_1, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_1 { + background-color: #acbad4; +} +table.dataTable.display tbody tr.even.selected > .sorting_2, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_2 { + background-color: #adbbd6; +} +table.dataTable.display tbody tr.even.selected > .sorting_3, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_3 { + background-color: #afbdd8; +} +table.dataTable.display tbody tr:hover > .sorting_1, +table.dataTable.display tbody tr.odd:hover > .sorting_1, +table.dataTable.display tbody tr.even:hover > .sorting_1, table.dataTable.order-column.hover tbody tr:hover > .sorting_1, +table.dataTable.order-column.hover tbody tr.odd:hover > .sorting_1, +table.dataTable.order-column.hover tbody tr.even:hover > .sorting_1 { + background-color: #eaeaea; +} +table.dataTable.display tbody tr:hover > .sorting_2, +table.dataTable.display tbody tr.odd:hover > .sorting_2, +table.dataTable.display tbody tr.even:hover > .sorting_2, table.dataTable.order-column.hover tbody tr:hover > .sorting_2, +table.dataTable.order-column.hover tbody tr.odd:hover > .sorting_2, +table.dataTable.order-column.hover tbody tr.even:hover > .sorting_2 { + background-color: #ebebeb; +} +table.dataTable.display tbody tr:hover > .sorting_3, +table.dataTable.display tbody tr.odd:hover > .sorting_3, +table.dataTable.display tbody tr.even:hover > .sorting_3, table.dataTable.order-column.hover tbody tr:hover > .sorting_3, +table.dataTable.order-column.hover tbody tr.odd:hover > .sorting_3, +table.dataTable.order-column.hover tbody tr.even:hover > .sorting_3 { + background-color: #eeeeee; +} +table.dataTable.display tbody tr:hover.selected > .sorting_1, +table.dataTable.display tbody tr.odd:hover.selected > .sorting_1, +table.dataTable.display tbody tr.even:hover.selected > .sorting_1, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_1, +table.dataTable.order-column.hover tbody tr.odd:hover.selected > .sorting_1, +table.dataTable.order-column.hover tbody tr.even:hover.selected > .sorting_1 { + background-color: #a1aec7; +} +table.dataTable.display tbody tr:hover.selected > .sorting_2, +table.dataTable.display tbody tr.odd:hover.selected > .sorting_2, +table.dataTable.display tbody tr.even:hover.selected > .sorting_2, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_2, +table.dataTable.order-column.hover tbody tr.odd:hover.selected > .sorting_2, +table.dataTable.order-column.hover tbody tr.even:hover.selected > .sorting_2 { + background-color: #a2afc8; +} +table.dataTable.display tbody tr:hover.selected > .sorting_3, +table.dataTable.display tbody tr.odd:hover.selected > .sorting_3, +table.dataTable.display tbody tr.even:hover.selected > .sorting_3, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_3, +table.dataTable.order-column.hover tbody tr.odd:hover.selected > .sorting_3, +table.dataTable.order-column.hover tbody tr.even:hover.selected > .sorting_3 { + background-color: #a4b2cb; +} +table.dataTable.no-footer { + border-bottom: 1px solid #111111; +} + +table.dataTable, +table.dataTable th, +table.dataTable td { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; +} + +/* + * Control feature layout + */ +.dataTables_wrapper { + position: relative; + clear: both; + *zoom: 1; + zoom: 1; +} +.dataTables_wrapper .dataTables_length { + float: left; +} +.dataTables_wrapper .dataTables_filter { + float: right; + text-align: right; +} +.dataTables_wrapper .dataTables_filter input { + margin-left: 0.5em; +} +.dataTables_wrapper .dataTables_info { + clear: both; + float: left; + padding-top: 0.755em; +} +.dataTables_wrapper .dataTables_paginate { + float: right; + text-align: right; + padding-top: 0.25em; +} +.dataTables_wrapper .dataTables_paginate .paginate_button { + box-sizing: border-box; + display: inline-block; + min-width: 1.5em; + padding: 0.5em 1em; + margin-left: 2px; + text-align: center; + text-decoration: none !important; + cursor: pointer; + *cursor: hand; + color: #333333 !important; + border: 1px solid transparent; +} +.dataTables_wrapper .dataTables_paginate .paginate_button.current, .dataTables_wrapper .dataTables_paginate .paginate_button.current:hover { + color: #333333 !important; + border: 1px solid #cacaca; + background-color: white; + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, white), color-stop(100%, gainsboro)); + /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, white 0%, gainsboro 100%); + /* Chrome10+,Safari5.1+ */ + background: -moz-linear-gradient(top, white 0%, gainsboro 100%); + /* FF3.6+ */ + background: -ms-linear-gradient(top, white 0%, gainsboro 100%); + /* IE10+ */ + background: -o-linear-gradient(top, white 0%, gainsboro 100%); + /* Opera 11.10+ */ + background: linear-gradient(to bottom, white 0%, gainsboro 100%); + /* W3C */ +} +.dataTables_wrapper .dataTables_paginate .paginate_button.disabled, .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover, .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active { + cursor: default; + color: #666 !important; + border: 1px solid transparent; + background: transparent; + box-shadow: none; +} +.dataTables_wrapper .dataTables_paginate .paginate_button:hover { + color: white !important; + border: 1px solid #111111; + background-color: #585858; + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #585858), color-stop(100%, #111111)); + /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #585858 0%, #111111 100%); + /* Chrome10+,Safari5.1+ */ + background: -moz-linear-gradient(top, #585858 0%, #111111 100%); + /* FF3.6+ */ + background: -ms-linear-gradient(top, #585858 0%, #111111 100%); + /* IE10+ */ + background: -o-linear-gradient(top, #585858 0%, #111111 100%); + /* Opera 11.10+ */ + background: linear-gradient(to bottom, #585858 0%, #111111 100%); + /* W3C */ +} +.dataTables_wrapper .dataTables_paginate .paginate_button:active { + outline: none; + background-color: #2b2b2b; + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #2b2b2b), color-stop(100%, #0c0c0c)); + /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%); + /* Chrome10+,Safari5.1+ */ + background: -moz-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%); + /* FF3.6+ */ + background: -ms-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%); + /* IE10+ */ + background: -o-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%); + /* Opera 11.10+ */ + background: linear-gradient(to bottom, #2b2b2b 0%, #0c0c0c 100%); + /* W3C */ + box-shadow: inset 0 0 3px #111; +} +.dataTables_wrapper .dataTables_processing { + position: absolute; + top: 50%; + left: 50%; + width: 100%; + height: 40px; + margin-left: -50%; + margin-top: -25px; + padding-top: 20px; + text-align: center; + font-size: 1.2em; + background-color: white; + background: -webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255, 255, 255, 0)), color-stop(25%, rgba(255, 255, 255, 0.9)), color-stop(75%, rgba(255, 255, 255, 0.9)), color-stop(100%, rgba(255, 255, 255, 0))); + /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%); + /* Chrome10+,Safari5.1+ */ + background: -moz-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%); + /* FF3.6+ */ + background: -ms-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%); + /* IE10+ */ + background: -o-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%); + /* Opera 11.10+ */ + background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%); + /* W3C */ +} +.dataTables_wrapper .dataTables_length, +.dataTables_wrapper .dataTables_filter, +.dataTables_wrapper .dataTables_info, +.dataTables_wrapper .dataTables_processing, +.dataTables_wrapper .dataTables_paginate { + color: #333333; +} +.dataTables_wrapper .dataTables_scroll { + clear: both; +} +.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody { + *margin-top: -1px; + -webkit-overflow-scrolling: touch; +} +.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody th > div.dataTables_sizing, +.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody td > div.dataTables_sizing { + height: 0; + overflow: hidden; + margin: 0 !important; + padding: 0 !important; +} +.dataTables_wrapper.no-footer .dataTables_scrollBody { + border-bottom: 1px solid #111111; +} +.dataTables_wrapper.no-footer div.dataTables_scrollHead table, +.dataTables_wrapper.no-footer div.dataTables_scrollBody table { + border-bottom: none; +} +.dataTables_wrapper:after { + visibility: hidden; + display: block; + content: ""; + clear: both; + height: 0; +} + +@media screen and (max-width: 767px) { + .dataTables_wrapper .dataTables_info, + .dataTables_wrapper .dataTables_paginate { + float: none; + text-align: center; + } + .dataTables_wrapper .dataTables_paginate { + margin-top: 0.5em; + } +} +@media screen and (max-width: 640px) { + .dataTables_wrapper .dataTables_length, + .dataTables_wrapper .dataTables_filter { + float: none; + text-align: center; + } + .dataTables_wrapper .dataTables_filter { + margin-top: 0.5em; + } +} diff --git a/snf-admin-app/synnefo_admin/admin/static/css/main-light.css b/snf-admin-app/synnefo_admin/admin/static/css/main-light.css new file mode 100644 index 0000000000000000000000000000000000000000..a0609457ab76f80aadbcfafa8d6c2264866aa09c --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/css/main-light.css @@ -0,0 +1,8478 @@ +/*! normalize.css v3.0.0 | MIT License | git.io/normalize */ +/* line 9, ../sass/bootstrap/_normalize.scss */ +html { + font-family: sans-serif; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; } + +/* line 19, ../sass/bootstrap/_normalize.scss */ +body { + margin: 0; } + +/* line 41, ../sass/bootstrap/_normalize.scss */ +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +nav, +section, +summary { + display: block; } + +/* line 53, ../sass/bootstrap/_normalize.scss */ +audio, +canvas, +progress, +video { + display: inline-block; + vertical-align: baseline; } + +/* line 63, ../sass/bootstrap/_normalize.scss */ +audio:not([controls]) { + display: none; + height: 0; } + +/* line 74, ../sass/bootstrap/_normalize.scss */ +[hidden], +template { + display: none; } + +/* line 85, ../sass/bootstrap/_normalize.scss */ +a { + background: transparent; } + +/* line 94, ../sass/bootstrap/_normalize.scss */ +a:active, +a:hover { + outline: 0; } + +/* line 105, ../sass/bootstrap/_normalize.scss */ +abbr[title] { + border-bottom: 1px dotted; } + +/* line 114, ../sass/bootstrap/_normalize.scss */ +b, +strong { + font-weight: bold; } + +/* line 122, ../sass/bootstrap/_normalize.scss */ +dfn { + font-style: italic; } + +/* line 131, ../sass/bootstrap/_normalize.scss */ +h1 { + font-size: 2em; + margin: 0.67em 0; } + +/* line 140, ../sass/bootstrap/_normalize.scss */ +mark { + background: #ff0; + color: #000; } + +/* line 149, ../sass/bootstrap/_normalize.scss */ +small { + font-size: 80%; } + +/* line 158, ../sass/bootstrap/_normalize.scss */ +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; } + +/* line 165, ../sass/bootstrap/_normalize.scss */ +sup { + top: -0.5em; } + +/* line 169, ../sass/bootstrap/_normalize.scss */ +sub { + bottom: -0.25em; } + +/* line 180, ../sass/bootstrap/_normalize.scss */ +img { + border: 0; } + +/* line 188, ../sass/bootstrap/_normalize.scss */ +svg:not(:root) { + overflow: hidden; } + +/* line 199, ../sass/bootstrap/_normalize.scss */ +figure { + margin: 1em 40px; } + +/* line 207, ../sass/bootstrap/_normalize.scss */ +hr { + -moz-box-sizing: content-box; + box-sizing: content-box; + height: 0; } + +/* line 217, ../sass/bootstrap/_normalize.scss */ +pre { + overflow: auto; } + +/* line 228, ../sass/bootstrap/_normalize.scss */ +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; } + +/* line 252, ../sass/bootstrap/_normalize.scss */ +button, +input, +optgroup, +select, +textarea { + color: inherit; + font: inherit; + margin: 0; } + +/* line 262, ../sass/bootstrap/_normalize.scss */ +button { + overflow: visible; } + +/* line 274, ../sass/bootstrap/_normalize.scss */ +button, +select { + text-transform: none; } + +/* line 289, ../sass/bootstrap/_normalize.scss */ +button, +html input[type="button"], +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; + cursor: pointer; } + +/* line 299, ../sass/bootstrap/_normalize.scss */ +button[disabled], +html input[disabled] { + cursor: default; } + +/* line 308, ../sass/bootstrap/_normalize.scss */ +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; } + +/* line 318, ../sass/bootstrap/_normalize.scss */ +input { + line-height: normal; } + +/* line 331, ../sass/bootstrap/_normalize.scss */ +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; + padding: 0; } + +/* line 343, ../sass/bootstrap/_normalize.scss */ +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; } + +/* line 353, ../sass/bootstrap/_normalize.scss */ +input[type="search"] { + -webkit-appearance: textfield; + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; + box-sizing: content-box; } + +/* line 367, ../sass/bootstrap/_normalize.scss */ +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; } + +/* line 375, ../sass/bootstrap/_normalize.scss */ +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; } + +/* line 386, ../sass/bootstrap/_normalize.scss */ +legend { + border: 0; + padding: 0; } + +/* line 395, ../sass/bootstrap/_normalize.scss */ +textarea { + overflow: auto; } + +/* line 404, ../sass/bootstrap/_normalize.scss */ +optgroup { + font-weight: bold; } + +/* line 415, ../sass/bootstrap/_normalize.scss */ +table { + border-collapse: collapse; + border-spacing: 0; } + +/* line 421, ../sass/bootstrap/_normalize.scss */ +td, +th { + padding: 0; } + +@media print { + /* line 8, ../sass/bootstrap/_print.scss */ + * { + text-shadow: none !important; + color: #000 !important; + background: transparent !important; + box-shadow: none !important; } + + /* line 16, ../sass/bootstrap/_print.scss */ + a, + a:visited { + text-decoration: underline; } + + /* line 20, ../sass/bootstrap/_print.scss */ + a[href]:after { + content: " (" attr(href) ")"; } + + /* line 24, ../sass/bootstrap/_print.scss */ + abbr[title]:after { + content: " (" attr(title) ")"; } + + /* line 30, ../sass/bootstrap/_print.scss */ + a[href^="javascript:"]:after, + a[href^="#"]:after { + content: ""; } + + /* line 35, ../sass/bootstrap/_print.scss */ + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; } + + /* line 40, ../sass/bootstrap/_print.scss */ + thead { + display: table-header-group; } + + /* line 45, ../sass/bootstrap/_print.scss */ + tr, + img { + page-break-inside: avoid; } + + /* line 49, ../sass/bootstrap/_print.scss */ + img { + max-width: 100% !important; } + + /* line 55, ../sass/bootstrap/_print.scss */ + p, + h2, + h3 { + orphans: 3; + widows: 3; } + + /* line 61, ../sass/bootstrap/_print.scss */ + h2, + h3 { + page-break-after: avoid; } + + /* line 67, ../sass/bootstrap/_print.scss */ + select { + background: #fff !important; } + + /* line 72, ../sass/bootstrap/_print.scss */ + .navbar { + display: none; } + + /* line 77, ../sass/bootstrap/_print.scss */ + .table td, + .table th { + background-color: #fff !important; } + + /* line 83, ../sass/bootstrap/_print.scss */ + .btn > .caret, + .dropup > .btn > .caret { + border-top-color: #000 !important; } + + /* line 87, ../sass/bootstrap/_print.scss */ + .label { + border: 1px solid #000; } + + /* line 91, ../sass/bootstrap/_print.scss */ + .table { + border-collapse: collapse !important; } + + /* line 96, ../sass/bootstrap/_print.scss */ + .table-bordered th, + .table-bordered td { + border: 1px solid #ddd !important; } } +/* line 11, ../sass/bootstrap/_scaffolding.scss */ +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } + +/* line 15, ../sass/bootstrap/_scaffolding.scss */ +*:before, +*:after { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } + +/* line 22, ../sass/bootstrap/_scaffolding.scss */ +html { + font-size: 62.5%; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } + +/* line 27, ../sass/bootstrap/_scaffolding.scss */ +body { + font-family: "Open Sans", sans-serif; + font-size: 14px; + line-height: 1.42857; + color: #222222; + background-color: white; } + +/* line 39, ../sass/bootstrap/_scaffolding.scss */ +input, +button, +select, +textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; + color: #222222; } + +/* line 49, ../sass/bootstrap/_scaffolding.scss */ +a { + color: #005b9a; + text-decoration: none; } + /* line 54, ../sass/bootstrap/_scaffolding.scss */ + a:hover, a:focus { + color: #ee5161; } + /* line 58, ../sass/bootstrap/_scaffolding.scss */ + a:focus { + outline: 0 none; } + +/* line 69, ../sass/bootstrap/_scaffolding.scss */ +figure { + margin: 0; } + +/* line 76, ../sass/bootstrap/_scaffolding.scss */ +img { + vertical-align: middle; } + +/* line 81, ../sass/bootstrap/_scaffolding.scss */ +.img-responsive { + display: block; + max-width: 100%; + height: auto; } + +/* line 86, ../sass/bootstrap/_scaffolding.scss */ +.img-rounded { + border-radius: 6px; } + +/* line 93, ../sass/bootstrap/_scaffolding.scss */ +.img-thumbnail { + padding: 4px; + line-height: 1.42857; + background-color: #303030; + border: 1px solid #dddddd; + border-radius: 0; + -webkit-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; + display: inline-block; + max-width: 100%; + height: auto; } + +/* line 106, ../sass/bootstrap/_scaffolding.scss */ +.img-circle { + border-radius: 50%; } + +/* line 113, ../sass/bootstrap/_scaffolding.scss */ +hr { + margin-top: 20px; + margin-bottom: 20px; + border: 0; + border-top: 1px solid #d9d9d9; } + +/* line 125, ../sass/bootstrap/_scaffolding.scss */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; } + +/* line 10, ../sass/bootstrap/_type.scss */ +h1, h2, h3, h4, h5, h6, +.h1, .h2, .h3, .h4, .h5, .h6 { + font-family: inherit; + font-weight: 500; + line-height: 1.1; + color: inherit; } + /* line 17, ../sass/bootstrap/_type.scss */ + h1 small, + h1 .small, h2 small, + h2 .small, h3 small, + h3 .small, h4 small, + h4 .small, h5 small, + h5 .small, h6 small, + h6 .small, + .h1 small, + .h1 .small, .h2 small, + .h2 .small, .h3 small, + .h3 .small, .h4 small, + .h4 .small, .h5 small, + .h5 .small, .h6 small, + .h6 .small { + font-weight: normal; + line-height: 1; + color: #4e4e4e; } + +/* line 26, ../sass/bootstrap/_type.scss */ +h1, .h1, +h2, .h2, +h3, .h3 { + margin-top: 20px; + margin-bottom: 10px; } + /* line 31, ../sass/bootstrap/_type.scss */ + h1 small, + h1 .small, .h1 small, + .h1 .small, + h2 small, + h2 .small, .h2 small, + .h2 .small, + h3 small, + h3 .small, .h3 small, + .h3 .small { + font-size: 65%; } + +/* line 37, ../sass/bootstrap/_type.scss */ +h4, .h4, +h5, .h5, +h6, .h6 { + margin-top: 10px; + margin-bottom: 10px; } + /* line 42, ../sass/bootstrap/_type.scss */ + h4 small, + h4 .small, .h4 small, + .h4 .small, + h5 small, + h5 .small, .h5 small, + .h5 .small, + h6 small, + h6 .small, .h6 small, + .h6 .small { + font-size: 75%; } + +/* line 47, ../sass/bootstrap/_type.scss */ +h1, .h1 { + font-size: 36px; } + +/* line 48, ../sass/bootstrap/_type.scss */ +h2, .h2 { + font-size: 30px; } + +/* line 49, ../sass/bootstrap/_type.scss */ +h3, .h3 { + font-size: 24px; } + +/* line 50, ../sass/bootstrap/_type.scss */ +h4, .h4 { + font-size: 18px; } + +/* line 51, ../sass/bootstrap/_type.scss */ +h5, .h5 { + font-size: 14px; } + +/* line 52, ../sass/bootstrap/_type.scss */ +h6, .h6 { + font-size: 12px; } + +/* line 58, ../sass/bootstrap/_type.scss */ +p { + margin: 0 0 10px; } + +/* line 62, ../sass/bootstrap/_type.scss */ +.lead { + margin-bottom: 20px; + font-size: 16px; + font-weight: 200; + line-height: 1.4; } + @media (min-width: 768px) { + /* line 62, ../sass/bootstrap/_type.scss */ + .lead { + font-size: 21px; } } + +/* line 79, ../sass/bootstrap/_type.scss */ +small, +.small { + font-size: 85%; } + +/* line 82, ../sass/bootstrap/_type.scss */ +cite { + font-style: normal; } + +/* line 85, ../sass/bootstrap/_type.scss */ +.text-left { + text-align: left; } + +/* line 86, ../sass/bootstrap/_type.scss */ +.text-right { + text-align: right; } + +/* line 87, ../sass/bootstrap/_type.scss */ +.text-center { + text-align: center; } + +/* line 88, ../sass/bootstrap/_type.scss */ +.text-justify { + text-align: justify; } + +/* line 91, ../sass/bootstrap/_type.scss */ +.text-muted { + color: #4e4e4e; } + +/* line 606, ../sass/bootstrap/_mixins.scss */ +.text-primary { + color: white; } + +/* line 609, ../sass/bootstrap/_mixins.scss */ +a.text-primary:hover { + color: #e6e6e6; } + +/* line 606, ../sass/bootstrap/_mixins.scss */ +.text-success { + color: #3c763d; } + +/* line 609, ../sass/bootstrap/_mixins.scss */ +a.text-success:hover { + color: #2b542c; } + +/* line 606, ../sass/bootstrap/_mixins.scss */ +.text-info { + color: #31708f; } + +/* line 609, ../sass/bootstrap/_mixins.scss */ +a.text-info:hover { + color: #245269; } + +/* line 606, ../sass/bootstrap/_mixins.scss */ +.text-warning { + color: #8a6d3b; } + +/* line 609, ../sass/bootstrap/_mixins.scss */ +a.text-warning:hover { + color: #66512c; } + +/* line 606, ../sass/bootstrap/_mixins.scss */ +.text-danger { + color: #a94442; } + +/* line 609, ../sass/bootstrap/_mixins.scss */ +a.text-danger:hover { + color: #843534; } + +/* line 108, ../sass/bootstrap/_type.scss */ +.bg-primary { + color: #fff; } + +/* line 594, ../sass/bootstrap/_mixins.scss */ +.bg-primary { + background-color: white; } + +/* line 597, ../sass/bootstrap/_mixins.scss */ +a.bg-primary:hover { + background-color: #e6e6e6; } + +/* line 594, ../sass/bootstrap/_mixins.scss */ +.bg-success { + background-color: #dff0d8; } + +/* line 597, ../sass/bootstrap/_mixins.scss */ +a.bg-success:hover { + background-color: #c1e2b3; } + +/* line 594, ../sass/bootstrap/_mixins.scss */ +.bg-info { + background-color: #d9edf7; } + +/* line 597, ../sass/bootstrap/_mixins.scss */ +a.bg-info:hover { + background-color: #afd9ee; } + +/* line 594, ../sass/bootstrap/_mixins.scss */ +.bg-warning { + background-color: #fcf8e3; } + +/* line 597, ../sass/bootstrap/_mixins.scss */ +a.bg-warning:hover { + background-color: #f7ecb5; } + +/* line 594, ../sass/bootstrap/_mixins.scss */ +.bg-danger { + background-color: #f2dede; } + +/* line 597, ../sass/bootstrap/_mixins.scss */ +a.bg-danger:hover { + background-color: #e4b9b9; } + +/* line 127, ../sass/bootstrap/_type.scss */ +.page-header { + padding-bottom: 9px; + margin: 40px 0 20px; + border-bottom: 1px solid #d9d9d9; } + +/* line 139, ../sass/bootstrap/_type.scss */ +ul, +ol { + margin-top: 0; + margin-bottom: 10px; } + /* line 143, ../sass/bootstrap/_type.scss */ + ul ul, + ul ol, + ol ul, + ol ol { + margin-bottom: 0; } + +/* line 151, ../sass/bootstrap/_type.scss */ +.list-unstyled, .list-inline { + padding-left: 0; + list-style: none; } + +/* line 157, ../sass/bootstrap/_type.scss */ +.list-inline { + margin-left: -5px; } + /* line 161, ../sass/bootstrap/_type.scss */ + .list-inline > li { + display: inline-block; + padding-left: 5px; + padding-right: 5px; } + +/* line 169, ../sass/bootstrap/_type.scss */ +dl { + margin-top: 0; + margin-bottom: 0; } + +/* line 174, ../sass/bootstrap/_type.scss */ +dt, +dd { + line-height: 1.42857; } + +/* line 177, ../sass/bootstrap/_type.scss */ +dt { + font-weight: bold; } + +/* line 180, ../sass/bootstrap/_type.scss */ +dd { + margin-left: 0; } + +@media (min-width: 768px) { + /* line 191, ../sass/bootstrap/_type.scss */ + .dl-horizontal dt { + float: left; + width: 160px; + clear: left; + text-align: right; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } + /* line 198, ../sass/bootstrap/_type.scss */ + .dl-horizontal dd { + margin-left: 180px; } + /* line 21, ../sass/bootstrap/_mixins.scss */ + .dl-horizontal dd:before, .dl-horizontal dd:after { + content: " "; + display: table; } + /* line 25, ../sass/bootstrap/_mixins.scss */ + .dl-horizontal dd:after { + clear: both; } } +/* line 211, ../sass/bootstrap/_type.scss */ +abbr[title], +abbr[data-original-title] { + cursor: help; + border-bottom: 1px dotted #4e4e4e; } + +/* line 215, ../sass/bootstrap/_type.scss */ +.initialism { + font-size: 90%; + text-transform: uppercase; } + +/* line 221, ../sass/bootstrap/_type.scss */ +blockquote { + padding: 10px 20px; + margin: 0 0 20px; + font-size: 17.5px; + border-left: 5px solid #d9d9d9; } + /* line 230, ../sass/bootstrap/_type.scss */ + blockquote p:last-child, + blockquote ul:last-child, + blockquote ol:last-child { + margin-bottom: 0; } + /* line 239, ../sass/bootstrap/_type.scss */ + blockquote footer, + blockquote small, + blockquote .small { + display: block; + font-size: 80%; + line-height: 1.42857; + color: #4e4e4e; } + /* line 245, ../sass/bootstrap/_type.scss */ + blockquote footer:before, + blockquote small:before, + blockquote .small:before { + content: '\2014 \00A0'; } + +/* line 255, ../sass/bootstrap/_type.scss */ +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + border-right: 5px solid #d9d9d9; + border-left: 0; + text-align: right; } + /* line 266, ../sass/bootstrap/_type.scss */ + .blockquote-reverse footer:before, + .blockquote-reverse small:before, + .blockquote-reverse .small:before, + blockquote.pull-right footer:before, + blockquote.pull-right small:before, + blockquote.pull-right .small:before { + content: ''; } + /* line 267, ../sass/bootstrap/_type.scss */ + .blockquote-reverse footer:after, + .blockquote-reverse small:after, + .blockquote-reverse .small:after, + blockquote.pull-right footer:after, + blockquote.pull-right small:after, + blockquote.pull-right .small:after { + content: '\00A0 \2014'; } + +/* line 275, ../sass/bootstrap/_type.scss */ +blockquote:before, +blockquote:after { + content: ""; } + +/* line 280, ../sass/bootstrap/_type.scss */ +address { + margin-bottom: 20px; + font-style: normal; + line-height: 1.42857; } + +/* line 10, ../sass/bootstrap/_code.scss */ +code, +kbd, +pre, +samp { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; } + +/* line 15, ../sass/bootstrap/_code.scss */ +code { + padding: 2px 4px; + font-size: 90%; + color: #c7254e; + background-color: #f9f2f4; + white-space: nowrap; + border-radius: 0; } + +/* line 25, ../sass/bootstrap/_code.scss */ +kbd { + padding: 2px 4px; + font-size: 90%; + color: white; + background-color: #333333; + border-radius: 3px; + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); } + +/* line 35, ../sass/bootstrap/_code.scss */ +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 1.42857; + word-break: break-all; + word-wrap: break-word; + color: #303030; + background-color: whitesmoke; + border: 1px solid #cccccc; + border-radius: 0; } + /* line 49, ../sass/bootstrap/_code.scss */ + pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; } + +/* line 60, ../sass/bootstrap/_code.scss */ +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; } + +/* line 10, ../sass/bootstrap/_grid.scss */ +.container { + margin-right: auto; + margin-left: auto; + padding-left: 15px; + padding-right: 15px; } + /* line 21, ../sass/bootstrap/_mixins.scss */ + .container:before, .container:after { + content: " "; + display: table; } + /* line 25, ../sass/bootstrap/_mixins.scss */ + .container:after { + clear: both; } + @media (min-width: 768px) { + /* line 10, ../sass/bootstrap/_grid.scss */ + .container { + width: 810px; } } + @media (min-width: 992px) { + /* line 10, ../sass/bootstrap/_grid.scss */ + .container { + width: 1010px; } } + @media (min-width: 1200px) { + /* line 10, ../sass/bootstrap/_grid.scss */ + .container { + width: 1170px; } } + +/* line 30, ../sass/bootstrap/_grid.scss */ +.container-fluid { + margin-right: auto; + margin-left: auto; + padding-left: 15px; + padding-right: 15px; } + /* line 21, ../sass/bootstrap/_mixins.scss */ + .container-fluid:before, .container-fluid:after { + content: " "; + display: table; } + /* line 25, ../sass/bootstrap/_mixins.scss */ + .container-fluid:after { + clear: both; } + +/* line 39, ../sass/bootstrap/_grid.scss */ +.row { + margin-left: -15px; + margin-right: -15px; } + /* line 21, ../sass/bootstrap/_mixins.scss */ + .row:before, .row:after { + content: " "; + display: table; } + /* line 25, ../sass/bootstrap/_mixins.scss */ + .row:after { + clear: both; } + +/* line 799, ../sass/bootstrap/_mixins.scss */ +.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 { + position: relative; + min-height: 1px; + padding-left: 15px; + padding-right: 15px; } + +/* line 818, ../sass/bootstrap/_mixins.scss */ +.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 { + float: left; } + +/* line 826, ../sass/bootstrap/_mixins.scss */ +.col-xs-1 { + width: 8.33333%; } + +/* line 826, ../sass/bootstrap/_mixins.scss */ +.col-xs-2 { + width: 16.66667%; } + +/* line 826, ../sass/bootstrap/_mixins.scss */ +.col-xs-3 { + width: 25%; } + +/* line 826, ../sass/bootstrap/_mixins.scss */ +.col-xs-4 { + width: 33.33333%; } + +/* line 826, ../sass/bootstrap/_mixins.scss */ +.col-xs-5 { + width: 41.66667%; } + +/* line 826, ../sass/bootstrap/_mixins.scss */ +.col-xs-6 { + width: 50%; } + +/* line 826, ../sass/bootstrap/_mixins.scss */ +.col-xs-7 { + width: 58.33333%; } + +/* line 826, ../sass/bootstrap/_mixins.scss */ +.col-xs-8 { + width: 66.66667%; } + +/* line 826, ../sass/bootstrap/_mixins.scss */ +.col-xs-9 { + width: 75%; } + +/* line 826, ../sass/bootstrap/_mixins.scss */ +.col-xs-10 { + width: 83.33333%; } + +/* line 826, ../sass/bootstrap/_mixins.scss */ +.col-xs-11 { + width: 91.66667%; } + +/* line 826, ../sass/bootstrap/_mixins.scss */ +.col-xs-12 { + width: 100%; } + +/* line 836, ../sass/bootstrap/_mixins.scss */ +.col-xs-pull-0 { + right: 0%; } + +/* line 836, ../sass/bootstrap/_mixins.scss */ +.col-xs-pull-1 { + right: 8.33333%; } + +/* line 836, ../sass/bootstrap/_mixins.scss */ +.col-xs-pull-2 { + right: 16.66667%; } + +/* line 836, ../sass/bootstrap/_mixins.scss */ +.col-xs-pull-3 { + right: 25%; } + +/* line 836, ../sass/bootstrap/_mixins.scss */ +.col-xs-pull-4 { + right: 33.33333%; } + +/* line 836, ../sass/bootstrap/_mixins.scss */ +.col-xs-pull-5 { + right: 41.66667%; } + +/* line 836, ../sass/bootstrap/_mixins.scss */ +.col-xs-pull-6 { + right: 50%; } + +/* line 836, ../sass/bootstrap/_mixins.scss */ +.col-xs-pull-7 { + right: 58.33333%; } + +/* line 836, ../sass/bootstrap/_mixins.scss */ +.col-xs-pull-8 { + right: 66.66667%; } + +/* line 836, ../sass/bootstrap/_mixins.scss */ +.col-xs-pull-9 { + right: 75%; } + +/* line 836, ../sass/bootstrap/_mixins.scss */ +.col-xs-pull-10 { + right: 83.33333%; } + +/* line 836, ../sass/bootstrap/_mixins.scss */ +.col-xs-pull-11 { + right: 91.66667%; } + +/* line 836, ../sass/bootstrap/_mixins.scss */ +.col-xs-pull-12 { + right: 100%; } + +/* line 831, ../sass/bootstrap/_mixins.scss */ +.col-xs-push-0 { + left: 0%; } + +/* line 831, ../sass/bootstrap/_mixins.scss */ +.col-xs-push-1 { + left: 8.33333%; } + +/* line 831, ../sass/bootstrap/_mixins.scss */ +.col-xs-push-2 { + left: 16.66667%; } + +/* line 831, ../sass/bootstrap/_mixins.scss */ +.col-xs-push-3 { + left: 25%; } + +/* line 831, ../sass/bootstrap/_mixins.scss */ +.col-xs-push-4 { + left: 33.33333%; } + +/* line 831, ../sass/bootstrap/_mixins.scss */ +.col-xs-push-5 { + left: 41.66667%; } + +/* line 831, ../sass/bootstrap/_mixins.scss */ +.col-xs-push-6 { + left: 50%; } + +/* line 831, ../sass/bootstrap/_mixins.scss */ +.col-xs-push-7 { + left: 58.33333%; } + +/* line 831, ../sass/bootstrap/_mixins.scss */ +.col-xs-push-8 { + left: 66.66667%; } + +/* line 831, ../sass/bootstrap/_mixins.scss */ +.col-xs-push-9 { + left: 75%; } + +/* line 831, ../sass/bootstrap/_mixins.scss */ +.col-xs-push-10 { + left: 83.33333%; } + +/* line 831, ../sass/bootstrap/_mixins.scss */ +.col-xs-push-11 { + left: 91.66667%; } + +/* line 831, ../sass/bootstrap/_mixins.scss */ +.col-xs-push-12 { + left: 100%; } + +/* line 841, ../sass/bootstrap/_mixins.scss */ +.col-xs-offset-0 { + margin-left: 0%; } + +/* line 841, ../sass/bootstrap/_mixins.scss */ +.col-xs-offset-1 { + margin-left: 8.33333%; } + +/* line 841, ../sass/bootstrap/_mixins.scss */ +.col-xs-offset-2 { + margin-left: 16.66667%; } + +/* line 841, ../sass/bootstrap/_mixins.scss */ +.col-xs-offset-3 { + margin-left: 25%; } + +/* line 841, ../sass/bootstrap/_mixins.scss */ +.col-xs-offset-4 { + margin-left: 33.33333%; } + +/* line 841, ../sass/bootstrap/_mixins.scss */ +.col-xs-offset-5 { + margin-left: 41.66667%; } + +/* line 841, ../sass/bootstrap/_mixins.scss */ +.col-xs-offset-6 { + margin-left: 50%; } + +/* line 841, ../sass/bootstrap/_mixins.scss */ +.col-xs-offset-7 { + margin-left: 58.33333%; } + +/* line 841, ../sass/bootstrap/_mixins.scss */ +.col-xs-offset-8 { + margin-left: 66.66667%; } + +/* line 841, ../sass/bootstrap/_mixins.scss */ +.col-xs-offset-9 { + margin-left: 75%; } + +/* line 841, ../sass/bootstrap/_mixins.scss */ +.col-xs-offset-10 { + margin-left: 83.33333%; } + +/* line 841, ../sass/bootstrap/_mixins.scss */ +.col-xs-offset-11 { + margin-left: 91.66667%; } + +/* line 841, ../sass/bootstrap/_mixins.scss */ +.col-xs-offset-12 { + margin-left: 100%; } + +@media (min-width: 768px) { + /* line 818, ../sass/bootstrap/_mixins.scss */ + .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 { + float: left; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-sm-1 { + width: 8.33333%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-sm-2 { + width: 16.66667%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-sm-3 { + width: 25%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-sm-4 { + width: 33.33333%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-sm-5 { + width: 41.66667%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-sm-6 { + width: 50%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-sm-7 { + width: 58.33333%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-sm-8 { + width: 66.66667%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-sm-9 { + width: 75%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-sm-10 { + width: 83.33333%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-sm-11 { + width: 91.66667%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-sm-12 { + width: 100%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-sm-pull-0 { + right: 0%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-sm-pull-1 { + right: 8.33333%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-sm-pull-2 { + right: 16.66667%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-sm-pull-3 { + right: 25%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-sm-pull-4 { + right: 33.33333%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-sm-pull-5 { + right: 41.66667%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-sm-pull-6 { + right: 50%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-sm-pull-7 { + right: 58.33333%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-sm-pull-8 { + right: 66.66667%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-sm-pull-9 { + right: 75%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-sm-pull-10 { + right: 83.33333%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-sm-pull-11 { + right: 91.66667%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-sm-pull-12 { + right: 100%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-sm-push-0 { + left: 0%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-sm-push-1 { + left: 8.33333%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-sm-push-2 { + left: 16.66667%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-sm-push-3 { + left: 25%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-sm-push-4 { + left: 33.33333%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-sm-push-5 { + left: 41.66667%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-sm-push-6 { + left: 50%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-sm-push-7 { + left: 58.33333%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-sm-push-8 { + left: 66.66667%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-sm-push-9 { + left: 75%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-sm-push-10 { + left: 83.33333%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-sm-push-11 { + left: 91.66667%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-sm-push-12 { + left: 100%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-sm-offset-0 { + margin-left: 0%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-sm-offset-1 { + margin-left: 8.33333%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-sm-offset-2 { + margin-left: 16.66667%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-sm-offset-3 { + margin-left: 25%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-sm-offset-4 { + margin-left: 33.33333%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-sm-offset-5 { + margin-left: 41.66667%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-sm-offset-6 { + margin-left: 50%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-sm-offset-7 { + margin-left: 58.33333%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-sm-offset-8 { + margin-left: 66.66667%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-sm-offset-9 { + margin-left: 75%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-sm-offset-10 { + margin-left: 83.33333%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-sm-offset-11 { + margin-left: 91.66667%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-sm-offset-12 { + margin-left: 100%; } } +@media (min-width: 992px) { + /* line 818, ../sass/bootstrap/_mixins.scss */ + .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 { + float: left; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-md-1 { + width: 8.33333%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-md-2 { + width: 16.66667%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-md-3 { + width: 25%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-md-4 { + width: 33.33333%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-md-5 { + width: 41.66667%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-md-6 { + width: 50%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-md-7 { + width: 58.33333%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-md-8 { + width: 66.66667%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-md-9 { + width: 75%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-md-10 { + width: 83.33333%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-md-11 { + width: 91.66667%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-md-12 { + width: 100%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-md-pull-0 { + right: 0%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-md-pull-1 { + right: 8.33333%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-md-pull-2 { + right: 16.66667%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-md-pull-3 { + right: 25%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-md-pull-4 { + right: 33.33333%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-md-pull-5 { + right: 41.66667%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-md-pull-6 { + right: 50%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-md-pull-7 { + right: 58.33333%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-md-pull-8 { + right: 66.66667%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-md-pull-9 { + right: 75%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-md-pull-10 { + right: 83.33333%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-md-pull-11 { + right: 91.66667%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-md-pull-12 { + right: 100%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-md-push-0 { + left: 0%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-md-push-1 { + left: 8.33333%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-md-push-2 { + left: 16.66667%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-md-push-3 { + left: 25%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-md-push-4 { + left: 33.33333%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-md-push-5 { + left: 41.66667%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-md-push-6 { + left: 50%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-md-push-7 { + left: 58.33333%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-md-push-8 { + left: 66.66667%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-md-push-9 { + left: 75%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-md-push-10 { + left: 83.33333%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-md-push-11 { + left: 91.66667%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-md-push-12 { + left: 100%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-md-offset-0 { + margin-left: 0%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-md-offset-1 { + margin-left: 8.33333%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-md-offset-2 { + margin-left: 16.66667%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-md-offset-3 { + margin-left: 25%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-md-offset-4 { + margin-left: 33.33333%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-md-offset-5 { + margin-left: 41.66667%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-md-offset-6 { + margin-left: 50%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-md-offset-7 { + margin-left: 58.33333%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-md-offset-8 { + margin-left: 66.66667%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-md-offset-9 { + margin-left: 75%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-md-offset-10 { + margin-left: 83.33333%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-md-offset-11 { + margin-left: 91.66667%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-md-offset-12 { + margin-left: 100%; } } +@media (min-width: 1200px) { + /* line 818, ../sass/bootstrap/_mixins.scss */ + .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 { + float: left; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-lg-1 { + width: 8.33333%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-lg-2 { + width: 16.66667%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-lg-3 { + width: 25%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-lg-4 { + width: 33.33333%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-lg-5 { + width: 41.66667%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-lg-6 { + width: 50%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-lg-7 { + width: 58.33333%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-lg-8 { + width: 66.66667%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-lg-9 { + width: 75%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-lg-10 { + width: 83.33333%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-lg-11 { + width: 91.66667%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-lg-12 { + width: 100%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-lg-pull-0 { + right: 0%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-lg-pull-1 { + right: 8.33333%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-lg-pull-2 { + right: 16.66667%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-lg-pull-3 { + right: 25%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-lg-pull-4 { + right: 33.33333%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-lg-pull-5 { + right: 41.66667%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-lg-pull-6 { + right: 50%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-lg-pull-7 { + right: 58.33333%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-lg-pull-8 { + right: 66.66667%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-lg-pull-9 { + right: 75%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-lg-pull-10 { + right: 83.33333%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-lg-pull-11 { + right: 91.66667%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-lg-pull-12 { + right: 100%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-lg-push-0 { + left: 0%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-lg-push-1 { + left: 8.33333%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-lg-push-2 { + left: 16.66667%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-lg-push-3 { + left: 25%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-lg-push-4 { + left: 33.33333%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-lg-push-5 { + left: 41.66667%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-lg-push-6 { + left: 50%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-lg-push-7 { + left: 58.33333%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-lg-push-8 { + left: 66.66667%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-lg-push-9 { + left: 75%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-lg-push-10 { + left: 83.33333%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-lg-push-11 { + left: 91.66667%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-lg-push-12 { + left: 100%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-lg-offset-0 { + margin-left: 0%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-lg-offset-1 { + margin-left: 8.33333%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-lg-offset-2 { + margin-left: 16.66667%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-lg-offset-3 { + margin-left: 25%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-lg-offset-4 { + margin-left: 33.33333%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-lg-offset-5 { + margin-left: 41.66667%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-lg-offset-6 { + margin-left: 50%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-lg-offset-7 { + margin-left: 58.33333%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-lg-offset-8 { + margin-left: 66.66667%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-lg-offset-9 { + margin-left: 75%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-lg-offset-10 { + margin-left: 83.33333%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-lg-offset-11 { + margin-left: 91.66667%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-lg-offset-12 { + margin-left: 100%; } } +/* line 6, ../sass/bootstrap/_tables.scss */ +table { + max-width: 100%; + background-color: transparent; } + +/* line 10, ../sass/bootstrap/_tables.scss */ +th { + text-align: left; } + +/* line 17, ../sass/bootstrap/_tables.scss */ +.table { + width: 100%; + margin-bottom: 20px; } + /* line 26, ../sass/bootstrap/_tables.scss */ + .table > thead > tr > th, + .table > thead > tr > td, + .table > tbody > tr > th, + .table > tbody > tr > td, + .table > tfoot > tr > th, + .table > tfoot > tr > td { + padding: 10px; + line-height: 1.42857; + vertical-align: top; + border-top: 1px solid #cccccc; } + /* line 35, ../sass/bootstrap/_tables.scss */ + .table > thead > tr > th { + vertical-align: bottom; + border-bottom: 2px solid #cccccc; } + /* line 45, ../sass/bootstrap/_tables.scss */ + .table > caption + thead > tr:first-child > th, + .table > caption + thead > tr:first-child > td, + .table > colgroup + thead > tr:first-child > th, + .table > colgroup + thead > tr:first-child > td, + .table > thead:first-child > tr:first-child > th, + .table > thead:first-child > tr:first-child > td { + border-top: 0; } + /* line 51, ../sass/bootstrap/_tables.scss */ + .table > tbody + tbody { + border-top: 2px solid #cccccc; } + /* line 56, ../sass/bootstrap/_tables.scss */ + .table .table { + background-color: white; } + +/* line 70, ../sass/bootstrap/_tables.scss */ +.table-condensed > thead > tr > th, +.table-condensed > thead > tr > td, +.table-condensed > tbody > tr > th, +.table-condensed > tbody > tr > td, +.table-condensed > tfoot > tr > th, +.table-condensed > tfoot > tr > td { + padding: 5px; } + +/* line 82, ../sass/bootstrap/_tables.scss */ +.table-bordered { + border: 1px solid #cccccc; } + /* line 89, ../sass/bootstrap/_tables.scss */ + .table-bordered > thead > tr > th, + .table-bordered > thead > tr > td, + .table-bordered > tbody > tr > th, + .table-bordered > tbody > tr > td, + .table-bordered > tfoot > tr > th, + .table-bordered > tfoot > tr > td { + border: 1px solid #cccccc; } + /* line 96, ../sass/bootstrap/_tables.scss */ + .table-bordered > thead > tr > th, + .table-bordered > thead > tr > td { + border-bottom-width: 2px; } + +/* line 110, ../sass/bootstrap/_tables.scss */ +.table-striped > tbody > tr:nth-child(odd) > td, +.table-striped > tbody > tr:nth-child(odd) > th { + background-color: #f9f9f9; } + +/* line 124, ../sass/bootstrap/_tables.scss */ +.table-hover > tbody > tr:hover > td, +.table-hover > tbody > tr:hover > th { + background-color: whitesmoke; } + +/* line 135, ../sass/bootstrap/_tables.scss */ +table col[class*="col-"] { + position: static; + float: none; + display: table-column; } + +/* line 143, ../sass/bootstrap/_tables.scss */ +table td[class*="col-"], +table th[class*="col-"] { + position: static; + float: none; + display: table-cell; } + +/* line 449, ../sass/bootstrap/_mixins.scss */ +.table > thead > tr > td.active, +.table > thead > tr > th.active, .table > thead > tr.active > td, .table > thead > tr.active > th, +.table > tbody > tr > td.active, +.table > tbody > tr > th.active, +.table > tbody > tr.active > td, +.table > tbody > tr.active > th, +.table > tfoot > tr > td.active, +.table > tfoot > tr > th.active, +.table > tfoot > tr.active > td, +.table > tfoot > tr.active > th { + background-color: whitesmoke; } + +/* line 460, ../sass/bootstrap/_mixins.scss */ +.table-hover > tbody > tr > td.active:hover, +.table-hover > tbody > tr > th.active:hover, .table-hover > tbody > tr.active:hover > td, .table-hover > tbody > tr.active:hover > th { + background-color: #e8e8e8; } + +/* line 449, ../sass/bootstrap/_mixins.scss */ +.table > thead > tr > td.success, +.table > thead > tr > th.success, .table > thead > tr.success > td, .table > thead > tr.success > th, +.table > tbody > tr > td.success, +.table > tbody > tr > th.success, +.table > tbody > tr.success > td, +.table > tbody > tr.success > th, +.table > tfoot > tr > td.success, +.table > tfoot > tr > th.success, +.table > tfoot > tr.success > td, +.table > tfoot > tr.success > th { + background-color: #dff0d8; } + +/* line 460, ../sass/bootstrap/_mixins.scss */ +.table-hover > tbody > tr > td.success:hover, +.table-hover > tbody > tr > th.success:hover, .table-hover > tbody > tr.success:hover > td, .table-hover > tbody > tr.success:hover > th { + background-color: #d0e9c6; } + +/* line 449, ../sass/bootstrap/_mixins.scss */ +.table > thead > tr > td.info, +.table > thead > tr > th.info, .table > thead > tr.info > td, .table > thead > tr.info > th, +.table > tbody > tr > td.info, +.table > tbody > tr > th.info, +.table > tbody > tr.info > td, +.table > tbody > tr.info > th, +.table > tfoot > tr > td.info, +.table > tfoot > tr > th.info, +.table > tfoot > tr.info > td, +.table > tfoot > tr.info > th { + background-color: #d9edf7; } + +/* line 460, ../sass/bootstrap/_mixins.scss */ +.table-hover > tbody > tr > td.info:hover, +.table-hover > tbody > tr > th.info:hover, .table-hover > tbody > tr.info:hover > td, .table-hover > tbody > tr.info:hover > th { + background-color: #c4e3f3; } + +/* line 449, ../sass/bootstrap/_mixins.scss */ +.table > thead > tr > td.warning, +.table > thead > tr > th.warning, .table > thead > tr.warning > td, .table > thead > tr.warning > th, +.table > tbody > tr > td.warning, +.table > tbody > tr > th.warning, +.table > tbody > tr.warning > td, +.table > tbody > tr.warning > th, +.table > tfoot > tr > td.warning, +.table > tfoot > tr > th.warning, +.table > tfoot > tr.warning > td, +.table > tfoot > tr.warning > th { + background-color: #fcf8e3; } + +/* line 460, ../sass/bootstrap/_mixins.scss */ +.table-hover > tbody > tr > td.warning:hover, +.table-hover > tbody > tr > th.warning:hover, .table-hover > tbody > tr.warning:hover > td, .table-hover > tbody > tr.warning:hover > th { + background-color: #faf2cc; } + +/* line 449, ../sass/bootstrap/_mixins.scss */ +.table > thead > tr > td.danger, +.table > thead > tr > th.danger, .table > thead > tr.danger > td, .table > thead > tr.danger > th, +.table > tbody > tr > td.danger, +.table > tbody > tr > th.danger, +.table > tbody > tr.danger > td, +.table > tbody > tr.danger > th, +.table > tfoot > tr > td.danger, +.table > tfoot > tr > th.danger, +.table > tfoot > tr.danger > td, +.table > tfoot > tr.danger > th { + background-color: #f2dede; } + +/* line 460, ../sass/bootstrap/_mixins.scss */ +.table-hover > tbody > tr > td.danger:hover, +.table-hover > tbody > tr > th.danger:hover, .table-hover > tbody > tr.danger:hover > td, .table-hover > tbody > tr.danger:hover > th { + background-color: #ebcccc; } + +@media (max-width: 767px) { + /* line 172, ../sass/bootstrap/_tables.scss */ + .table-responsive { + width: 100%; + margin-bottom: 15px; + overflow-y: hidden; + overflow-x: scroll; + -ms-overflow-style: -ms-autohiding-scrollbar; + border: 1px solid #cccccc; + -webkit-overflow-scrolling: touch; } + /* line 182, ../sass/bootstrap/_tables.scss */ + .table-responsive > .table { + margin-bottom: 0; } + /* line 191, ../sass/bootstrap/_tables.scss */ + .table-responsive > .table > thead > tr > th, + .table-responsive > .table > thead > tr > td, + .table-responsive > .table > tbody > tr > th, + .table-responsive > .table > tbody > tr > td, + .table-responsive > .table > tfoot > tr > th, + .table-responsive > .table > tfoot > tr > td { + white-space: nowrap; } + /* line 199, ../sass/bootstrap/_tables.scss */ + .table-responsive > .table-bordered { + border: 0; } + /* line 208, ../sass/bootstrap/_tables.scss */ + .table-responsive > .table-bordered > thead > tr > th:first-child, + .table-responsive > .table-bordered > thead > tr > td:first-child, + .table-responsive > .table-bordered > tbody > tr > th:first-child, + .table-responsive > .table-bordered > tbody > tr > td:first-child, + .table-responsive > .table-bordered > tfoot > tr > th:first-child, + .table-responsive > .table-bordered > tfoot > tr > td:first-child { + border-left: 0; } + /* line 212, ../sass/bootstrap/_tables.scss */ + .table-responsive > .table-bordered > thead > tr > th:last-child, + .table-responsive > .table-bordered > thead > tr > td:last-child, + .table-responsive > .table-bordered > tbody > tr > th:last-child, + .table-responsive > .table-bordered > tbody > tr > td:last-child, + .table-responsive > .table-bordered > tfoot > tr > th:last-child, + .table-responsive > .table-bordered > tfoot > tr > td:last-child { + border-right: 0; } + /* line 225, ../sass/bootstrap/_tables.scss */ + .table-responsive > .table-bordered > tbody > tr:last-child > th, + .table-responsive > .table-bordered > tbody > tr:last-child > td, + .table-responsive > .table-bordered > tfoot > tr:last-child > th, + .table-responsive > .table-bordered > tfoot > tr:last-child > td { + border-bottom: 0; } } +/* line 10, ../sass/bootstrap/_forms.scss */ +fieldset { + padding: 0; + margin: 0; + border: 0; + min-width: 0; } + +/* line 20, ../sass/bootstrap/_forms.scss */ +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: 20px; + font-size: 21px; + line-height: inherit; + color: #303030; + border: 0; + border-bottom: 1px solid #e5e5e5; } + +/* line 32, ../sass/bootstrap/_forms.scss */ +label { + display: inline-block; + margin-bottom: 5px; + font-weight: bold; } + +/* line 46, ../sass/bootstrap/_forms.scss */ +input[type="search"] { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } + +/* line 52, ../sass/bootstrap/_forms.scss */ +input[type="radio"], +input[type="checkbox"] { + margin: 4px 0 0; + margin-top: 1px \9; + /* IE8-9 */ + line-height: normal; } + +/* line 59, ../sass/bootstrap/_forms.scss */ +input[type="file"] { + display: block; } + +/* line 64, ../sass/bootstrap/_forms.scss */ +input[type="range"] { + display: block; + width: 100%; } + +/* line 71, ../sass/bootstrap/_forms.scss */ +select[multiple], +select[size] { + height: auto; } + +/* line 78, ../sass/bootstrap/_forms.scss */ +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { + outline: 0 none; } + +/* line 83, ../sass/bootstrap/_forms.scss */ +output { + display: block; + padding-top: 7px; + font-size: 14px; + line-height: 1.42857; + color: #555555; } + +/* line 114, ../sass/bootstrap/_forms.scss */ +.form-control { + display: block; + width: 100%; + height: 34px; + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857; + color: #555555; + background-color: white; + background-image: none; + border: 1px solid #cccccc; + border-radius: 0; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -webkit-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; + transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; } + /* line 916, ../sass/bootstrap/_mixins.scss */ + .form-control:focus { + border-color: #66afe9; + outline: 0; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); } + /* line 57, ../sass/bootstrap/_mixins.scss */ + .form-control::-moz-placeholder { + color: #4e4e4e; + opacity: 1; } + /* line 59, ../sass/bootstrap/_mixins.scss */ + .form-control:-ms-input-placeholder { + color: #4e4e4e; } + /* line 60, ../sass/bootstrap/_mixins.scss */ + .form-control::-webkit-input-placeholder { + color: #4e4e4e; } + /* line 142, ../sass/bootstrap/_forms.scss */ + .form-control[disabled], .form-control[readonly], fieldset[disabled] .form-control { + cursor: not-allowed; + background-color: #d9d9d9; + opacity: 1; } + +/* line 152, ../sass/bootstrap/_forms.scss */ +textarea.form-control { + height: auto; } + +/* line 164, ../sass/bootstrap/_forms.scss */ +input[type="search"] { + -webkit-appearance: none; } + +/* line 174, ../sass/bootstrap/_forms.scss */ +input[type="date"] { + line-height: 34px; } + +/* line 184, ../sass/bootstrap/_forms.scss */ +.form-group { + margin-bottom: 15px; } + +/* line 194, ../sass/bootstrap/_forms.scss */ +.radio, +.checkbox { + display: block; + min-height: 20px; + margin-top: 10px; + margin-bottom: 10px; + padding-left: 20px; } + /* line 200, ../sass/bootstrap/_forms.scss */ + .radio label, + .checkbox label { + display: inline; + font-weight: normal; + cursor: pointer; } + +/* line 209, ../sass/bootstrap/_forms.scss */ +.radio input[type="radio"], +.radio-inline input[type="radio"], +.checkbox input[type="checkbox"], +.checkbox-inline input[type="checkbox"] { + float: left; + margin-left: -20px; } + +/* line 214, ../sass/bootstrap/_forms.scss */ +.radio + .radio, +.checkbox + .checkbox { + margin-top: -5px; } + +/* line 220, ../sass/bootstrap/_forms.scss */ +.radio-inline, +.checkbox-inline { + display: inline-block; + padding-left: 20px; + margin-bottom: 0; + vertical-align: middle; + font-weight: normal; + cursor: pointer; } + +/* line 229, ../sass/bootstrap/_forms.scss */ +.radio-inline + .radio-inline, +.checkbox-inline + .checkbox-inline { + margin-top: 0; + margin-left: 10px; } + +/* line 244, ../sass/bootstrap/_forms.scss */ +input[type="radio"][disabled], fieldset[disabled] input[type="radio"], +input[type="checkbox"][disabled], fieldset[disabled] +input[type="checkbox"], +.radio[disabled], fieldset[disabled] +.radio, +.radio-inline[disabled], fieldset[disabled] +.radio-inline, +.checkbox[disabled], fieldset[disabled] +.checkbox, +.checkbox-inline[disabled], fieldset[disabled] +.checkbox-inline { + cursor: not-allowed; } + +/* line 931, ../sass/bootstrap/_mixins.scss */ +.input-sm, .input-group-sm > .form-control, +.input-group-sm > .input-group-addon, +.input-group-sm > .input-group-btn > .btn { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; } + +/* line 939, ../sass/bootstrap/_mixins.scss */ +select.input-sm, .input-group-sm > select.form-control, +.input-group-sm > select.input-group-addon, +.input-group-sm > .input-group-btn > select.btn { + height: 30px; + line-height: 30px; } + +/* line 945, ../sass/bootstrap/_mixins.scss */ +textarea.input-sm, .input-group-sm > textarea.form-control, +.input-group-sm > textarea.input-group-addon, +.input-group-sm > .input-group-btn > textarea.btn, +select[multiple].input-sm, +.input-group-sm > select[multiple].form-control, +.input-group-sm > select[multiple].input-group-addon, +.input-group-sm > .input-group-btn > select[multiple].btn { + height: auto; } + +/* line 931, ../sass/bootstrap/_mixins.scss */ +.input-lg, .input-group-lg > .form-control, +.input-group-lg > .input-group-addon, +.input-group-lg > .input-group-btn > .btn { + height: 46px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.33; + border-radius: 6px; } + +/* line 939, ../sass/bootstrap/_mixins.scss */ +select.input-lg, .input-group-lg > select.form-control, +.input-group-lg > select.input-group-addon, +.input-group-lg > .input-group-btn > select.btn { + height: 46px; + line-height: 46px; } + +/* line 945, ../sass/bootstrap/_mixins.scss */ +textarea.input-lg, .input-group-lg > textarea.form-control, +.input-group-lg > textarea.input-group-addon, +.input-group-lg > .input-group-btn > textarea.btn, +select[multiple].input-lg, +.input-group-lg > select[multiple].form-control, +.input-group-lg > select[multiple].input-group-addon, +.input-group-lg > .input-group-btn > select[multiple].btn { + height: auto; } + +/* line 264, ../sass/bootstrap/_forms.scss */ +.has-feedback { + position: relative; } + /* line 269, ../sass/bootstrap/_forms.scss */ + .has-feedback .form-control { + padding-right: 42.5px; } + /* line 274, ../sass/bootstrap/_forms.scss */ + .has-feedback .form-control-feedback { + position: absolute; + top: 25px; + right: 0; + display: block; + width: 34px; + height: 34px; + line-height: 34px; + text-align: center; } + +/* line 876, ../sass/bootstrap/_mixins.scss */ +.has-success .help-block, +.has-success .control-label, +.has-success .radio, +.has-success .checkbox, +.has-success .radio-inline, +.has-success .checkbox-inline { + color: #3c763d; } +/* line 880, ../sass/bootstrap/_mixins.scss */ +.has-success .form-control { + border-color: #3c763d; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); } + /* line 883, ../sass/bootstrap/_mixins.scss */ + .has-success .form-control:focus { + border-color: #2b542c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168; } +/* line 890, ../sass/bootstrap/_mixins.scss */ +.has-success .input-group-addon { + color: #3c763d; + border-color: #3c763d; + background-color: #dff0d8; } +/* line 896, ../sass/bootstrap/_mixins.scss */ +.has-success .form-control-feedback { + color: #3c763d; } + +/* line 876, ../sass/bootstrap/_mixins.scss */ +.has-warning .help-block, +.has-warning .control-label, +.has-warning .radio, +.has-warning .checkbox, +.has-warning .radio-inline, +.has-warning .checkbox-inline { + color: #8a6d3b; } +/* line 880, ../sass/bootstrap/_mixins.scss */ +.has-warning .form-control { + border-color: #8a6d3b; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); } + /* line 883, ../sass/bootstrap/_mixins.scss */ + .has-warning .form-control:focus { + border-color: #66512c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b; } +/* line 890, ../sass/bootstrap/_mixins.scss */ +.has-warning .input-group-addon { + color: #8a6d3b; + border-color: #8a6d3b; + background-color: #fcf8e3; } +/* line 896, ../sass/bootstrap/_mixins.scss */ +.has-warning .form-control-feedback { + color: #8a6d3b; } + +/* line 876, ../sass/bootstrap/_mixins.scss */ +.has-error .help-block, +.has-error .control-label, +.has-error .radio, +.has-error .checkbox, +.has-error .radio-inline, +.has-error .checkbox-inline { + color: #a94442; } +/* line 880, ../sass/bootstrap/_mixins.scss */ +.has-error .form-control { + border-color: #a94442; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); } + /* line 883, ../sass/bootstrap/_mixins.scss */ + .has-error .form-control:focus { + border-color: #843534; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483; } +/* line 890, ../sass/bootstrap/_mixins.scss */ +.has-error .input-group-addon { + color: #a94442; + border-color: #a94442; + background-color: #f2dede; } +/* line 896, ../sass/bootstrap/_mixins.scss */ +.has-error .form-control-feedback { + color: #a94442; } + +/* line 303, ../sass/bootstrap/_forms.scss */ +.form-control-static { + margin-bottom: 0; } + +/* line 313, ../sass/bootstrap/_forms.scss */ +.help-block { + display: block; + margin-top: 5px; + margin-bottom: 10px; + color: #626262; } + +@media (min-width: 768px) { + /* line 338, ../sass/bootstrap/_forms.scss */ + .form-inline .form-group, .navbar-form .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; } + /* line 345, ../sass/bootstrap/_forms.scss */ + .form-inline .form-control, .navbar-form .form-control { + display: inline-block; + width: auto; + vertical-align: middle; } + /* line 351, ../sass/bootstrap/_forms.scss */ + .form-inline .input-group > .form-control, .navbar-form .input-group > .form-control { + width: 100%; } + /* line 355, ../sass/bootstrap/_forms.scss */ + .form-inline .control-label, .navbar-form .control-label { + margin-bottom: 0; + vertical-align: middle; } + /* line 364, ../sass/bootstrap/_forms.scss */ + .form-inline .radio, .navbar-form .radio, + .form-inline .checkbox, + .navbar-form .checkbox { + display: inline-block; + margin-top: 0; + margin-bottom: 0; + padding-left: 0; + vertical-align: middle; } + /* line 372, ../sass/bootstrap/_forms.scss */ + .form-inline .radio input[type="radio"], .navbar-form .radio input[type="radio"], + .form-inline .checkbox input[type="checkbox"], + .navbar-form .checkbox input[type="checkbox"] { + float: none; + margin-left: 0; } + /* line 381, ../sass/bootstrap/_forms.scss */ + .form-inline .has-feedback .form-control-feedback, .navbar-form .has-feedback .form-control-feedback { + top: 0; } } + +/* line 400, ../sass/bootstrap/_forms.scss */ +.form-horizontal .control-label, +.form-horizontal .radio, +.form-horizontal .checkbox, +.form-horizontal .radio-inline, +.form-horizontal .checkbox-inline { + margin-top: 0; + margin-bottom: 0; + padding-top: 7px; } +/* line 408, ../sass/bootstrap/_forms.scss */ +.form-horizontal .radio, +.form-horizontal .checkbox { + min-height: 27px; } +/* line 413, ../sass/bootstrap/_forms.scss */ +.form-horizontal .form-group { + margin-left: -15px; + margin-right: -15px; } + /* line 21, ../sass/bootstrap/_mixins.scss */ + .form-horizontal .form-group:before, .form-horizontal .form-group:after { + content: " "; + display: table; } + /* line 25, ../sass/bootstrap/_mixins.scss */ + .form-horizontal .form-group:after { + clear: both; } +/* line 417, ../sass/bootstrap/_forms.scss */ +.form-horizontal .form-control-static { + padding-top: 7px; } +@media (min-width: 768px) { + /* line 423, ../sass/bootstrap/_forms.scss */ + .form-horizontal .control-label { + text-align: right; } } +/* line 432, ../sass/bootstrap/_forms.scss */ +.form-horizontal .has-feedback .form-control-feedback { + top: 0; + right: 15px; } + +/* line 9, ../sass/bootstrap/_buttons.scss */ +.btn { + display: inline-block; + margin-bottom: 0; + font-weight: normal; + text-align: center; + vertical-align: middle; + cursor: pointer; + background-image: none; + border: 1px solid transparent; + white-space: nowrap; + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857; + border-radius: 0; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } + /* line 25, ../sass/bootstrap/_buttons.scss */ + .btn:focus, .btn:active:focus, .btn.active:focus { + outline: 0 none; } + /* line 31, ../sass/bootstrap/_buttons.scss */ + .btn:hover, .btn:focus { + color: #333333; + text-decoration: none; } + /* line 37, ../sass/bootstrap/_buttons.scss */ + .btn:active, .btn.active { + outline: 0; + background-image: none; + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); } + /* line 45, ../sass/bootstrap/_buttons.scss */ + .btn.disabled, .btn[disabled], fieldset[disabled] .btn { + cursor: not-allowed; + pointer-events: none; + opacity: 0.65; + filter: alpha(opacity=65); + -webkit-box-shadow: none; + box-shadow: none; } + +/* line 57, ../sass/bootstrap/_buttons.scss */ +.btn-default { + color: #333333; + background-color: white; + border-color: #cccccc; } + /* line 508, ../sass/bootstrap/_mixins.scss */ + .btn-default:hover, .btn-default:focus, .btn-default:active, .btn-default.active { + color: #333333; + background-color: #ebebeb; + border-color: #adadad; } + /* line 513, ../sass/bootstrap/_mixins.scss */ + .open .btn-default.dropdown-toggle { + color: #333333; + background-color: #ebebeb; + border-color: #adadad; } + /* line 519, ../sass/bootstrap/_mixins.scss */ + .btn-default:active, .btn-default.active { + background-image: none; } + /* line 522, ../sass/bootstrap/_mixins.scss */ + .open .btn-default.dropdown-toggle { + background-image: none; } + /* line 532, ../sass/bootstrap/_mixins.scss */ + .btn-default.disabled, .btn-default.disabled:hover, .btn-default.disabled:focus, .btn-default.disabled:active, .btn-default.disabled.active, .btn-default[disabled], .btn-default[disabled]:hover, .btn-default[disabled]:focus, .btn-default[disabled]:active, .btn-default[disabled].active, fieldset[disabled] .btn-default, fieldset[disabled] .btn-default:hover, fieldset[disabled] .btn-default:focus, fieldset[disabled] .btn-default:active, fieldset[disabled] .btn-default.active { + background-color: white; + border-color: #cccccc; } + /* line 538, ../sass/bootstrap/_mixins.scss */ + .btn-default .badge { + color: white; + background-color: #333333; } + +/* line 60, ../sass/bootstrap/_buttons.scss */ +.btn-primary { + color: white; + background-color: white; + border-color: #f2f2f2; } + /* line 508, ../sass/bootstrap/_mixins.scss */ + .btn-primary:hover, .btn-primary:focus, .btn-primary:active, .btn-primary.active { + color: white; + background-color: #ebebeb; + border-color: #d4d4d4; } + /* line 513, ../sass/bootstrap/_mixins.scss */ + .open .btn-primary.dropdown-toggle { + color: white; + background-color: #ebebeb; + border-color: #d4d4d4; } + /* line 519, ../sass/bootstrap/_mixins.scss */ + .btn-primary:active, .btn-primary.active { + background-image: none; } + /* line 522, ../sass/bootstrap/_mixins.scss */ + .open .btn-primary.dropdown-toggle { + background-image: none; } + /* line 532, ../sass/bootstrap/_mixins.scss */ + .btn-primary.disabled, .btn-primary.disabled:hover, .btn-primary.disabled:focus, .btn-primary.disabled:active, .btn-primary.disabled.active, .btn-primary[disabled], .btn-primary[disabled]:hover, .btn-primary[disabled]:focus, .btn-primary[disabled]:active, .btn-primary[disabled].active, fieldset[disabled] .btn-primary, fieldset[disabled] .btn-primary:hover, fieldset[disabled] .btn-primary:focus, fieldset[disabled] .btn-primary:active, fieldset[disabled] .btn-primary.active { + background-color: white; + border-color: #f2f2f2; } + /* line 538, ../sass/bootstrap/_mixins.scss */ + .btn-primary .badge { + color: white; + background-color: white; } + +/* line 64, ../sass/bootstrap/_buttons.scss */ +.btn-success { + color: white; + background-color: #5cb85c; + border-color: #4cae4c; } + /* line 508, ../sass/bootstrap/_mixins.scss */ + .btn-success:hover, .btn-success:focus, .btn-success:active, .btn-success.active { + color: white; + background-color: #47a447; + border-color: #398439; } + /* line 513, ../sass/bootstrap/_mixins.scss */ + .open .btn-success.dropdown-toggle { + color: white; + background-color: #47a447; + border-color: #398439; } + /* line 519, ../sass/bootstrap/_mixins.scss */ + .btn-success:active, .btn-success.active { + background-image: none; } + /* line 522, ../sass/bootstrap/_mixins.scss */ + .open .btn-success.dropdown-toggle { + background-image: none; } + /* line 532, ../sass/bootstrap/_mixins.scss */ + .btn-success.disabled, .btn-success.disabled:hover, .btn-success.disabled:focus, .btn-success.disabled:active, .btn-success.disabled.active, .btn-success[disabled], .btn-success[disabled]:hover, .btn-success[disabled]:focus, .btn-success[disabled]:active, .btn-success[disabled].active, fieldset[disabled] .btn-success, fieldset[disabled] .btn-success:hover, fieldset[disabled] .btn-success:focus, fieldset[disabled] .btn-success:active, fieldset[disabled] .btn-success.active { + background-color: #5cb85c; + border-color: #4cae4c; } + /* line 538, ../sass/bootstrap/_mixins.scss */ + .btn-success .badge { + color: #5cb85c; + background-color: white; } + +/* line 68, ../sass/bootstrap/_buttons.scss */ +.btn-info { + color: white; + background-color: #5bc0de; + border-color: #46b8da; } + /* line 508, ../sass/bootstrap/_mixins.scss */ + .btn-info:hover, .btn-info:focus, .btn-info:active, .btn-info.active { + color: white; + background-color: #39b3d7; + border-color: #269abc; } + /* line 513, ../sass/bootstrap/_mixins.scss */ + .open .btn-info.dropdown-toggle { + color: white; + background-color: #39b3d7; + border-color: #269abc; } + /* line 519, ../sass/bootstrap/_mixins.scss */ + .btn-info:active, .btn-info.active { + background-image: none; } + /* line 522, ../sass/bootstrap/_mixins.scss */ + .open .btn-info.dropdown-toggle { + background-image: none; } + /* line 532, ../sass/bootstrap/_mixins.scss */ + .btn-info.disabled, .btn-info.disabled:hover, .btn-info.disabled:focus, .btn-info.disabled:active, .btn-info.disabled.active, .btn-info[disabled], .btn-info[disabled]:hover, .btn-info[disabled]:focus, .btn-info[disabled]:active, .btn-info[disabled].active, fieldset[disabled] .btn-info, fieldset[disabled] .btn-info:hover, fieldset[disabled] .btn-info:focus, fieldset[disabled] .btn-info:active, fieldset[disabled] .btn-info.active { + background-color: #5bc0de; + border-color: #46b8da; } + /* line 538, ../sass/bootstrap/_mixins.scss */ + .btn-info .badge { + color: #5bc0de; + background-color: white; } + +/* line 72, ../sass/bootstrap/_buttons.scss */ +.btn-warning { + color: white; + background-color: #f0ad4e; + border-color: #eea236; } + /* line 508, ../sass/bootstrap/_mixins.scss */ + .btn-warning:hover, .btn-warning:focus, .btn-warning:active, .btn-warning.active { + color: white; + background-color: #ed9c28; + border-color: #d58512; } + /* line 513, ../sass/bootstrap/_mixins.scss */ + .open .btn-warning.dropdown-toggle { + color: white; + background-color: #ed9c28; + border-color: #d58512; } + /* line 519, ../sass/bootstrap/_mixins.scss */ + .btn-warning:active, .btn-warning.active { + background-image: none; } + /* line 522, ../sass/bootstrap/_mixins.scss */ + .open .btn-warning.dropdown-toggle { + background-image: none; } + /* line 532, ../sass/bootstrap/_mixins.scss */ + .btn-warning.disabled, .btn-warning.disabled:hover, .btn-warning.disabled:focus, .btn-warning.disabled:active, .btn-warning.disabled.active, .btn-warning[disabled], .btn-warning[disabled]:hover, .btn-warning[disabled]:focus, .btn-warning[disabled]:active, .btn-warning[disabled].active, fieldset[disabled] .btn-warning, fieldset[disabled] .btn-warning:hover, fieldset[disabled] .btn-warning:focus, fieldset[disabled] .btn-warning:active, fieldset[disabled] .btn-warning.active { + background-color: #f0ad4e; + border-color: #eea236; } + /* line 538, ../sass/bootstrap/_mixins.scss */ + .btn-warning .badge { + color: #f0ad4e; + background-color: white; } + +/* line 76, ../sass/bootstrap/_buttons.scss */ +.btn-danger { + color: white; + background-color: #d9534f; + border-color: #d43f3a; } + /* line 508, ../sass/bootstrap/_mixins.scss */ + .btn-danger:hover, .btn-danger:focus, .btn-danger:active, .btn-danger.active { + color: white; + background-color: #d2322d; + border-color: #ac2925; } + /* line 513, ../sass/bootstrap/_mixins.scss */ + .open .btn-danger.dropdown-toggle { + color: white; + background-color: #d2322d; + border-color: #ac2925; } + /* line 519, ../sass/bootstrap/_mixins.scss */ + .btn-danger:active, .btn-danger.active { + background-image: none; } + /* line 522, ../sass/bootstrap/_mixins.scss */ + .open .btn-danger.dropdown-toggle { + background-image: none; } + /* line 532, ../sass/bootstrap/_mixins.scss */ + .btn-danger.disabled, .btn-danger.disabled:hover, .btn-danger.disabled:focus, .btn-danger.disabled:active, .btn-danger.disabled.active, .btn-danger[disabled], .btn-danger[disabled]:hover, .btn-danger[disabled]:focus, .btn-danger[disabled]:active, .btn-danger[disabled].active, fieldset[disabled] .btn-danger, fieldset[disabled] .btn-danger:hover, fieldset[disabled] .btn-danger:focus, fieldset[disabled] .btn-danger:active, fieldset[disabled] .btn-danger.active { + background-color: #d9534f; + border-color: #d43f3a; } + /* line 538, ../sass/bootstrap/_mixins.scss */ + .btn-danger .badge { + color: #d9534f; + background-color: white; } + +/* line 85, ../sass/bootstrap/_buttons.scss */ +.btn-link { + color: #005b9a; + font-weight: normal; + cursor: pointer; + border-radius: 0; } + /* line 94, ../sass/bootstrap/_buttons.scss */ + .btn-link, .btn-link:active, .btn-link[disabled], fieldset[disabled] .btn-link { + background-color: transparent; + -webkit-box-shadow: none; + box-shadow: none; } + /* line 101, ../sass/bootstrap/_buttons.scss */ + .btn-link, .btn-link:hover, .btn-link:focus, .btn-link:active { + border-color: transparent; } + /* line 105, ../sass/bootstrap/_buttons.scss */ + .btn-link:hover, .btn-link:focus { + color: #ee5161; + text-decoration: underline; + background-color: transparent; } + /* line 113, ../sass/bootstrap/_buttons.scss */ + .btn-link[disabled]:hover, .btn-link[disabled]:focus, fieldset[disabled] .btn-link:hover, fieldset[disabled] .btn-link:focus { + color: #818181; + text-decoration: none; } + +/* line 124, ../sass/bootstrap/_buttons.scss */ +.btn-lg, .btn-group-lg > .btn { + padding: 10px 16px; + font-size: 18px; + line-height: 1.33; + border-radius: 6px; } + +/* line 128, ../sass/bootstrap/_buttons.scss */ +.btn-sm, .btn-group-sm > .btn { + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; } + +/* line 132, ../sass/bootstrap/_buttons.scss */ +.btn-xs, .btn-group-xs > .btn { + padding: 1px 5px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; } + +/* line 140, ../sass/bootstrap/_buttons.scss */ +.btn-block { + display: block; + width: 100%; + padding-left: 0; + padding-right: 0; } + +/* line 148, ../sass/bootstrap/_buttons.scss */ +.btn-block + .btn-block { + margin-top: 5px; } + +/* line 156, ../sass/bootstrap/_buttons.scss */ +input[type="submit"].btn-block, +input[type="reset"].btn-block, +input[type="button"].btn-block { + width: 100%; } + +/* line 10, ../sass/bootstrap/_component-animations.scss */ +.fade { + opacity: 0; + -webkit-transition: opacity 0.15s linear; + transition: opacity 0.15s linear; } + /* line 13, ../sass/bootstrap/_component-animations.scss */ + .fade.in { + opacity: 1; } + +/* line 18, ../sass/bootstrap/_component-animations.scss */ +.collapse { + display: none; } + /* line 20, ../sass/bootstrap/_component-animations.scss */ + .collapse.in { + display: block; } + +/* line 24, ../sass/bootstrap/_component-animations.scss */ +.collapsing { + position: relative; + height: 0; + overflow: hidden; + -webkit-transition: height 0.35s ease; + transition: height 0.35s ease; } + +/* line 7, ../sass/bootstrap/_dropdowns.scss */ +.caret { + display: inline-block; + width: 0; + height: 0; + margin-left: 2px; + vertical-align: middle; + border-top: 4px solid; + border-right: 4px solid transparent; + border-left: 4px solid transparent; } + +/* line 19, ../sass/bootstrap/_dropdowns.scss */ +.dropdown { + position: relative; } + +/* line 24, ../sass/bootstrap/_dropdowns.scss */ +.dropdown-toggle:focus { + outline: 0; } + +/* line 29, ../sass/bootstrap/_dropdowns.scss */ +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + list-style: none; + font-size: 14px; + background-color: white; + border: 1px solid #cccccc; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 0; + -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + background-clip: padding-box; } + /* line 51, ../sass/bootstrap/_dropdowns.scss */ + .dropdown-menu.pull-right { + right: 0; + left: auto; } + /* line 57, ../sass/bootstrap/_dropdowns.scss */ + .dropdown-menu .divider { + height: 1px; + margin: 9px 0; + overflow: hidden; + background-color: #e5e5e5; } + /* line 62, ../sass/bootstrap/_dropdowns.scss */ + .dropdown-menu > li > a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 1.42857; + color: #303030; + white-space: nowrap; } + +/* line 76, ../sass/bootstrap/_dropdowns.scss */ +.dropdown-menu > li > a:hover, .dropdown-menu > li > a:focus { + text-decoration: none; + color: #303030; + background-color: #d9d9d9; } + +/* line 87, ../sass/bootstrap/_dropdowns.scss */ +.dropdown-menu > .active > a, .dropdown-menu > .active > a:hover, .dropdown-menu > .active > a:focus { + color: white; + text-decoration: none; + outline: 0; + background-color: #ee5161; } + +/* line 102, ../sass/bootstrap/_dropdowns.scss */ +.dropdown-menu > .disabled > a, .dropdown-menu > .disabled > a:hover, .dropdown-menu > .disabled > a:focus { + color: #4e4e4e; } + +/* line 109, ../sass/bootstrap/_dropdowns.scss */ +.dropdown-menu > .disabled > a:hover, .dropdown-menu > .disabled > a:focus { + text-decoration: none; + background-color: transparent; + background-image: none; + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + cursor: not-allowed; } + +/* line 121, ../sass/bootstrap/_dropdowns.scss */ +.open > .dropdown-menu { + display: block; } +/* line 126, ../sass/bootstrap/_dropdowns.scss */ +.open > a { + outline: 0; } + +/* line 135, ../sass/bootstrap/_dropdowns.scss */ +.dropdown-menu-right { + left: auto; + right: 0; } + +/* line 145, ../sass/bootstrap/_dropdowns.scss */ +.dropdown-menu-left { + left: 0; + right: auto; } + +/* line 151, ../sass/bootstrap/_dropdowns.scss */ +.dropdown-header { + display: block; + padding: 3px 20px; + font-size: 12px; + line-height: 1.42857; + color: #4e4e4e; } + +/* line 160, ../sass/bootstrap/_dropdowns.scss */ +.dropdown-backdrop { + position: fixed; + left: 0; + right: 0; + bottom: 0; + top: 0; + z-index: 990; } + +/* line 170, ../sass/bootstrap/_dropdowns.scss */ +.pull-right > .dropdown-menu { + right: 0; + left: auto; } + +/* line 183, ../sass/bootstrap/_dropdowns.scss */ +.dropup .caret, +.navbar-fixed-bottom .dropdown .caret { + border-top: 0; + border-bottom: 4px solid; + content: ""; } +/* line 189, ../sass/bootstrap/_dropdowns.scss */ +.dropup .dropdown-menu, +.navbar-fixed-bottom .dropdown .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 1px; } + +@media (min-width: 768px) { + /* line 203, ../sass/bootstrap/_dropdowns.scss */ + .navbar-right .dropdown-menu { + right: 0; + left: auto; } + /* line 208, ../sass/bootstrap/_dropdowns.scss */ + .navbar-right .dropdown-menu-left { + left: 0; + right: auto; } } +/* line 7, ../sass/bootstrap/_button-groups.scss */ +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-block; + vertical-align: middle; } + /* line 11, ../sass/bootstrap/_button-groups.scss */ + .btn-group > .btn, + .btn-group-vertical > .btn { + position: relative; + float: left; } + /* line 18, ../sass/bootstrap/_button-groups.scss */ + .btn-group > .btn:hover, .btn-group > .btn:focus, .btn-group > .btn:active, .btn-group > .btn.active, + .btn-group-vertical > .btn:hover, + .btn-group-vertical > .btn:focus, + .btn-group-vertical > .btn:active, + .btn-group-vertical > .btn.active { + z-index: 2; } + /* line 21, ../sass/bootstrap/_button-groups.scss */ + .btn-group > .btn:focus, + .btn-group-vertical > .btn:focus { + outline: none; } + +/* line 33, ../sass/bootstrap/_button-groups.scss */ +.btn-group .btn + .btn, +.btn-group .btn + .btn-group, +.btn-group .btn-group + .btn, +.btn-group .btn-group + .btn-group { + margin-left: -1px; } + +/* line 39, ../sass/bootstrap/_button-groups.scss */ +.btn-toolbar { + margin-left: -5px; } + /* line 21, ../sass/bootstrap/_mixins.scss */ + .btn-toolbar:before, .btn-toolbar:after { + content: " "; + display: table; } + /* line 25, ../sass/bootstrap/_mixins.scss */ + .btn-toolbar:after { + clear: both; } + /* line 44, ../sass/bootstrap/_button-groups.scss */ + .btn-toolbar .btn-group, + .btn-toolbar .input-group { + float: left; } + /* line 49, ../sass/bootstrap/_button-groups.scss */ + .btn-toolbar > .btn, + .btn-toolbar > .btn-group, + .btn-toolbar > .input-group { + margin-left: 5px; } + +/* line 54, ../sass/bootstrap/_button-groups.scss */ +.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { + border-radius: 0; } + +/* line 59, ../sass/bootstrap/_button-groups.scss */ +.btn-group > .btn:first-child { + margin-left: 0; } + /* line 61, ../sass/bootstrap/_button-groups.scss */ + .btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) { + border-bottom-right-radius: 0; + border-top-right-radius: 0; } + +/* line 67, ../sass/bootstrap/_button-groups.scss */ +.btn-group > .btn:last-child:not(:first-child), +.btn-group > .dropdown-toggle:not(:first-child) { + border-bottom-left-radius: 0; + border-top-left-radius: 0; } + +/* line 72, ../sass/bootstrap/_button-groups.scss */ +.btn-group > .btn-group { + float: left; } + +/* line 75, ../sass/bootstrap/_button-groups.scss */ +.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; } + +/* line 80, ../sass/bootstrap/_button-groups.scss */ +.btn-group > .btn-group:first-child > .btn:last-child, +.btn-group > .btn-group:first-child > .dropdown-toggle { + border-bottom-right-radius: 0; + border-top-right-radius: 0; } + +/* line 84, ../sass/bootstrap/_button-groups.scss */ +.btn-group > .btn-group:last-child > .btn:first-child { + border-bottom-left-radius: 0; + border-top-left-radius: 0; } + +/* line 90, ../sass/bootstrap/_button-groups.scss */ +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; } + +/* line 108, ../sass/bootstrap/_button-groups.scss */ +.btn-group > .btn + .dropdown-toggle { + padding-left: 8px; + padding-right: 8px; } + +/* line 112, ../sass/bootstrap/_button-groups.scss */ +.btn-group > .btn-lg + .dropdown-toggle, .btn-group-lg.btn-group > .btn + .dropdown-toggle { + padding-left: 12px; + padding-right: 12px; } + +/* line 119, ../sass/bootstrap/_button-groups.scss */ +.btn-group.open .dropdown-toggle { + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); } + /* line 123, ../sass/bootstrap/_button-groups.scss */ + .btn-group.open .dropdown-toggle.btn-link { + -webkit-box-shadow: none; + box-shadow: none; } + +/* line 130, ../sass/bootstrap/_button-groups.scss */ +.btn .caret { + margin-left: 0; } + +/* line 134, ../sass/bootstrap/_button-groups.scss */ +.btn-lg .caret, .btn-group-lg > .btn .caret { + border-width: 5px 5px 0; + border-bottom-width: 0; } + +/* line 139, ../sass/bootstrap/_button-groups.scss */ +.dropup .btn-lg .caret, .dropup .btn-group-lg > .btn .caret { + border-width: 0 5px 5px; } + +/* line 150, ../sass/bootstrap/_button-groups.scss */ +.btn-group-vertical > .btn, +.btn-group-vertical > .btn-group, +.btn-group-vertical > .btn-group > .btn { + display: block; + float: none; + width: 100%; + max-width: 100%; } +/* line 21, ../sass/bootstrap/_mixins.scss */ +.btn-group-vertical > .btn-group:before, .btn-group-vertical > .btn-group:after { + content: " "; + display: table; } +/* line 25, ../sass/bootstrap/_mixins.scss */ +.btn-group-vertical > .btn-group:after { + clear: both; } +/* line 160, ../sass/bootstrap/_button-groups.scss */ +.btn-group-vertical > .btn-group > .btn { + float: none; } +/* line 168, ../sass/bootstrap/_button-groups.scss */ +.btn-group-vertical > .btn + .btn, +.btn-group-vertical > .btn + .btn-group, +.btn-group-vertical > .btn-group + .btn, +.btn-group-vertical > .btn-group + .btn-group { + margin-top: -1px; + margin-left: 0; } + +/* line 175, ../sass/bootstrap/_button-groups.scss */ +.btn-group-vertical > .btn:not(:first-child):not(:last-child) { + border-radius: 0; } +/* line 178, ../sass/bootstrap/_button-groups.scss */ +.btn-group-vertical > .btn:first-child:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; } +/* line 182, ../sass/bootstrap/_button-groups.scss */ +.btn-group-vertical > .btn:last-child:not(:first-child) { + border-bottom-left-radius: 0; + border-top-right-radius: 0; + border-top-left-radius: 0; } + +/* line 187, ../sass/bootstrap/_button-groups.scss */ +.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; } + +/* line 192, ../sass/bootstrap/_button-groups.scss */ +.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child, +.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; } + +/* line 196, ../sass/bootstrap/_button-groups.scss */ +.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { + border-top-right-radius: 0; + border-top-left-radius: 0; } + +/* line 205, ../sass/bootstrap/_button-groups.scss */ +.btn-group-justified { + display: table; + width: 100%; + table-layout: fixed; + border-collapse: separate; } + /* line 211, ../sass/bootstrap/_button-groups.scss */ + .btn-group-justified > .btn, + .btn-group-justified > .btn-group { + float: none; + display: table-cell; + width: 1%; } + /* line 216, ../sass/bootstrap/_button-groups.scss */ + .btn-group-justified > .btn-group .btn { + width: 100%; } + +/* line 224, ../sass/bootstrap/_button-groups.scss */ +[data-toggle="buttons"] > .btn > input[type="radio"], +[data-toggle="buttons"] > .btn > input[type="checkbox"] { + display: none; } + +/* line 7, ../sass/bootstrap/_input-groups.scss */ +.input-group { + position: relative; + display: table; + border-collapse: separate; } + /* line 13, ../sass/bootstrap/_input-groups.scss */ + .input-group[class*="col-"] { + float: none; + padding-left: 0; + padding-right: 0; } + /* line 19, ../sass/bootstrap/_input-groups.scss */ + .input-group .form-control { + position: relative; + z-index: 2; + float: left; + width: 100%; + margin-bottom: 0; } + +/* line 52, ../sass/bootstrap/_input-groups.scss */ +.input-group-addon, +.input-group-btn, +.input-group .form-control { + display: table-cell; } + /* line 55, ../sass/bootstrap/_input-groups.scss */ + .input-group-addon:not(:first-child):not(:last-child), + .input-group-btn:not(:first-child):not(:last-child), + .input-group .form-control:not(:first-child):not(:last-child) { + border-radius: 0; } + +/* line 61, ../sass/bootstrap/_input-groups.scss */ +.input-group-addon, +.input-group-btn { + width: 1%; + white-space: nowrap; + vertical-align: middle; } + +/* line 69, ../sass/bootstrap/_input-groups.scss */ +.input-group-addon { + padding: 6px 12px; + font-size: 14px; + font-weight: normal; + line-height: 1; + color: #555555; + text-align: center; + background-color: #d9d9d9; + border: 1px solid #cccccc; + border-radius: 0; } + /* line 81, ../sass/bootstrap/_input-groups.scss */ + .input-group-addon.input-sm, + .input-group-sm > .input-group-addon, + .input-group-sm > .input-group-btn > .input-group-addon.btn { + padding: 5px 10px; + font-size: 12px; + border-radius: 3px; } + /* line 86, ../sass/bootstrap/_input-groups.scss */ + .input-group-addon.input-lg, + .input-group-lg > .input-group-addon, + .input-group-lg > .input-group-btn > .input-group-addon.btn { + padding: 10px 16px; + font-size: 18px; + border-radius: 6px; } + /* line 94, ../sass/bootstrap/_input-groups.scss */ + .input-group-addon input[type="radio"], + .input-group-addon input[type="checkbox"] { + margin-top: 0; } + +/* line 106, ../sass/bootstrap/_input-groups.scss */ +.input-group .form-control:first-child, +.input-group-addon:first-child, +.input-group-btn:first-child > .btn, +.input-group-btn:first-child > .btn-group > .btn, +.input-group-btn:first-child > .dropdown-toggle, +.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle), +.input-group-btn:last-child > .btn-group:not(:last-child) > .btn { + border-bottom-right-radius: 0; + border-top-right-radius: 0; } + +/* line 109, ../sass/bootstrap/_input-groups.scss */ +.input-group-addon:first-child { + border-right: 0; } + +/* line 118, ../sass/bootstrap/_input-groups.scss */ +.input-group .form-control:last-child, +.input-group-addon:last-child, +.input-group-btn:last-child > .btn, +.input-group-btn:last-child > .btn-group > .btn, +.input-group-btn:last-child > .dropdown-toggle, +.input-group-btn:first-child > .btn:not(:first-child), +.input-group-btn:first-child > .btn-group:not(:first-child) > .btn { + border-bottom-left-radius: 0; + border-top-left-radius: 0; } + +/* line 121, ../sass/bootstrap/_input-groups.scss */ +.input-group-addon:last-child { + border-left: 0; } + +/* line 127, ../sass/bootstrap/_input-groups.scss */ +.input-group-btn { + position: relative; + font-size: 0; + white-space: nowrap; } + /* line 136, ../sass/bootstrap/_input-groups.scss */ + .input-group-btn > .btn { + position: relative; } + /* line 138, ../sass/bootstrap/_input-groups.scss */ + .input-group-btn > .btn + .btn { + margin-left: -1px; } + /* line 144, ../sass/bootstrap/_input-groups.scss */ + .input-group-btn > .btn:hover, .input-group-btn > .btn:focus, .input-group-btn > .btn:active { + z-index: 2; } + /* line 152, ../sass/bootstrap/_input-groups.scss */ + .input-group-btn:first-child > .btn, + .input-group-btn:first-child > .btn-group { + margin-right: -1px; } + /* line 158, ../sass/bootstrap/_input-groups.scss */ + .input-group-btn:last-child > .btn, + .input-group-btn:last-child > .btn-group { + margin-left: -1px; } + +/* line 9, ../sass/bootstrap/_navs.scss */ +.nav { + margin-bottom: 0; + padding-left: 0; + list-style: none; } + /* line 21, ../sass/bootstrap/_mixins.scss */ + .nav:before, .nav:after { + content: " "; + display: table; } + /* line 25, ../sass/bootstrap/_mixins.scss */ + .nav:after { + clear: both; } + /* line 15, ../sass/bootstrap/_navs.scss */ + .nav > li { + position: relative; + display: block; } + /* line 19, ../sass/bootstrap/_navs.scss */ + .nav > li > a { + position: relative; + display: block; + padding: 10px 15px; } + /* line 24, ../sass/bootstrap/_navs.scss */ + .nav > li > a:hover, .nav > li > a:focus { + text-decoration: none; + background-color: #d9d9d9; } + /* line 31, ../sass/bootstrap/_navs.scss */ + .nav > li.disabled > a { + color: #4e4e4e; } + /* line 35, ../sass/bootstrap/_navs.scss */ + .nav > li.disabled > a:hover, .nav > li.disabled > a:focus { + color: #4e4e4e; + text-decoration: none; + background-color: transparent; + cursor: not-allowed; } + /* line 48, ../sass/bootstrap/_navs.scss */ + .nav .open > a, .nav .open > a:hover, .nav .open > a:focus { + background-color: #d9d9d9; + border-color: #005b9a; } + /* line 59, ../sass/bootstrap/_navs.scss */ + .nav .nav-divider { + height: 1px; + margin: 9px 0; + overflow: hidden; + background-color: #e5e5e5; } + /* line 66, ../sass/bootstrap/_navs.scss */ + .nav > li > a > img { + max-width: none; } + +/* line 76, ../sass/bootstrap/_navs.scss */ +.nav-tabs { + border-bottom: 1px solid #e0e0e0; } + /* line 78, ../sass/bootstrap/_navs.scss */ + .nav-tabs > li { + float: left; + margin-bottom: -1px; } + /* line 84, ../sass/bootstrap/_navs.scss */ + .nav-tabs > li > a { + margin-right: 2px; + line-height: 1.42857; + border: 1px solid transparent; + border-radius: 0 0 0 0; + color: #222222; } + /* line 91, ../sass/bootstrap/_navs.scss */ + .nav-tabs > li > a:hover, .nav-tabs > li > a:focus { + background: inherit; + border-color: inherit inherit #e0e0e0; } + /* line 102, ../sass/bootstrap/_navs.scss */ + .nav-tabs > li.active > a, .nav-tabs > li.active > a:hover, .nav-tabs > li.active > a:focus { + color: #222222; + background-color: #d9d9d9; + border: 1px solid inherit; + border-bottom-color: transparent; + cursor: default; } + +/* line 122, ../sass/bootstrap/_navs.scss */ +.nav-pills > li { + float: left; } + /* line 126, ../sass/bootstrap/_navs.scss */ + .nav-pills > li > a { + border-radius: 0; } + /* line 129, ../sass/bootstrap/_navs.scss */ + .nav-pills > li + li { + margin-left: 2px; } + /* line 137, ../sass/bootstrap/_navs.scss */ + .nav-pills > li.active > a, .nav-pills > li.active > a:hover, .nav-pills > li.active > a:focus { + color: white; + background-color: #ee5161; } + +/* line 148, ../sass/bootstrap/_navs.scss */ +.nav-stacked > li { + float: none; } + /* line 150, ../sass/bootstrap/_navs.scss */ + .nav-stacked > li + li { + margin-top: 2px; + margin-left: 0; } + +/* line 164, ../sass/bootstrap/_navs.scss */ +.nav-justified, .nav-tabs.nav-justified { + width: 100%; } + /* line 167, ../sass/bootstrap/_navs.scss */ + .nav-justified > li, .nav-tabs.nav-justified > li { + float: none; } + /* line 169, ../sass/bootstrap/_navs.scss */ + .nav-justified > li > a, .nav-tabs.nav-justified > li > a { + text-align: center; + margin-bottom: 5px; } + /* line 175, ../sass/bootstrap/_navs.scss */ + .nav-justified > .dropdown .dropdown-menu { + top: auto; + left: auto; } + @media (min-width: 768px) { + /* line 181, ../sass/bootstrap/_navs.scss */ + .nav-justified > li, .nav-tabs.nav-justified > li { + display: table-cell; + width: 1%; } + /* line 184, ../sass/bootstrap/_navs.scss */ + .nav-justified > li > a, .nav-tabs.nav-justified > li > a { + margin-bottom: 0; } } + +/* line 194, ../sass/bootstrap/_navs.scss */ +.nav-tabs-justified, .nav-tabs.nav-justified { + border-bottom: 0; } + /* line 197, ../sass/bootstrap/_navs.scss */ + .nav-tabs-justified > li > a, .nav-tabs.nav-justified > li > a { + margin-right: 0; + border-radius: 0; } + /* line 205, ../sass/bootstrap/_navs.scss */ + .nav-tabs-justified > .active > a, .nav-tabs.nav-justified > .active > a, + .nav-tabs-justified > .active > a:hover, + .nav-tabs.nav-justified > .active > a:hover, + .nav-tabs-justified > .active > a:focus, + .nav-tabs.nav-justified > .active > a:focus { + border: 1px solid #dddddd; } + @media (min-width: 768px) { + /* line 210, ../sass/bootstrap/_navs.scss */ + .nav-tabs-justified > li > a, .nav-tabs.nav-justified > li > a { + border-bottom: 1px solid #dddddd; + border-radius: 0 0 0 0; } + /* line 216, ../sass/bootstrap/_navs.scss */ + .nav-tabs-justified > .active > a, .nav-tabs.nav-justified > .active > a, + .nav-tabs-justified > .active > a:hover, + .nav-tabs.nav-justified > .active > a:hover, + .nav-tabs-justified > .active > a:focus, + .nav-tabs.nav-justified > .active > a:focus { + border-bottom-color: #303030; } } + +/* line 228, ../sass/bootstrap/_navs.scss */ +.tab-content > .tab-pane { + display: none; } +/* line 231, ../sass/bootstrap/_navs.scss */ +.tab-content > .active { + display: block; } + +/* line 241, ../sass/bootstrap/_navs.scss */ +.nav-tabs .dropdown-menu { + margin-top: -1px; + border-top-right-radius: 0; + border-top-left-radius: 0; } + +/* line 11, ../sass/bootstrap/_navbar.scss */ +.navbar { + position: relative; + min-height: 50px; + margin-bottom: 20px; + border: 1px solid transparent; } + /* line 21, ../sass/bootstrap/_mixins.scss */ + .navbar:before, .navbar:after { + content: " "; + display: table; } + /* line 25, ../sass/bootstrap/_mixins.scss */ + .navbar:after { + clear: both; } + @media (min-width: 768px) { + /* line 11, ../sass/bootstrap/_navbar.scss */ + .navbar { + border-radius: 0; } } + +/* line 21, ../sass/bootstrap/_mixins.scss */ +.navbar-header:before, .navbar-header:after { + content: " "; + display: table; } +/* line 25, ../sass/bootstrap/_mixins.scss */ +.navbar-header:after { + clear: both; } +@media (min-width: 768px) { + /* line 31, ../sass/bootstrap/_navbar.scss */ + .navbar-header { + float: left; } } + +/* line 50, ../sass/bootstrap/_navbar.scss */ +.navbar-collapse { + max-height: 340px; + overflow-x: visible; + padding-right: 0; + padding-left: 0; + border-top: 1px solid transparent; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); + -webkit-overflow-scrolling: touch; } + /* line 21, ../sass/bootstrap/_mixins.scss */ + .navbar-collapse:before, .navbar-collapse:after { + content: " "; + display: table; } + /* line 25, ../sass/bootstrap/_mixins.scss */ + .navbar-collapse:after { + clear: both; } + /* line 60, ../sass/bootstrap/_navbar.scss */ + .navbar-collapse.in { + overflow-y: auto; } + @media (min-width: 768px) { + /* line 50, ../sass/bootstrap/_navbar.scss */ + .navbar-collapse { + width: auto; + border-top: 0; + box-shadow: none; } + /* line 69, ../sass/bootstrap/_navbar.scss */ + .navbar-collapse.collapse { + display: block !important; + height: auto !important; + padding-bottom: 0; + overflow: visible !important; } + /* line 76, ../sass/bootstrap/_navbar.scss */ + .navbar-collapse.in { + overflow-y: visible; } + /* line 84, ../sass/bootstrap/_navbar.scss */ + .navbar-fixed-top .navbar-collapse, .navbar-static-top .navbar-collapse, .navbar-fixed-bottom .navbar-collapse { + padding-left: 0; + padding-right: 0; } } + +/* line 99, ../sass/bootstrap/_navbar.scss */ +.container > .navbar-header, +.container > .navbar-collapse, +.container-fluid > .navbar-header, +.container-fluid > .navbar-collapse { + margin-right: 0; + margin-left: 0; } + @media (min-width: 768px) { + /* line 99, ../sass/bootstrap/_navbar.scss */ + .container > .navbar-header, + .container > .navbar-collapse, + .container-fluid > .navbar-header, + .container-fluid > .navbar-collapse { + margin-right: 0; + margin-left: 0; } } + +/* line 118, ../sass/bootstrap/_navbar.scss */ +.navbar-static-top { + z-index: 1000; + border-width: 0 0 1px; } + @media (min-width: 768px) { + /* line 118, ../sass/bootstrap/_navbar.scss */ + .navbar-static-top { + border-radius: 0; } } + +/* line 129, ../sass/bootstrap/_navbar.scss */ +.navbar-fixed-top, +.navbar-fixed-bottom { + position: fixed; + right: 0; + left: 0; + z-index: 1030; } + @media (min-width: 768px) { + /* line 129, ../sass/bootstrap/_navbar.scss */ + .navbar-fixed-top, + .navbar-fixed-bottom { + border-radius: 0; } } + +/* line 140, ../sass/bootstrap/_navbar.scss */ +.navbar-fixed-top { + top: 0; + border-width: 0 0 1px; } + +/* line 144, ../sass/bootstrap/_navbar.scss */ +.navbar-fixed-bottom { + bottom: 0; + margin-bottom: 0; + border-width: 1px 0 0; } + +/* line 153, ../sass/bootstrap/_navbar.scss */ +.navbar-brand { + float: left; + padding: 15px 0; + font-size: 18px; + line-height: 20px; + height: 50px; } + /* line 161, ../sass/bootstrap/_navbar.scss */ + .navbar-brand:hover, .navbar-brand:focus { + text-decoration: none; } + @media (min-width: 768px) { + /* line 167, ../sass/bootstrap/_navbar.scss */ + .navbar > .container .navbar-brand, .navbar > .container-fluid .navbar-brand { + margin-left: 0; } } + +/* line 179, ../sass/bootstrap/_navbar.scss */ +.navbar-toggle { + position: relative; + float: right; + margin-right: 0; + padding: 9px 10px; + margin-top: 8px; + margin-bottom: 8px; + background-color: transparent; + background-image: none; + border: 1px solid transparent; + border-radius: 0; } + /* line 192, ../sass/bootstrap/_navbar.scss */ + .navbar-toggle:focus { + outline: none; } + /* line 197, ../sass/bootstrap/_navbar.scss */ + .navbar-toggle .icon-bar { + display: block; + width: 22px; + height: 2px; + border-radius: 1px; } + /* line 203, ../sass/bootstrap/_navbar.scss */ + .navbar-toggle .icon-bar + .icon-bar { + margin-top: 4px; } + @media (min-width: 768px) { + /* line 179, ../sass/bootstrap/_navbar.scss */ + .navbar-toggle { + display: none; } } + +/* line 218, ../sass/bootstrap/_navbar.scss */ +.navbar-nav { + margin: 7.5px 0; } + /* line 221, ../sass/bootstrap/_navbar.scss */ + .navbar-nav > li > a { + padding-top: 10px; + padding-bottom: 10px; + line-height: 20px; } + @media (max-width: 767px) { + /* line 229, ../sass/bootstrap/_navbar.scss */ + .navbar-nav .open .dropdown-menu { + position: static; + float: none; + width: auto; + margin-top: 0; + background-color: transparent; + border: 0; + box-shadow: none; } + /* line 238, ../sass/bootstrap/_navbar.scss */ + .navbar-nav .open .dropdown-menu > li > a, + .navbar-nav .open .dropdown-menu .dropdown-header { + padding: 5px 15px 5px 25px; } + /* line 241, ../sass/bootstrap/_navbar.scss */ + .navbar-nav .open .dropdown-menu > li > a { + line-height: 20px; } + /* line 244, ../sass/bootstrap/_navbar.scss */ + .navbar-nav .open .dropdown-menu > li > a:hover, .navbar-nav .open .dropdown-menu > li > a:focus { + background-image: none; } } + @media (min-width: 768px) { + /* line 218, ../sass/bootstrap/_navbar.scss */ + .navbar-nav { + float: left; + margin: 0; } + /* line 256, ../sass/bootstrap/_navbar.scss */ + .navbar-nav > li { + float: left; } + /* line 258, ../sass/bootstrap/_navbar.scss */ + .navbar-nav > li > a { + padding-top: 15px; + padding-bottom: 15px; } + /* line 264, ../sass/bootstrap/_navbar.scss */ + .navbar-nav.navbar-right:last-child { + margin-right: 0; } } + +@media (min-width: 768px) { + /* line 278, ../sass/bootstrap/_navbar.scss */ + .navbar-left { + float: left !important; } + + /* line 281, ../sass/bootstrap/_navbar.scss */ + .navbar-right { + float: right !important; } } +/* line 292, ../sass/bootstrap/_navbar.scss */ +.navbar-form { + margin-left: 0; + margin-right: 0; + padding: 10px 0; + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + margin-top: 8px; + margin-bottom: 8px; } + @media (max-width: 767px) { + /* line 304, ../sass/bootstrap/_navbar.scss */ + .navbar-form .form-group { + margin-bottom: 5px; } } + @media (min-width: 768px) { + /* line 292, ../sass/bootstrap/_navbar.scss */ + .navbar-form { + width: auto; + border: 0; + margin-left: 0; + margin-right: 0; + padding-top: 0; + padding-bottom: 0; + -webkit-box-shadow: none; + box-shadow: none; } + /* line 324, ../sass/bootstrap/_navbar.scss */ + .navbar-form.navbar-right:last-child { + margin-right: 0; } } + +/* line 334, ../sass/bootstrap/_navbar.scss */ +.navbar-nav > li > .dropdown-menu { + margin-top: 0; + border-top-right-radius: 0; + border-top-left-radius: 0; } + +/* line 339, ../sass/bootstrap/_navbar.scss */ +.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; } + +/* line 348, ../sass/bootstrap/_navbar.scss */ +.navbar-btn { + margin-top: 8px; + margin-bottom: 8px; } + /* line 351, ../sass/bootstrap/_navbar.scss */ + .navbar-btn.btn-sm, .btn-group-sm > .navbar-btn.btn { + margin-top: 10px; + margin-bottom: 10px; } + /* line 354, ../sass/bootstrap/_navbar.scss */ + .navbar-btn.btn-xs, .btn-group-xs > .navbar-btn.btn { + margin-top: 14px; + margin-bottom: 14px; } + +/* line 364, ../sass/bootstrap/_navbar.scss */ +.navbar-text { + margin-top: 15px; + margin-bottom: 15px; } + @media (min-width: 768px) { + /* line 364, ../sass/bootstrap/_navbar.scss */ + .navbar-text { + float: left; + margin-left: 0; + margin-right: 0; } + /* line 373, ../sass/bootstrap/_navbar.scss */ + .navbar-text.navbar-right:last-child { + margin-right: 0; } } + +/* line 383, ../sass/bootstrap/_navbar.scss */ +.navbar-default { + background-color: #ececec; + border-color: #e0e0e0; } + /* line 387, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-brand { + color: white; } + /* line 390, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-brand:hover, .navbar-default .navbar-brand:focus { + color: #e6e6e6; + background-color: #008b44; } + /* line 396, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-text { + color: #777777; } + /* line 401, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-nav > li > a { + color: #222222; } + /* line 405, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-nav > li > a:hover, .navbar-default .navbar-nav > li > a:focus { + color: #222222; + background-color: #e4e4e4; } + /* line 411, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-nav > .has-dropdown:not(.active):hover > a:first-child { + color: #222222; + background-color: #e4e4e4; } + /* line 419, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-nav > .active > a, .navbar-default .navbar-nav > .active > a:hover, .navbar-default .navbar-nav > .active > a:focus { + color: white; + background-color: #ee5161; } + /* line 427, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-nav > .disabled > a, .navbar-default .navbar-nav > .disabled > a:hover, .navbar-default .navbar-nav > .disabled > a:focus { + color: #cccccc; + background-color: transparent; } + /* line 434, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-toggle { + border-color: #dddddd; } + /* line 437, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-toggle:hover, .navbar-default .navbar-toggle:focus { + background-color: #dddddd; } + /* line 440, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-toggle .icon-bar { + background-color: #888888; } + /* line 446, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-collapse, + .navbar-default .navbar-form { + border-color: #e0e0e0; } + /* line 456, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-nav > .open > a, .navbar-default .navbar-nav > .open > a:hover, .navbar-default .navbar-nav > .open > a:focus { + background-color: #ee5161; + color: white; } + @media (max-width: 767px) { + /* line 465, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-nav .open .dropdown-menu > li > a { + color: #222222; } + /* line 468, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus { + color: #222222; + background-color: #e4e4e4; } + /* line 476, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-nav .open .dropdown-menu > .active > a, .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus { + color: white; + background-color: #ee5161; } + /* line 484, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a, .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus { + color: #cccccc; + background-color: transparent; } } + /* line 498, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-link { + color: #222222; } + /* line 500, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-link:hover { + color: #222222; } + +/* line 509, ../sass/bootstrap/_navbar.scss */ +.navbar-inverse { + background-color: #cccccc; + border-color: transparent; } + /* line 513, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-brand { + color: white; } + /* line 516, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-brand:hover, .navbar-inverse .navbar-brand:focus { + color: white; + background-color: transparent; } + /* line 522, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-text { + color: #222222; } + /* line 527, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-nav > li > a { + color: #222222; } + /* line 531, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-nav > li > a:hover, .navbar-inverse .navbar-nav > li > a:focus { + color: #222222; + background-color: #d9d9d9; } + /* line 537, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-nav > li.has-dropdown:hover > a:first-child { + color: #222222; + background-color: #d9d9d9; } + /* line 545, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-nav > .active > a, .navbar-inverse .navbar-nav > .active > a:hover, .navbar-inverse .navbar-nav > .active > a:focus { + color: white; + background-color: #353535; } + /* line 553, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-nav > .disabled > a, .navbar-inverse .navbar-nav > .disabled > a:hover, .navbar-inverse .navbar-nav > .disabled > a:focus { + color: #444444; + background-color: transparent; } + /* line 561, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-toggle { + border-color: #333333; } + /* line 564, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-toggle:hover, .navbar-inverse .navbar-toggle:focus { + background-color: #333333; } + /* line 567, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-toggle .icon-bar { + background-color: white; } + /* line 573, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-collapse, + .navbar-inverse .navbar-form { + border-color: #bababa; } + /* line 582, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-nav > .open > a, .navbar-inverse .navbar-nav > .open > a:hover, .navbar-inverse .navbar-nav > .open > a:focus { + background-color: #353535; + color: white; } + @media (max-width: 767px) { + /* line 591, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header { + border-color: transparent; } + /* line 594, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-nav .open .dropdown-menu .divider { + background-color: transparent; } + /* line 597, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a { + color: #222222; } + /* line 600, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover, .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus { + color: #222222; + background-color: #d9d9d9; } + /* line 608, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a, .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover, .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus { + color: white; + background-color: #353535; } + /* line 616, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a, .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover, .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus { + color: #444444; + background-color: transparent; } } + /* line 625, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-link { + color: #222222; } + /* line 627, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-link:hover { + color: #222222; } + +/* line 6, ../sass/bootstrap/_pager.scss */ +.pager { + padding-left: 0; + margin: 20px 0; + list-style: none; + text-align: center; } + /* line 21, ../sass/bootstrap/_mixins.scss */ + .pager:before, .pager:after { + content: " "; + display: table; } + /* line 25, ../sass/bootstrap/_mixins.scss */ + .pager:after { + clear: both; } + /* line 12, ../sass/bootstrap/_pager.scss */ + .pager li { + display: inline; } + /* line 15, ../sass/bootstrap/_pager.scss */ + .pager li > a, + .pager li > span { + display: inline-block; + padding: 5px 14px; + background-color: white; + border: 1px solid #dddddd; + border-radius: 15px; } + /* line 24, ../sass/bootstrap/_pager.scss */ + .pager li > a:hover, + .pager li > a:focus { + text-decoration: none; + background-color: #d9d9d9; } + /* line 32, ../sass/bootstrap/_pager.scss */ + .pager .next > a, + .pager .next > span { + float: right; } + /* line 39, ../sass/bootstrap/_pager.scss */ + .pager .previous > a, + .pager .previous > span { + float: left; } + /* line 48, ../sass/bootstrap/_pager.scss */ + .pager .disabled > a, + .pager .disabled > a:hover, + .pager .disabled > a:focus, + .pager .disabled > span { + color: #4e4e4e; + background-color: white; + cursor: not-allowed; } + +/* line 5, ../sass/bootstrap/_labels.scss */ +.label { + display: inline; + padding: .2em .6em .3em; + font-size: 13px; + line-height: 1; + color: white; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25em; + background: #444444; } + /* line 20, ../sass/bootstrap/_labels.scss */ + .label[href]:hover, .label[href]:focus { + color: white; + text-decoration: none; + cursor: pointer; } + /* line 28, ../sass/bootstrap/_labels.scss */ + .label:empty { + display: none; } + /* line 33, ../sass/bootstrap/_labels.scss */ + .btn .label { + position: relative; + top: -1px; } + +/* line 42, ../sass/bootstrap/_labels.scss */ +.label-default { + background-color: #444444; + color: white; } + /* line 584, ../sass/bootstrap/_mixins.scss */ + .label-default[href]:hover, .label-default[href]:focus { + background-color: #2b2b2b; } + +/* line 46, ../sass/bootstrap/_labels.scss */ +.label-primary { + background-color: white; + color: white; } + /* line 584, ../sass/bootstrap/_mixins.scss */ + .label-primary[href]:hover, .label-primary[href]:focus { + background-color: #e6e6e6; } + +/* line 50, ../sass/bootstrap/_labels.scss */ +.label-success { + background-color: #5cb85c; + color: white; } + /* line 584, ../sass/bootstrap/_mixins.scss */ + .label-success[href]:hover, .label-success[href]:focus { + background-color: #449d44; } + +/* line 54, ../sass/bootstrap/_labels.scss */ +.label-info { + background-color: #5bc0de; + color: white; } + /* line 584, ../sass/bootstrap/_mixins.scss */ + .label-info[href]:hover, .label-info[href]:focus { + background-color: #31b0d5; } + +/* line 58, ../sass/bootstrap/_labels.scss */ +.label-warning { + background-color: #f0ad4e; + color: white; } + /* line 584, ../sass/bootstrap/_mixins.scss */ + .label-warning[href]:hover, .label-warning[href]:focus { + background-color: #ec971f; } + +/* line 62, ../sass/bootstrap/_labels.scss */ +.label-danger { + background-color: #d9534f; + color: white; } + /* line 584, ../sass/bootstrap/_mixins.scss */ + .label-danger[href]:hover, .label-danger[href]:focus { + background-color: #c9302c; } + +/* line 7, ../sass/bootstrap/_badges.scss */ +.badge { + display: inline-block; + min-width: 10px; + padding: 3px 7px; + font-size: 12px; + font-weight: bold; + color: inherit; + line-height: 1; + vertical-align: baseline; + white-space: nowrap; + text-align: center; + background-color: #4e4e4e; + border-radius: 0; } + /* line 22, ../sass/bootstrap/_badges.scss */ + .badge:empty { + display: none; } + /* line 27, ../sass/bootstrap/_badges.scss */ + .btn .badge { + position: relative; + top: -1px; } + /* line 31, ../sass/bootstrap/_badges.scss */ + .btn-xs .badge, .btn-group-xs > .btn .badge { + top: 0; + padding: 1px 5px; } + +/* line 40, ../sass/bootstrap/_badges.scss */ +a.badge:hover, a.badge:focus { + color: inherit; + text-decoration: none; + cursor: pointer; } + +/* line 49, ../sass/bootstrap/_badges.scss */ +a.list-group-item.active > .badge, +.nav-pills > .active > a > .badge { + color: #4d99d8; + background-color: white; } + +/* line 53, ../sass/bootstrap/_badges.scss */ +.nav-pills > li > a > .badge { + margin-left: 3px; } + +/* line 9, ../sass/bootstrap/_alerts.scss */ +.alert { + padding: 15px; + margin-bottom: 20px; + border: 1px solid transparent; + border-radius: 0; } + /* line 16, ../sass/bootstrap/_alerts.scss */ + .alert h4 { + margin-top: 0; + color: inherit; } + /* line 22, ../sass/bootstrap/_alerts.scss */ + .alert .alert-link { + font-weight: bold; } + /* line 28, ../sass/bootstrap/_alerts.scss */ + .alert > p, + .alert > ul { + margin-bottom: 0; } + /* line 31, ../sass/bootstrap/_alerts.scss */ + .alert > p + p { + margin-top: 5px; } + +/* line 40, ../sass/bootstrap/_alerts.scss */ +.alert-dismissable { + padding-right: 35px; } + /* line 44, ../sass/bootstrap/_alerts.scss */ + .alert-dismissable .close { + position: relative; + top: -2px; + right: -21px; + color: inherit; } + +/* line 56, ../sass/bootstrap/_alerts.scss */ +.alert-success { + background-color: #dff0d8; + border-color: #d6e9c6; + color: #3c763d; } + /* line 430, ../sass/bootstrap/_mixins.scss */ + .alert-success hr { + border-top-color: #c9e2b3; } + /* line 433, ../sass/bootstrap/_mixins.scss */ + .alert-success .alert-link { + color: #2b542c; } + +/* line 59, ../sass/bootstrap/_alerts.scss */ +.alert-info { + background-color: #d9edf7; + border-color: #bce8f1; + color: #31708f; } + /* line 430, ../sass/bootstrap/_mixins.scss */ + .alert-info hr { + border-top-color: #a6e1ec; } + /* line 433, ../sass/bootstrap/_mixins.scss */ + .alert-info .alert-link { + color: #245269; } + +/* line 62, ../sass/bootstrap/_alerts.scss */ +.alert-warning { + background-color: #fcf8e3; + border-color: #faebcc; + color: #8a6d3b; } + /* line 430, ../sass/bootstrap/_mixins.scss */ + .alert-warning hr { + border-top-color: #f7e1b5; } + /* line 433, ../sass/bootstrap/_mixins.scss */ + .alert-warning .alert-link { + color: #66512c; } + +/* line 65, ../sass/bootstrap/_alerts.scss */ +.alert-danger { + background-color: #f2dede; + border-color: #ebccd1; + color: #a94442; } + /* line 430, ../sass/bootstrap/_mixins.scss */ + .alert-danger hr { + border-top-color: #e4b9c0; } + /* line 433, ../sass/bootstrap/_mixins.scss */ + .alert-danger .alert-link { + color: #843534; } + +/* line 7, ../sass/bootstrap/_panels.scss */ +.panel { + margin-bottom: 20px; + background-color: white; + border: 1px solid transparent; + border-radius: 0; + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); } + +/* line 16, ../sass/bootstrap/_panels.scss */ +.panel-body { + padding: 15px; } + /* line 21, ../sass/bootstrap/_mixins.scss */ + .panel-body:before, .panel-body:after { + content: " "; + display: table; } + /* line 25, ../sass/bootstrap/_mixins.scss */ + .panel-body:after { + clear: both; } + +/* line 22, ../sass/bootstrap/_panels.scss */ +.panel-heading { + padding: 10px 15px; + border-bottom: 1px solid transparent; + border-top-right-radius: -1; + border-top-left-radius: -1; } + /* line 27, ../sass/bootstrap/_panels.scss */ + .panel-heading > .dropdown .dropdown-toggle { + color: inherit; } + +/* line 33, ../sass/bootstrap/_panels.scss */ +.panel-title { + margin-top: 0; + margin-bottom: 0; + font-size: 16px; + color: inherit; } + /* line 39, ../sass/bootstrap/_panels.scss */ + .panel-title > a { + color: inherit; } + +/* line 45, ../sass/bootstrap/_panels.scss */ +.panel-footer { + padding: 10px 15px; + background-color: whitesmoke; + border-top: 1px solid #dddddd; + border-bottom-right-radius: -1; + border-bottom-left-radius: -1; } + +/* line 59, ../sass/bootstrap/_panels.scss */ +.panel > .list-group { + margin-bottom: 0; } + /* line 62, ../sass/bootstrap/_panels.scss */ + .panel > .list-group .list-group-item { + border-width: 1px 0; + border-radius: 0; } + /* line 69, ../sass/bootstrap/_panels.scss */ + .panel > .list-group:first-child .list-group-item:first-child { + border-top: 0; + border-top-right-radius: -1; + border-top-left-radius: -1; } + /* line 76, ../sass/bootstrap/_panels.scss */ + .panel > .list-group:last-child .list-group-item:last-child { + border-bottom: 0; + border-bottom-right-radius: -1; + border-bottom-left-radius: -1; } + +/* line 85, ../sass/bootstrap/_panels.scss */ +.panel-heading + .list-group .list-group-item:first-child { + border-top-width: 0; } + +/* line 98, ../sass/bootstrap/_panels.scss */ +.panel > .table, +.panel > .table-responsive > .table { + margin-bottom: 0; } +/* line 103, ../sass/bootstrap/_panels.scss */ +.panel > .table:first-child, +.panel > .table-responsive:first-child > .table:first-child { + border-top-right-radius: -1; + border-top-left-radius: -1; } + /* line 110, ../sass/bootstrap/_panels.scss */ + .panel > .table:first-child > thead:first-child > tr:first-child td:first-child, + .panel > .table:first-child > thead:first-child > tr:first-child th:first-child, + .panel > .table:first-child > tbody:first-child > tr:first-child td:first-child, + .panel > .table:first-child > tbody:first-child > tr:first-child th:first-child, + .panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child, + .panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child, + .panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child, + .panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child { + border-top-left-radius: -1; } + /* line 114, ../sass/bootstrap/_panels.scss */ + .panel > .table:first-child > thead:first-child > tr:first-child td:last-child, + .panel > .table:first-child > thead:first-child > tr:first-child th:last-child, + .panel > .table:first-child > tbody:first-child > tr:first-child td:last-child, + .panel > .table:first-child > tbody:first-child > tr:first-child th:last-child, + .panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child, + .panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child, + .panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child, + .panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child { + border-top-right-radius: -1; } +/* line 122, ../sass/bootstrap/_panels.scss */ +.panel > .table:last-child, +.panel > .table-responsive:last-child > .table:last-child { + border-bottom-right-radius: -1; + border-bottom-left-radius: -1; } + /* line 129, ../sass/bootstrap/_panels.scss */ + .panel > .table:last-child > tbody:last-child > tr:last-child td:first-child, + .panel > .table:last-child > tbody:last-child > tr:last-child th:first-child, + .panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child, + .panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child, + .panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child, + .panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child, + .panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child, + .panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child { + border-bottom-left-radius: -1; } + /* line 133, ../sass/bootstrap/_panels.scss */ + .panel > .table:last-child > tbody:last-child > tr:last-child td:last-child, + .panel > .table:last-child > tbody:last-child > tr:last-child th:last-child, + .panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child, + .panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child, + .panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child, + .panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child, + .panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child, + .panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child { + border-bottom-right-radius: -1; } +/* line 140, ../sass/bootstrap/_panels.scss */ +.panel > .panel-body + .table, +.panel > .panel-body + .table-responsive { + border-top: 1px solid #cccccc; } +/* line 144, ../sass/bootstrap/_panels.scss */ +.panel > .table > tbody:first-child > tr:first-child th, +.panel > .table > tbody:first-child > tr:first-child td { + border-top: 0; } +/* line 148, ../sass/bootstrap/_panels.scss */ +.panel > .table-bordered, +.panel > .table-responsive > .table-bordered { + border: 0; } + /* line 155, ../sass/bootstrap/_panels.scss */ + .panel > .table-bordered > thead > tr > th:first-child, + .panel > .table-bordered > thead > tr > td:first-child, + .panel > .table-bordered > tbody > tr > th:first-child, + .panel > .table-bordered > tbody > tr > td:first-child, + .panel > .table-bordered > tfoot > tr > th:first-child, + .panel > .table-bordered > tfoot > tr > td:first-child, + .panel > .table-responsive > .table-bordered > thead > tr > th:first-child, + .panel > .table-responsive > .table-bordered > thead > tr > td:first-child, + .panel > .table-responsive > .table-bordered > tbody > tr > th:first-child, + .panel > .table-responsive > .table-bordered > tbody > tr > td:first-child, + .panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child, + .panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child { + border-left: 0; } + /* line 159, ../sass/bootstrap/_panels.scss */ + .panel > .table-bordered > thead > tr > th:last-child, + .panel > .table-bordered > thead > tr > td:last-child, + .panel > .table-bordered > tbody > tr > th:last-child, + .panel > .table-bordered > tbody > tr > td:last-child, + .panel > .table-bordered > tfoot > tr > th:last-child, + .panel > .table-bordered > tfoot > tr > td:last-child, + .panel > .table-responsive > .table-bordered > thead > tr > th:last-child, + .panel > .table-responsive > .table-bordered > thead > tr > td:last-child, + .panel > .table-responsive > .table-bordered > tbody > tr > th:last-child, + .panel > .table-responsive > .table-bordered > tbody > tr > td:last-child, + .panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child, + .panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child { + border-right: 0; } + /* line 168, ../sass/bootstrap/_panels.scss */ + .panel > .table-bordered > thead > tr:first-child > td, + .panel > .table-bordered > thead > tr:first-child > th, + .panel > .table-bordered > tbody > tr:first-child > td, + .panel > .table-bordered > tbody > tr:first-child > th, + .panel > .table-responsive > .table-bordered > thead > tr:first-child > td, + .panel > .table-responsive > .table-bordered > thead > tr:first-child > th, + .panel > .table-responsive > .table-bordered > tbody > tr:first-child > td, + .panel > .table-responsive > .table-bordered > tbody > tr:first-child > th { + border-bottom: 0; } + /* line 177, ../sass/bootstrap/_panels.scss */ + .panel > .table-bordered > tbody > tr:last-child > td, + .panel > .table-bordered > tbody > tr:last-child > th, + .panel > .table-bordered > tfoot > tr:last-child > td, + .panel > .table-bordered > tfoot > tr:last-child > th, + .panel > .table-responsive > .table-bordered > tbody > tr:last-child > td, + .panel > .table-responsive > .table-bordered > tbody > tr:last-child > th, + .panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td, + .panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th { + border-bottom: 0; } +/* line 183, ../sass/bootstrap/_panels.scss */ +.panel > .table-responsive { + border: 0; + margin-bottom: 0; } + +/* line 195, ../sass/bootstrap/_panels.scss */ +.panel-group { + margin-bottom: 20px; } + /* line 199, ../sass/bootstrap/_panels.scss */ + .panel-group .panel { + margin-bottom: 0; + border-radius: 0; + overflow: hidden; } + /* line 203, ../sass/bootstrap/_panels.scss */ + .panel-group .panel + .panel { + margin-top: 5px; } + /* line 208, ../sass/bootstrap/_panels.scss */ + .panel-group .panel-heading { + border-bottom: 0; } + /* line 210, ../sass/bootstrap/_panels.scss */ + .panel-group .panel-heading + .panel-collapse .panel-body { + border-top: 1px solid #dddddd; } + /* line 214, ../sass/bootstrap/_panels.scss */ + .panel-group .panel-footer { + border-top: 0; } + /* line 216, ../sass/bootstrap/_panels.scss */ + .panel-group .panel-footer + .panel-collapse .panel-body { + border-bottom: 1px solid #dddddd; } + +/* line 224, ../sass/bootstrap/_panels.scss */ +.panel-default { + border-color: #dddddd; } + /* line 407, ../sass/bootstrap/_mixins.scss */ + .panel-default > .panel-heading { + color: #303030; + background-color: whitesmoke; + border-color: #dddddd; } + /* line 412, ../sass/bootstrap/_mixins.scss */ + .panel-default > .panel-heading + .panel-collapse .panel-body { + border-top-color: #dddddd; } + /* line 417, ../sass/bootstrap/_mixins.scss */ + .panel-default > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: #dddddd; } + +/* line 227, ../sass/bootstrap/_panels.scss */ +.panel-primary { + border-color: white; } + /* line 407, ../sass/bootstrap/_mixins.scss */ + .panel-primary > .panel-heading { + color: white; + background-color: white; + border-color: white; } + /* line 412, ../sass/bootstrap/_mixins.scss */ + .panel-primary > .panel-heading + .panel-collapse .panel-body { + border-top-color: white; } + /* line 417, ../sass/bootstrap/_mixins.scss */ + .panel-primary > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: white; } + +/* line 230, ../sass/bootstrap/_panels.scss */ +.panel-success { + border-color: #d6e9c6; } + /* line 407, ../sass/bootstrap/_mixins.scss */ + .panel-success > .panel-heading { + color: #3c763d; + background-color: #dff0d8; + border-color: #d6e9c6; } + /* line 412, ../sass/bootstrap/_mixins.scss */ + .panel-success > .panel-heading + .panel-collapse .panel-body { + border-top-color: #d6e9c6; } + /* line 417, ../sass/bootstrap/_mixins.scss */ + .panel-success > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: #d6e9c6; } + +/* line 233, ../sass/bootstrap/_panels.scss */ +.panel-info { + border-color: #bce8f1; } + /* line 407, ../sass/bootstrap/_mixins.scss */ + .panel-info > .panel-heading { + color: #31708f; + background-color: #d9edf7; + border-color: #bce8f1; } + /* line 412, ../sass/bootstrap/_mixins.scss */ + .panel-info > .panel-heading + .panel-collapse .panel-body { + border-top-color: #bce8f1; } + /* line 417, ../sass/bootstrap/_mixins.scss */ + .panel-info > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: #bce8f1; } + +/* line 236, ../sass/bootstrap/_panels.scss */ +.panel-warning { + border-color: #faebcc; } + /* line 407, ../sass/bootstrap/_mixins.scss */ + .panel-warning > .panel-heading { + color: #8a6d3b; + background-color: #fcf8e3; + border-color: #faebcc; } + /* line 412, ../sass/bootstrap/_mixins.scss */ + .panel-warning > .panel-heading + .panel-collapse .panel-body { + border-top-color: #faebcc; } + /* line 417, ../sass/bootstrap/_mixins.scss */ + .panel-warning > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: #faebcc; } + +/* line 239, ../sass/bootstrap/_panels.scss */ +.panel-danger { + border-color: #ebccd1; } + /* line 407, ../sass/bootstrap/_mixins.scss */ + .panel-danger > .panel-heading { + color: #a94442; + background-color: #f2dede; + border-color: #ebccd1; } + /* line 412, ../sass/bootstrap/_mixins.scss */ + .panel-danger > .panel-heading + .panel-collapse .panel-body { + border-top-color: #ebccd1; } + /* line 417, ../sass/bootstrap/_mixins.scss */ + .panel-danger > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: #ebccd1; } + +/* line 7, ../sass/bootstrap/_wells.scss */ +.well { + min-height: 20px; + padding: 0; + margin-bottom: 20px; + background-color: inherit; + border: 1px solid inherit; + border-radius: 0; } + /* line 14, ../sass/bootstrap/_wells.scss */ + .well blockquote { + border-color: #ddd; + border-color: rgba(0, 0, 0, 0.15); } + +/* line 21, ../sass/bootstrap/_wells.scss */ +.well-lg { + padding: 24px; + border-radius: 6px; } + +/* line 25, ../sass/bootstrap/_wells.scss */ +.well-sm { + padding: 9px; + border-radius: 3px; } + +/* line 6, ../sass/bootstrap/_close.scss */ +.close { + float: right; + font-size: 21px; + font-weight: bold; + line-height: 1; + color: black; + text-shadow: 0 1px 0 white; + opacity: 0.2; + filter: alpha(opacity=20); } + /* line 16, ../sass/bootstrap/_close.scss */ + .close:hover, .close:focus { + color: black; + text-decoration: none; + cursor: pointer; + opacity: 0.5; + filter: alpha(opacity=50); } + +/* line 29, ../sass/bootstrap/_close.scss */ +button.close { + padding: 0; + cursor: pointer; + background: transparent; + border: 0; + -webkit-appearance: none; } + +/* line 11, ../sass/bootstrap/_modals.scss */ +.modal-open { + overflow: hidden; } + +/* line 16, ../sass/bootstrap/_modals.scss */ +.modal { + display: none; + overflow: auto; + overflow-y: scroll; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1050; + -webkit-overflow-scrolling: touch; + outline: 0; } + /* line 33, ../sass/bootstrap/_modals.scss */ + .modal.fade .modal-dialog { + -webkit-transform: translate(0, -25%); + -ms-transform: translate(0, -25%); + transform: translate(0, -25%); + -webkit-transition: -webkit-transform 0.3s ease-out; + -moz-transition: -moz-transform 0.3s ease-out; + -o-transition: -o-transform 0.3s ease-out; + transition: transform 0.3s ease-out; } + /* line 37, ../sass/bootstrap/_modals.scss */ + .modal.in .modal-dialog { + -webkit-transform: translate(0, 0); + -ms-transform: translate(0, 0); + transform: translate(0, 0); } + +/* line 41, ../sass/bootstrap/_modals.scss */ +.modal-dialog { + position: relative; + width: auto; + margin: 10px; } + +/* line 48, ../sass/bootstrap/_modals.scss */ +.modal-content { + position: relative; + background-color: white; + border: 1px solid #999999; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 0; + -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); + box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); + background-clip: padding-box; + outline: none; } + +/* line 61, ../sass/bootstrap/_modals.scss */ +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + background-color: black; } + /* line 70, ../sass/bootstrap/_modals.scss */ + .modal-backdrop.fade { + opacity: 0; + filter: alpha(opacity=0); } + /* line 71, ../sass/bootstrap/_modals.scss */ + .modal-backdrop.in { + opacity: 0.5; + filter: alpha(opacity=50); } + +/* line 76, ../sass/bootstrap/_modals.scss */ +.modal-header { + padding: 15px; + border-bottom: 1px solid transparent; + min-height: 16.42857px; } + +/* line 82, ../sass/bootstrap/_modals.scss */ +.modal-header .close { + margin-top: -2px; } + +/* line 87, ../sass/bootstrap/_modals.scss */ +.modal-title { + margin: 0; + line-height: 1.42857; } + +/* line 94, ../sass/bootstrap/_modals.scss */ +.modal-body { + position: relative; + padding: 20px; } + +/* line 100, ../sass/bootstrap/_modals.scss */ +.modal-footer { + margin-top: 15px; + padding: 19px 20px 20px; + text-align: right; + border-top: 1px solid transparent; } + /* line 21, ../sass/bootstrap/_mixins.scss */ + .modal-footer:before, .modal-footer:after { + content: " "; + display: table; } + /* line 25, ../sass/bootstrap/_mixins.scss */ + .modal-footer:after { + clear: both; } + /* line 108, ../sass/bootstrap/_modals.scss */ + .modal-footer .btn + .btn { + margin-left: 5px; + margin-bottom: 0; } + /* line 113, ../sass/bootstrap/_modals.scss */ + .modal-footer .btn-group .btn + .btn { + margin-left: -1px; } + /* line 117, ../sass/bootstrap/_modals.scss */ + .modal-footer .btn-block + .btn-block { + margin-left: 0; } + +@media (min-width: 768px) { + /* line 125, ../sass/bootstrap/_modals.scss */ + .modal-dialog { + width: 760px; + margin: 30px auto; } + + /* line 129, ../sass/bootstrap/_modals.scss */ + .modal-content { + -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); } + + /* line 134, ../sass/bootstrap/_modals.scss */ + .modal-sm { + width: 300px; } } +@media (min-width: 992px) { + /* line 138, ../sass/bootstrap/_modals.scss */ + .modal-lg { + width: 900px; } } +/* line 7, ../sass/bootstrap/_tooltip.scss */ +.tooltip { + position: absolute; + z-index: 1030; + display: block; + visibility: visible; + font-size: 12px; + line-height: 1.4; + opacity: 0; + filter: alpha(opacity=0); } + /* line 16, ../sass/bootstrap/_tooltip.scss */ + .tooltip.in { + opacity: 0.9; + filter: alpha(opacity=90); } + /* line 17, ../sass/bootstrap/_tooltip.scss */ + .tooltip.top { + margin-top: -3px; + padding: 5px 0; } + /* line 18, ../sass/bootstrap/_tooltip.scss */ + .tooltip.right { + margin-left: 3px; + padding: 0 5px; } + /* line 19, ../sass/bootstrap/_tooltip.scss */ + .tooltip.bottom { + margin-top: 3px; + padding: 5px 0; } + /* line 20, ../sass/bootstrap/_tooltip.scss */ + .tooltip.left { + margin-left: -3px; + padding: 0 5px; } + +/* line 24, ../sass/bootstrap/_tooltip.scss */ +.tooltip-inner { + max-width: 200px; + padding: 3px 8px; + color: white; + text-align: center; + text-decoration: none; + background-color: black; + border-radius: 0; } + +/* line 35, ../sass/bootstrap/_tooltip.scss */ +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; } + +/* line 43, ../sass/bootstrap/_tooltip.scss */ +.tooltip.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-width: 5px 5px 0; + border-top-color: black; } +/* line 50, ../sass/bootstrap/_tooltip.scss */ +.tooltip.top-left .tooltip-arrow { + bottom: 0; + left: 5px; + border-width: 5px 5px 0; + border-top-color: black; } +/* line 56, ../sass/bootstrap/_tooltip.scss */ +.tooltip.top-right .tooltip-arrow { + bottom: 0; + right: 5px; + border-width: 5px 5px 0; + border-top-color: black; } +/* line 62, ../sass/bootstrap/_tooltip.scss */ +.tooltip.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-width: 5px 5px 5px 0; + border-right-color: black; } +/* line 69, ../sass/bootstrap/_tooltip.scss */ +.tooltip.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-width: 5px 0 5px 5px; + border-left-color: black; } +/* line 76, ../sass/bootstrap/_tooltip.scss */ +.tooltip.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-width: 0 5px 5px; + border-bottom-color: black; } +/* line 83, ../sass/bootstrap/_tooltip.scss */ +.tooltip.bottom-left .tooltip-arrow { + top: 0; + left: 5px; + border-width: 0 5px 5px; + border-bottom-color: black; } +/* line 89, ../sass/bootstrap/_tooltip.scss */ +.tooltip.bottom-right .tooltip-arrow { + top: 0; + right: 5px; + border-width: 0 5px 5px; + border-bottom-color: black; } + +/* line 6, ../sass/bootstrap/_popovers.scss */ +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1010; + display: none; + max-width: 276px; + padding: 1px; + text-align: left; + background-color: white; + background-clip: padding-box; + border: 1px solid #cccccc; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 3px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + white-space: normal; } + /* line 26, ../sass/bootstrap/_popovers.scss */ + .popover.top { + margin-top: -10px; } + /* line 27, ../sass/bootstrap/_popovers.scss */ + .popover.right { + margin-left: 10px; } + /* line 28, ../sass/bootstrap/_popovers.scss */ + .popover.bottom { + margin-top: 10px; } + /* line 29, ../sass/bootstrap/_popovers.scss */ + .popover.left { + margin-left: -10px; } + +/* line 32, ../sass/bootstrap/_popovers.scss */ +.popover-title { + margin: 0; + padding: 8px 14px; + font-size: 14px; + font-weight: normal; + line-height: 18px; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + border-radius: 3px 3px 0 0; } + +/* line 43, ../sass/bootstrap/_popovers.scss */ +.popover-content { + padding: 5px; } + +/* line 53, ../sass/bootstrap/_popovers.scss */ +.popover > .arrow, .popover > .arrow:after { + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; } + +/* line 62, ../sass/bootstrap/_popovers.scss */ +.popover > .arrow { + border-width: 11px; } + +/* line 65, ../sass/bootstrap/_popovers.scss */ +.popover > .arrow:after { + border-width: 10px; + content: ""; } + +/* line 71, ../sass/bootstrap/_popovers.scss */ +.popover.top > .arrow { + left: 50%; + margin-left: -11px; + border-bottom-width: 0; + border-top-color: #999999; + border-top-color: fadein(rgba(0, 0, 0, 0.2), 5%); + bottom: -11px; } + /* line 78, ../sass/bootstrap/_popovers.scss */ + .popover.top > .arrow:after { + content: " "; + bottom: 1px; + margin-left: -10px; + border-bottom-width: 0; + border-top-color: white; } +/* line 86, ../sass/bootstrap/_popovers.scss */ +.popover.right > .arrow { + top: 50%; + left: -11px; + margin-top: -11px; + border-left-width: 0; + border-right-color: #999999; + border-right-color: fadein(rgba(0, 0, 0, 0.2), 5%); } + /* line 93, ../sass/bootstrap/_popovers.scss */ + .popover.right > .arrow:after { + content: " "; + left: 1px; + bottom: -10px; + border-left-width: 0; + border-right-color: white; } +/* line 101, ../sass/bootstrap/_popovers.scss */ +.popover.bottom > .arrow { + left: 50%; + margin-left: -11px; + border-top-width: 0; + border-bottom-color: #999999; + border-bottom-color: fadein(rgba(0, 0, 0, 0.2), 5%); + top: -11px; } + /* line 108, ../sass/bootstrap/_popovers.scss */ + .popover.bottom > .arrow:after { + content: " "; + top: 1px; + margin-left: -10px; + border-top-width: 0; + border-bottom-color: white; } +/* line 117, ../sass/bootstrap/_popovers.scss */ +.popover.left > .arrow { + top: 50%; + right: -11px; + margin-top: -11px; + border-right-width: 0; + border-left-color: #999999; + border-left-color: fadein(rgba(0, 0, 0, 0.2), 5%); } + /* line 124, ../sass/bootstrap/_popovers.scss */ + .popover.left > .arrow:after { + content: " "; + right: 1px; + border-right-width: 0; + border-left-color: white; + bottom: -10px; } + +/* line 21, ../sass/bootstrap/_mixins.scss */ +.clearfix:before, .clearfix:after { + content: " "; + display: table; } +/* line 25, ../sass/bootstrap/_mixins.scss */ +.clearfix:after { + clear: both; } + +/* line 12, ../sass/bootstrap/_utilities.scss */ +.center-block { + display: block; + margin-left: auto; + margin-right: auto; } + +/* line 15, ../sass/bootstrap/_utilities.scss */ +.pull-right { + float: right !important; } + +/* line 18, ../sass/bootstrap/_utilities.scss */ +.pull-left { + float: left !important; } + +/* line 27, ../sass/bootstrap/_utilities.scss */ +.hide { + display: none !important; } + +/* line 30, ../sass/bootstrap/_utilities.scss */ +.show { + display: block !important; } + +/* line 33, ../sass/bootstrap/_utilities.scss */ +.invisible { + visibility: hidden; } + +/* line 36, ../sass/bootstrap/_utilities.scss */ +.text-hide { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; } + +/* line 45, ../sass/bootstrap/_utilities.scss */ +.hidden { + display: none !important; + visibility: hidden !important; } + +/* line 54, ../sass/bootstrap/_utilities.scss */ +.affix { + position: fixed; } + +@-ms-viewport { + width: device-width; } + +/* line 648, ../sass/bootstrap/_mixins.scss */ +.visible-xs, .visible-sm, .visible-md, .visible-lg { + display: none !important; } + +@media (max-width: 767px) { + /* line 637, ../sass/bootstrap/_mixins.scss */ + .visible-xs { + display: block !important; } + + /* line 640, ../sass/bootstrap/_mixins.scss */ + table.visible-xs { + display: table; } + + /* line 641, ../sass/bootstrap/_mixins.scss */ + tr.visible-xs { + display: table-row !important; } + + /* line 643, ../sass/bootstrap/_mixins.scss */ + th.visible-xs, + td.visible-xs { + display: table-cell !important; } } +@media (min-width: 768px) and (max-width: 991px) { + /* line 637, ../sass/bootstrap/_mixins.scss */ + .visible-sm { + display: block !important; } + + /* line 640, ../sass/bootstrap/_mixins.scss */ + table.visible-sm { + display: table; } + + /* line 641, ../sass/bootstrap/_mixins.scss */ + tr.visible-sm { + display: table-row !important; } + + /* line 643, ../sass/bootstrap/_mixins.scss */ + th.visible-sm, + td.visible-sm { + display: table-cell !important; } } +@media (min-width: 992px) and (max-width: 1199px) { + /* line 637, ../sass/bootstrap/_mixins.scss */ + .visible-md { + display: block !important; } + + /* line 640, ../sass/bootstrap/_mixins.scss */ + table.visible-md { + display: table; } + + /* line 641, ../sass/bootstrap/_mixins.scss */ + tr.visible-md { + display: table-row !important; } + + /* line 643, ../sass/bootstrap/_mixins.scss */ + th.visible-md, + td.visible-md { + display: table-cell !important; } } +@media (min-width: 1200px) { + /* line 637, ../sass/bootstrap/_mixins.scss */ + .visible-lg { + display: block !important; } + + /* line 640, ../sass/bootstrap/_mixins.scss */ + table.visible-lg { + display: table; } + + /* line 641, ../sass/bootstrap/_mixins.scss */ + tr.visible-lg { + display: table-row !important; } + + /* line 643, ../sass/bootstrap/_mixins.scss */ + th.visible-lg, + td.visible-lg { + display: table-cell !important; } } +@media (max-width: 767px) { + /* line 648, ../sass/bootstrap/_mixins.scss */ + .hidden-xs { + display: none !important; } } +@media (min-width: 768px) and (max-width: 991px) { + /* line 648, ../sass/bootstrap/_mixins.scss */ + .hidden-sm { + display: none !important; } } +@media (min-width: 992px) and (max-width: 1199px) { + /* line 648, ../sass/bootstrap/_mixins.scss */ + .hidden-md { + display: none !important; } } +@media (min-width: 1200px) { + /* line 648, ../sass/bootstrap/_mixins.scss */ + .hidden-lg { + display: none !important; } } +/* line 648, ../sass/bootstrap/_mixins.scss */ +.visible-print { + display: none !important; } + +@media print { + /* line 637, ../sass/bootstrap/_mixins.scss */ + .visible-print { + display: block !important; } + + /* line 640, ../sass/bootstrap/_mixins.scss */ + table.visible-print { + display: table; } + + /* line 641, ../sass/bootstrap/_mixins.scss */ + tr.visible-print { + display: table-row !important; } + + /* line 643, ../sass/bootstrap/_mixins.scss */ + th.visible-print, + td.visible-print { + display: table-cell !important; } } +@media print { + /* line 648, ../sass/bootstrap/_mixins.scss */ + .hidden-print { + display: none !important; } } +/* line 1, ../sass/_loaders.scss */ +.spinner { + text-align: center; } + +/* line 5, ../sass/_loaders.scss */ +.spinner > div { + width: 8px; + height: 8px; + background-color: #222222; + border-radius: 100%; + display: inline-block; + -webkit-animation: bouncedelay 1.4s infinite ease-in-out; + animation: bouncedelay 1.4s infinite ease-in-out; + /* Prevent first frame from flickering when animation starts */ + -webkit-animation-fill-mode: both; + animation-fill-mode: both; } + +/* line 19, ../sass/_loaders.scss */ +.spinner .bounce1 { + -webkit-animation-delay: -0.32s; + animation-delay: -0.32s; } + +/* line 24, ../sass/_loaders.scss */ +.spinner .bounce2 { + -webkit-animation-delay: -0.16s; + animation-delay: -0.16s; } + +@-webkit-keyframes bouncedelay { + /* line 30, ../sass/_loaders.scss */ + 0%, 80%, 100% { + -webkit-transform: scale(0); } + + /* line 31, ../sass/_loaders.scss */ + 40% { + -webkit-transform: scale(1); } } + +@keyframes bouncedelay { + /* line 35, ../sass/_loaders.scss */ + 0%, 80%, 100% { + transform: scale(0); + -webkit-transform: scale(0); } + + /* line 38, ../sass/_loaders.scss */ + 40% { + transform: scale(1); + -webkit-transform: scale(1); } } + +/* +Disabled buttons are transparent with light gray border +and light gray font colors +*/ +/* line 116, ../sass/_bars-btns.scss */ +.line-btn { + display: inline-block; + text-align: center; + opacity: 1; + background-color: #e0e0e0; + border-bottom: 2px solid #e0e0e0; + color: #222222; } + /* line 29, ../sass/_bars-btns.scss */ + .line-btn span { + display: inline-block; + height: 100%; + line-height: 100%; + padding: 8px; } + /* line 36, ../sass/_bars-btns.scss */ + .line-btn:hover, .line-btn:focus { + text-decoration: none; + opacity: 0.85; } + /* line 45, ../sass/_bars-btns.scss */ + .line-btn .snf-font-remove { + display: inline; } + /* line 48, ../sass/_bars-btns.scss */ + .line-btn.disabled { + background: transparent !important; + border-color: #818181 !important; + color: #818181 !important; } + /* line 15, ../sass/_bars-btns.scss */ + .line-btn.disabled:hover, .line-btn.disabled:focus { + cursor: default; + opacity: 1; } + /* line 18, ../sass/_bars-btns.scss */ + .line-btn.disabled:hover span, .line-btn.disabled:focus span { + color: #818181 !important; } + /* line 109, ../sass/_bars-btns.scss */ + .line-btn:hover, .line-btn:focus { + opacity: 1; + border-bottom-color: #222222; + color: #222222; } + +/* line 120, ../sass/_bars-btns.scss */ +.outline-btn { + display: inline-block; + text-align: center; + opacity: 1; + border: 1px solid #222222; + color: #222222; } + /* line 29, ../sass/_bars-btns.scss */ + .outline-btn span { + display: inline-block; + height: 100%; + line-height: 100%; + padding: 8px; } + /* line 36, ../sass/_bars-btns.scss */ + .outline-btn:hover, .outline-btn:focus { + text-decoration: none; + opacity: 0.85; } + /* line 45, ../sass/_bars-btns.scss */ + .outline-btn .snf-font-remove { + display: inline; } + /* line 48, ../sass/_bars-btns.scss */ + .outline-btn.disabled { + background: transparent !important; + border-color: #818181 !important; + color: #818181 !important; } + /* line 15, ../sass/_bars-btns.scss */ + .outline-btn.disabled:hover, .outline-btn.disabled:focus { + cursor: default; + opacity: 1; } + /* line 18, ../sass/_bars-btns.scss */ + .outline-btn.disabled:hover span, .outline-btn.disabled:focus span { + color: #818181 !important; } + /* line 59, ../sass/_bars-btns.scss */ + .outline-btn span { + border: 1px solid transparent; + width: 100%; } + /* line 65, ../sass/_bars-btns.scss */ + .outline-btn:hover span, .outline-btn:focus span { + border-color: #222222; } + /* line 69, ../sass/_bars-btns.scss */ + .outline-btn.disabled { + @inlcude disabled; + color: #818181; } + /* line 74, ../sass/_bars-btns.scss */ + .outline-btn.disabled:hover span, .outline-btn.disabled:focus span { + border-color: transparent; } + +/* line 124, ../sass/_bars-btns.scss */ +.custom-btn { + display: inline-block; + text-align: center; + opacity: 1; + border: 1px solid #3c96e0; + color: white; + background-color: #3c96e0; } + /* line 29, ../sass/_bars-btns.scss */ + .custom-btn span { + display: inline-block; + height: 100%; + line-height: 100%; + padding: 8px; } + /* line 36, ../sass/_bars-btns.scss */ + .custom-btn:hover, .custom-btn:focus { + text-decoration: none; + opacity: 0.85; } + /* line 45, ../sass/_bars-btns.scss */ + .custom-btn .snf-font-remove { + display: inline; } + /* line 48, ../sass/_bars-btns.scss */ + .custom-btn.disabled { + background: transparent !important; + border-color: #818181 !important; + color: #818181 !important; } + /* line 15, ../sass/_bars-btns.scss */ + .custom-btn.disabled:hover, .custom-btn.disabled:focus { + cursor: default; + opacity: 1; } + /* line 18, ../sass/_bars-btns.scss */ + .custom-btn.disabled:hover span, .custom-btn.disabled:focus span { + color: #818181 !important; } + /* line 87, ../sass/_bars-btns.scss */ + .custom-btn span { + border: 1px solid transparent; + background: transparent; } + /* line 93, ../sass/_bars-btns.scss */ + .custom-btn:hover span, .custom-btn:focus span { + color: white; } + /* line 97, ../sass/_bars-btns.scss */ + .custom-btn.disabled { + background: transparent !important; + border-color: #818181 !important; + color: #818181 !important; } + /* line 15, ../sass/_bars-btns.scss */ + .custom-btn.disabled:hover, .custom-btn.disabled:focus { + cursor: default; + opacity: 1; } + /* line 18, ../sass/_bars-btns.scss */ + .custom-btn.disabled:hover span, .custom-btn.disabled:focus span { + color: #818181 !important; } + /* line 126, ../sass/_bars-btns.scss */ + .custom-btn[data-karma="neutral"] { + background-color: #3c96e0; + border-color: #3c96e0; } + /* line 130, ../sass/_bars-btns.scss */ + .custom-btn[data-karma="good"] { + background-color: #00a551; + border-color: #00a551; } + /* line 135, ../sass/_bars-btns.scss */ + .custom-btn[data-karma="bad"] { + background-color: #d2881f; + border-color: #d2881f; } + /* line 140, ../sass/_bars-btns.scss */ + .custom-btn[data-caution="warning"][data-karma="good"], .custom-btn[data-caution="warning"][data-karma="neutral"] { + background-color: #d2881f; + border-color: #d2881f; } + /* line 145, ../sass/_bars-btns.scss */ + .custom-btn[data-caution="dangerous"][data-karma="bad"], .custom-btn[data-caution="dangerous"][data-karma="neutral"] { + background-color: #e42a48; + border-color: #e42a48; } + +/* line 151, ../sass/_bars-btns.scss */ +.search-btn { + display: inline-block; + text-align: center; + opacity: 1; + background-color: #e0e0e0; + border-bottom: 2px solid #e0e0e0; + color: #222222; + position: relative; + top: -2px; + margin-left: 20px; + cursor: pointer; } + /* line 29, ../sass/_bars-btns.scss */ + .search-btn span { + display: inline-block; + height: 100%; + line-height: 100%; + padding: 8px; } + /* line 36, ../sass/_bars-btns.scss */ + .search-btn:hover, .search-btn:focus { + text-decoration: none; + opacity: 0.85; } + /* line 45, ../sass/_bars-btns.scss */ + .search-btn .snf-font-remove { + display: inline; } + /* line 48, ../sass/_bars-btns.scss */ + .search-btn.disabled { + background: transparent !important; + border-color: #818181 !important; + color: #818181 !important; } + /* line 15, ../sass/_bars-btns.scss */ + .search-btn.disabled:hover, .search-btn.disabled:focus { + cursor: default; + opacity: 1; } + /* line 18, ../sass/_bars-btns.scss */ + .search-btn.disabled:hover span, .search-btn.disabled:focus span { + color: #818181 !important; } + /* line 109, ../sass/_bars-btns.scss */ + .search-btn:hover, .search-btn:focus { + opacity: 1; + border-bottom-color: #222222; + color: #222222; } + /* line 156, ../sass/_bars-btns.scss */ + .search-btn span { + padding: 7px; } + +/* line 162, ../sass/_bars-btns.scss */ +.search-mode-btn { + float: right; + line-height: 30px; } + /* line 165, ../sass/_bars-btns.scss */ + .search-mode-btn:hover { + cursor: pointer; } + +/* line 170, ../sass/_bars-btns.scss */ +.instructions .line-btn { + padding: 8px 10px; } + /* line 172, ../sass/_bars-btns.scss */ + .instructions .line-btn span { + padding: 0 4px; } + /* line 176, ../sass/_bars-btns.scss */ + .instructions .line-btn:hover .arrow { + font-weight: bold; } + /* line 180, ../sass/_bars-btns.scss */ + .instructions .line-btn.open:hover { + border-bottom-color: transparent; } + /* line 183, ../sass/_bars-btns.scss */ + .instructions .line-btn .arrow { + vertical-align: middle; } + +/* Sidebar */ +/* line 193, ../sass/_bars-btns.scss */ +.sidebar { + margin: 0 30px 0 0; + width: 110px; + height: auto; + float: left; } + /* line 198, ../sass/_bars-btns.scss */ + .sidebar .btn-group-vertical { + width: 100%; } + @media (max-width: 1200px) { + /* line 193, ../sass/_bars-btns.scss */ + .sidebar { + width: auto; + margin: 20px auto; + float: none; } + /* line 206, ../sass/_bars-btns.scss */ + .sidebar .btn-group-vertical a { + margin-right: 10px; + display: inline-block; } } + +/* +Positioning or customizing buttons +*/ +/* line 219, ../sass/_bars-btns.scss */ +.sidebar .custom-btn { + display: block; + margin: 0 0 1em; } + /* line 222, ../sass/_bars-btns.scss */ + .sidebar .custom-btn span { + padding: 8px; } + +/* line 228, ../sass/_bars-btns.scss */ +body .custom-buttons { + float: left; + margin-right: 10px; } + /* line 231, ../sass/_bars-btns.scss */ + body .custom-buttons .line-btn { + margin-right: 1em; } + /* line 234, ../sass/_bars-btns.scss */ + body .custom-buttons .disabled { + display: none; } + +/* +Extra-button is used to show total selected rows +*/ +/* line 251, ../sass/_bars-btns.scss */ +body .custom-buttons .extra-btn { + float: right; + margin-right: 0; } + /* line 254, ../sass/_bars-btns.scss */ + body .custom-buttons .extra-btn span { + display: inline-block; } + /* line 257, ../sass/_bars-btns.scss */ + body .custom-buttons .extra-btn .badge { + background: transparent; + line-height: 0.8; + display: inline; + padding: 0 5px 0 0; + font-weight: normal; + font-size: 1em; } + /* line 264, ../sass/_bars-btns.scss */ + body .custom-buttons .extra-btn .badge::before { + content: "("; } + /* line 267, ../sass/_bars-btns.scss */ + body .custom-buttons .extra-btn .badge::after { + content: ")"; } + +/* line 273, ../sass/_bars-btns.scss */ +.show-hide-all { + float: right; } + /* line 275, ../sass/_bars-btns.scss */ + .show-hide-all em { + font-style: normal; } + /* line 278, ../sass/_bars-btns.scss */ + .show-hide-all.line-btn { + padding: 8px; } + /* line 280, ../sass/_bars-btns.scss */ + .show-hide-all.line-btn span { + display: inline; } + +/* line 287, ../sass/_bars-btns.scss */ +.actions-per-item .custom-btn { + margin: 10px 10px 10px 0; } + +/* line 292, ../sass/_bars-btns.scss */ +.charts .chart { + display: none; } +/* line 296, ../sass/_bars-btns.scss */ +.charts .sidebar a { + display: inline-block; + text-align: center; + opacity: 1; + border: 1px solid #222222; + color: #222222; + display: block; + margin: 20px auto; } + /* line 29, ../sass/_bars-btns.scss */ + .charts .sidebar a span { + display: inline-block; + height: 100%; + line-height: 100%; + padding: 8px; } + /* line 36, ../sass/_bars-btns.scss */ + .charts .sidebar a:hover, .charts .sidebar a:focus { + text-decoration: none; + opacity: 0.85; } + /* line 45, ../sass/_bars-btns.scss */ + .charts .sidebar a .snf-font-remove { + display: inline; } + /* line 48, ../sass/_bars-btns.scss */ + .charts .sidebar a.disabled { + background: transparent !important; + border-color: #818181 !important; + color: #818181 !important; } + /* line 15, ../sass/_bars-btns.scss */ + .charts .sidebar a.disabled:hover, .charts .sidebar a.disabled:focus { + cursor: default; + opacity: 1; } + /* line 18, ../sass/_bars-btns.scss */ + .charts .sidebar a.disabled:hover span, .charts .sidebar a.disabled:focus span { + color: #818181 !important; } + /* line 59, ../sass/_bars-btns.scss */ + .charts .sidebar a span { + border: 1px solid transparent; + width: 100%; } + /* line 65, ../sass/_bars-btns.scss */ + .charts .sidebar a:hover span, .charts .sidebar a:focus span { + border-color: #222222; } + /* line 69, ../sass/_bars-btns.scss */ + .charts .sidebar a.disabled { + @inlcude disabled; + color: #818181; } + /* line 74, ../sass/_bars-btns.scss */ + .charts .sidebar a.disabled:hover span, .charts .sidebar a.disabled:focus span { + border-color: transparent; } +/* line 301, ../sass/_bars-btns.scss */ +.charts .sidebar a.active { + display: inline-block; + text-align: center; + opacity: 1; + border: 1px solid #3c96e0; + color: white; + background-color: #3c96e0; + display: block; } + /* line 29, ../sass/_bars-btns.scss */ + .charts .sidebar a.active span { + display: inline-block; + height: 100%; + line-height: 100%; + padding: 8px; } + /* line 36, ../sass/_bars-btns.scss */ + .charts .sidebar a.active:hover, .charts .sidebar a.active:focus { + text-decoration: none; + opacity: 0.85; } + /* line 45, ../sass/_bars-btns.scss */ + .charts .sidebar a.active .snf-font-remove { + display: inline; } + /* line 48, ../sass/_bars-btns.scss */ + .charts .sidebar a.active.disabled { + background: transparent !important; + border-color: #818181 !important; + color: #818181 !important; } + /* line 15, ../sass/_bars-btns.scss */ + .charts .sidebar a.active.disabled:hover, .charts .sidebar a.active.disabled:focus { + cursor: default; + opacity: 1; } + /* line 18, ../sass/_bars-btns.scss */ + .charts .sidebar a.active.disabled:hover span, .charts .sidebar a.active.disabled:focus span { + color: #818181 !important; } + /* line 87, ../sass/_bars-btns.scss */ + .charts .sidebar a.active span { + border: 1px solid transparent; + background: transparent; } + /* line 93, ../sass/_bars-btns.scss */ + .charts .sidebar a.active:hover span, .charts .sidebar a.active:focus span { + color: white; } + /* line 97, ../sass/_bars-btns.scss */ + .charts .sidebar a.active.disabled { + background: transparent !important; + border-color: #818181 !important; + color: #818181 !important; } + /* line 15, ../sass/_bars-btns.scss */ + .charts .sidebar a.active.disabled:hover, .charts .sidebar a.active.disabled:focus { + cursor: default; + opacity: 1; } + /* line 18, ../sass/_bars-btns.scss */ + .charts .sidebar a.active.disabled:hover span, .charts .sidebar a.active.disabled:focus span { + color: #818181 !important; } +@media (max-width: 1200px) { + /* line 306, ../sass/_bars-btns.scss */ + .charts .sidebar a, .charts .sidebar a.active { + margin-right: 10px; + display: inline-block; } } + +/* line 314, ../sass/_bars-btns.scss */ +.notify .reload-btn { + padding: 0 4px; + font-size: 18px; + vertical-align: middle; + cursor: pointer; } + +/* Switch in filters */ +/* line 323, ../sass/_bars-btns.scss */ +.onoffswitch { + display: inline-block; + float: right; + position: relative; + width: 134px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; } + +/* line 332, ../sass/_bars-btns.scss */ +.onoffswitch-checkbox { + display: none; } + +/* line 335, ../sass/_bars-btns.scss */ +.onoffswitch-label { + display: block; + overflow: hidden; + cursor: pointer; + /*border: 2px solid #F7EFEF;*/ + border-radius: 20px; } + +/* line 342, ../sass/_bars-btns.scss */ +.onoffswitch-inner { + display: block; + width: 200%; + margin-left: -100%; + -moz-transition: margin 0.3s ease-in 0s; + -webkit-transition: margin 0.3s ease-in 0s; + -o-transition: margin 0.3s ease-in 0s; + transition: margin 0.3s ease-in 0s; } + +/* line 349, ../sass/_bars-btns.scss */ +.onoffswitch-inner:before, .onoffswitch-inner:after { + display: block; + float: left; + width: 50%; + height: 30px; + padding: 0; + line-height: 30px; + font-size: 12px; + color: white; + font-family: Trebuchet, Arial, sans-serif; + font-weight: normal; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; } + +/* line 362, ../sass/_bars-btns.scss */ +.onoffswitch-inner:before { + content: "Standard View"; + padding-left: 10px; + background-color: #e0e0e0; + color: #222222; } + +/* line 368, ../sass/_bars-btns.scss */ +.onoffswitch-inner:after { + content: "Compact View"; + padding-right: 10px; + background-color: #e0e0e0; + color: #222222; + text-align: right; } + +/* line 375, ../sass/_bars-btns.scss */ +.onoffswitch-switch { + display: block; + width: 19px; + margin: 6px; + background: #222222; + border: 2px solid #F7EFEF; + border-radius: 20px; + position: absolute; + top: 0; + bottom: 4px; + right: 103px; + -moz-transition: all 0.3s ease-in 0s; + -webkit-transition: all 0.3s ease-in 0s; + -o-transition: all 0.3s ease-in 0s; + transition: all 0.3s ease-in 0s; } + +/* line 391, ../sass/_bars-btns.scss */ +.onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-inner { + margin-left: 0; } + +/* line 394, ../sass/_bars-btns.scss */ +.onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-switch { + right: 0px; } + +/* Clickable elements that change state */ +/* These are icon-fonts. We insert in html two icons (one for each state) */ +/* The icon with the false state is hidden and only the correct one is displayed */ +/* Which state is the correct it comes from the class of a parent element */ +/* line 404, ../sass/_bars-btns.scss */ +li.active .snf-checkbox-unchecked, li.active .snf-radio-unchecked { + display: none; } + +/* line 407, ../sass/_bars-btns.scss */ +li:not(.active) .snf-checkbox-checked, li:not(.active) .snf-radio-checked { + display: none; } + +/* line 411, ../sass/_bars-btns.scss */ +table.dataTable tbody tr.selected .snf-checkbox-unchecked { + display: none; } + +/* line 415, ../sass/_bars-btns.scss */ +table.dataTable tbody tr:not(.selected) .snf-checkbox-checked { + display: none; } + +/* line 418, ../sass/_bars-btns.scss */ +.show-hide-all.open .snf-font-arrow-down { + display: none; } + +/* line 421, ../sass/_bars-btns.scss */ +.show-hide-all:not(.open) .snf-font-arrow-up { + display: none; } + +/* line 425, ../sass/_bars-btns.scss */ +.instructions .line-btn.open .snf-angle-down { + display: none; } + +/* line 429, ../sass/_bars-btns.scss */ +.instructions .line-btn:not(.open) .snf-angle-up { + display: none; } + +@font-face { + font-family: 'font-icons'; + src: url("../fonts/font-icons.eot?hm0cup"); + src: url("../fonts/font-icons.eot?#iefixhm0cup") format("embedded-opentype"), url("../fonts/font-icons.woff?hm0cup") format("woff"), url("../fonts/font-icons.ttf?hm0cup") format("truetype"), url("../fonts/font-icons.svg?hm0cup#font-icons") format("svg"); + font-weight: normal; + font-style: normal; } + +/* Font with kpal icons */ +@font-face { + font-family: "snf-font"; + src: url("../fonts/snf-font.eot"); + src: url("../fonts/snf-font.eot?#iefix") format("embedded-opentype"), url("../fonts/snf-font.woff") format("woff"), url("../fonts/snf-font.ttf") format("truetype"), url("../fonts/snf-font.svg#snf-font") format("svg"); + font-weight: normal; + font-style: normal; } + +/* line 47, ../sass/icon-fonts.scss */ +.snf-ok { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-ok:before { + content: "\61"; } + +/* line 50, ../sass/icon-fonts.scss */ +.snf-remove, body .custom-buttons .snf-font-remove { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-remove:before, body .custom-buttons .snf-font-remove:before { + content: "\62"; } + +/* line 53, ../sass/icon-fonts.scss */ +.snf-envelope { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-envelope:before { + content: "\63"; } + +/* line 56, ../sass/icon-fonts.scss */ +.snf-envelope-alt { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-envelope-alt:before { + content: "\64"; } + +/* line 59, ../sass/icon-fonts.scss */ +.snf-angle-up { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-angle-up:before { + content: "\65"; } + +/* line 62, ../sass/icon-fonts.scss */ +.snf-angle-down { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-angle-down:before { + content: "\66"; } + +/* line 65, ../sass/icon-fonts.scss */ +.snf-exclamation-sign { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-exclamation-sign:before { + content: "\67"; } + +/* line 68, ../sass/icon-fonts.scss */ +.snf-clipboard-h, .snf-details-project { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-clipboard-h:before, .snf-details-project:before { + content: "\68"; } + +/* line 71, ../sass/icon-fonts.scss */ +.snf-clipboard-i { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-clipboard-i:before { + content: "\69"; } + +/* line 74, ../sass/icon-fonts.scss */ +.snf-copy { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-copy:before { + content: "\6c"; } + +/* line 77, ../sass/icon-fonts.scss */ +.snf-search { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-search:before { + content: "\6d"; } + +/* line 80, ../sass/icon-fonts.scss */ +.snf-sign-out { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-sign-out:before { + content: "\6e"; } + +/* line 83, ../sass/icon-fonts.scss */ +.snf-archive { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-archive:before { + content: "\6b"; } + +/* line 86, ../sass/icon-fonts.scss */ +.snf-checkbox-checked { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-checkbox-checked:before { + content: "\6f"; } + +/* line 89, ../sass/icon-fonts.scss */ +.snf-checkbox-unchecked { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-checkbox-unchecked:before { + content: "\70"; } + +/* line 92, ../sass/icon-fonts.scss */ +.snf-radio-checked { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-radio-checked:before { + content: "\71"; } + +/* line 95, ../sass/icon-fonts.scss */ +.snf-radio-unchecked { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-radio-unchecked:before { + content: "\72"; } + +/* line 98, ../sass/icon-fonts.scss */ +.snf-info { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-info:before { + content: "\73"; } + +/* line 101, ../sass/icon-fonts.scss */ +.snf-user-outline { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-user-outline:before { + content: "\75"; } + +/* line 104, ../sass/icon-fonts.scss */ +.snf-user-full, .snf-details-user { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-user-full:before, .snf-details-user:before { + content: "\74"; } + +/* line 107, ../sass/icon-fonts.scss */ +.snf-wallet-full, .snf-details-quota { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-wallet-full:before, .snf-details-quota:before { + content: "\78"; } + +/* line 110, ../sass/icon-fonts.scss */ +.snf-wallet-outline { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-wallet-outline:before { + content: "\79"; } + +/* line 113, ../sass/icon-fonts.scss */ +.snf-keyboard { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-keyboard:before { + content: "\7a"; } + +/* line 116, ../sass/icon-fonts.scss */ +.snf-book-2 { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-book-2:before { + content: "\42"; } + +/* line 119, ../sass/icon-fonts.scss */ +.snf-bell-1 { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-bell-1:before { + content: "\43"; } + +/* line 122, ../sass/icon-fonts.scss */ +.snf-bulb { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-bulb:before { + content: "\46"; } + +/* line 125, ../sass/icon-fonts.scss */ +.snf-sun-1 { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-sun-1:before { + content: "\47"; } + +/* line 128, ../sass/icon-fonts.scss */ +.snf-moon-1 { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-moon-1:before { + content: "\76"; } + +/* line 131, ../sass/icon-fonts.scss */ +.snf-sun-2-full { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-sun-2-full:before { + content: "\77"; } + +/* line 134, ../sass/icon-fonts.scss */ +.snf-sun-2-outline { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-sun-2-outline:before { + content: "\6a"; } + +/* line 137, ../sass/icon-fonts.scss */ +.snf-moon-2-full:before { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-moon-2-full:before:before { + content: "\44"; } + +/* line 140, ../sass/icon-fonts.scss */ +.snf-moon-2-outline { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-moon-2-outline:before { + content: "\45"; } + +/* line 143, ../sass/icon-fonts.scss */ +.snf-sun-3 { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-sun-3:before { + content: "\41"; } + +/* line 146, ../sass/icon-fonts.scss */ +.snf-filter { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-filter:before { + content: "\7b"; } + +/* line 149, ../sass/icon-fonts.scss */ +.snf-eye { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-eye:before { + content: "\41"; } + +/* line 152, ../sass/icon-fonts.scss */ +.snf-radio-checked { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-radio-checked:before { + content: "\42"; } + +/* line 155, ../sass/icon-fonts.scss */ +.snf-radio-unchecked { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-radio-unchecked:before { + content: "\43"; } + +/* line 158, ../sass/icon-fonts.scss */ +.snf-close { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-close:before { + content: "\44"; } + +/* line 161, ../sass/icon-fonts.scss */ +.snf-www { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-www:before { + content: "\49"; } + +/* line 164, ../sass/icon-fonts.scss */ +.snf-arrow-up, .show-hide-all span.snf-font-arrow-up { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-arrow-up:before, .show-hide-all span.snf-font-arrow-up:before { + content: "\4c"; } + +/* line 167, ../sass/icon-fonts.scss */ +.snf-arrow-down, .show-hide-all span.snf-font-arrow-down { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-arrow-down:before, .show-hide-all span.snf-font-arrow-down:before { + content: "\4d"; } + +/* line 170, ../sass/icon-fonts.scss */ +.snf-checkbox-unchecked { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-checkbox-unchecked:before { + content: "\61"; } + +/* line 173, ../sass/icon-fonts.scss */ +.snf-checkbox-checked { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-checkbox-checked:before { + content: "\62"; } + +/* line 176, ../sass/icon-fonts.scss */ +.snf-cancel-circled { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-cancel-circled:before { + content: "\63"; } + +/* line 179, ../sass/icon-fonts.scss */ +.snf-search { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-search:before { + content: "\64"; } + +/* line 182, ../sass/icon-fonts.scss */ +.snf-twitter-logo { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-twitter-logo:before { + content: "\67"; } + +/* line 185, ../sass/icon-fonts.scss */ +.snf-ok { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-ok:before { + content: "\68"; } + +/* line 188, ../sass/icon-fonts.scss */ +.snf-switch { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-switch:before { + content: "\69"; } + +/* line 191, ../sass/icon-fonts.scss */ +.snf-ban-circle { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-ban-circle:before { + content: "\6a"; } + +/* line 194, ../sass/icon-fonts.scss */ +.snf-ok-sign { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-ok-sign:before { + content: "\6c"; } + +/* line 197, ../sass/icon-fonts.scss */ +.snf-minus-sign { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-minus-sign:before { + content: "\6e"; } + +/* line 200, ../sass/icon-fonts.scss */ +.snf-edit { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-edit:before { + content: "\71"; } + +/* line 203, ../sass/icon-fonts.scss */ +.snf-listview { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-listview:before { + content: "\73"; } + +/* line 206, ../sass/icon-fonts.scss */ +.snf-gridview { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-gridview:before { + content: "\74"; } + +/* line 209, ../sass/icon-fonts.scss */ +.snf-dashboard-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-dashboard-outline:before { + content: "\7a"; } + +/* line 212, ../sass/icon-fonts.scss */ +.snf-pithos-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-pithos-outline:before { + content: "\79"; } + +/* line 215, ../sass/icon-fonts.scss */ +.snf-info-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-info-full:before { + content: "\70"; } + +/* line 218, ../sass/icon-fonts.scss */ +.snf-volume-create-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-volume-create-full:before { + content: "\36"; } + +/* line 221, ../sass/icon-fonts.scss */ +.snf-image-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-image-full:before { + content: "\51"; } + +/* line 224, ../sass/icon-fonts.scss */ +.snf-pc-create-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-pc-create-full:before { + content: "\53"; } + +/* line 227, ../sass/icon-fonts.scss */ +.snf-network-create-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-network-create-outline:before { + content: "\54"; } + +/* line 230, ../sass/icon-fonts.scss */ +.snf-network-create-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-network-create-full:before { + content: "\55"; } + +/* line 233, ../sass/icon-fonts.scss */ +.snf-ram-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-ram-outline:before { + content: "\4a"; } + +/* line 236, ../sass/icon-fonts.scss */ +.snf-nic-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-nic-outline:before { + content: "\50"; } + +/* line 239, ../sass/icon-fonts.scss */ +.snf-ram-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-ram-full:before { + content: "\52"; } + +/* line 242, ../sass/icon-fonts.scss */ +.snf-nic-full, .snf-details-nic { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-nic-full:before, .snf-details-nic:before { + content: "\72"; } + +/* line 245, ../sass/icon-fonts.scss */ +.snf-network-broken-1-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-network-broken-1-full:before { + content: "\56"; } + +/* line 248, ../sass/icon-fonts.scss */ +.snf-network-broken-2-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-network-broken-2-full:before { + content: "\57"; } + +/* line 251, ../sass/icon-fonts.scss */ +.snf-pc-broken-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-pc-broken-full:before { + content: "\58"; } + +/* line 254, ../sass/icon-fonts.scss */ +.snf-pc-reboot-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-pc-reboot-full:before { + content: "\59"; } + +/* line 257, ../sass/icon-fonts.scss */ +.snf-pc-switch-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-pc-switch-full:before { + content: "\5a"; } + +/* line 260, ../sass/icon-fonts.scss */ +.snf-key-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-key-full:before { + content: "\31"; } + +/* line 263, ../sass/icon-fonts.scss */ +.snf-router-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-router-full:before { + content: "\32"; } + +/* line 266, ../sass/icon-fonts.scss */ +.snf-chip-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-chip-full:before { + content: "\33"; } + +/* line 269, ../sass/icon-fonts.scss */ +.snf-plus-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-plus-full:before { + content: "\34"; } + +/* line 272, ../sass/icon-fonts.scss */ +.snf-snapshot-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-snapshot-full:before { + content: "\4e"; } + +/* line 275, ../sass/icon-fonts.scss */ +.snf-pithos-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-pithos-full:before { + content: "\35"; } + +/* line 278, ../sass/icon-fonts.scss */ +.snf-volume-full, .snf-details-volume { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-volume-full:before, .snf-details-volume:before { + content: "\4f"; } + +/* line 281, ../sass/icon-fonts.scss */ +.snf-network-full, .snf-details-network { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-network-full:before, .snf-details-network:before { + content: "\4b"; } + +/* line 284, ../sass/icon-fonts.scss */ +.snf-pc-full, .snf-details-vm { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-pc-full:before, .snf-details-vm:before { + content: "\78"; } + +/* line 287, ../sass/icon-fonts.scss */ +.snf-network-broken-1-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-network-broken-1-outline:before { + content: "\37"; } + +/* line 290, ../sass/icon-fonts.scss */ +.snf-network-broken-2-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-network-broken-2-outline:before { + content: "\38"; } + +/* line 293, ../sass/icon-fonts.scss */ +.snf-pc-broken-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-pc-broken-outline:before { + content: "\39"; } + +/* line 296, ../sass/icon-fonts.scss */ +.snf-volume-broken-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-volume-broken-outline:before { + content: "\30"; } + +/* line 299, ../sass/icon-fonts.scss */ +.snf-pc-reboot-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-pc-reboot-outline:before { + content: "\21"; } + +/* line 302, ../sass/icon-fonts.scss */ +.snf-pc-switch-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-pc-switch-outline:before { + content: "\40"; } + +/* line 305, ../sass/icon-fonts.scss */ +.snf-key-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-key-outline:before { + content: "\23"; } + +/* line 308, ../sass/icon-fonts.scss */ +.snf-router-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-router-outline:before { + content: "\48"; } + +/* line 311, ../sass/icon-fonts.scss */ +.snf-chip-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-chip-outline:before { + content: "\45"; } + +/* line 314, ../sass/icon-fonts.scss */ +.snf-image-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-image-outline:before { + content: "\66"; } + +/* line 317, ../sass/icon-fonts.scss */ +.snf-plus-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-plus-outline:before { + content: "\6d"; } + +/* line 320, ../sass/icon-fonts.scss */ +.snf-snapshot-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-snapshot-outline:before { + content: "\65"; } + +/* line 323, ../sass/icon-fonts.scss */ +.snf-volume-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-volume-outline:before { + content: "\75"; } + +/* line 326, ../sass/icon-fonts.scss */ +.snf-network-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-network-outline:before { + content: "\76"; } + +/* line 329, ../sass/icon-fonts.scss */ +.snf-pc-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-pc-outline:before { + content: "\77"; } + +/* line 332, ../sass/icon-fonts.scss */ +.snf-info-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-info-outline:before { + content: "\6f"; } + +/* line 335, ../sass/icon-fonts.scss */ +.snf-thunder-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-thunder-full:before { + content: "\6b"; } + +/* line 338, ../sass/icon-fonts.scss */ +.snf-lock-closed-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-lock-closed-full:before { + content: "\46"; } + +/* line 341, ../sass/icon-fonts.scss */ +.snf-lock-open-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-lock-open-full:before { + content: "\47"; } + +/* line 345, ../sass/icon-fonts.scss */ +.snf-link-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-link-outline:before { + content: "\26"; } + +/* line 348, ../sass/icon-fonts.scss */ +.snf-refresh-outline, body .custom-buttons .snf-font-reload { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-refresh-outline:before, body .custom-buttons .snf-font-reload:before { + content: "\29"; } + +/* line 351, ../sass/icon-fonts.scss */ +.snf-download-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-download-full:before { + content: "\25"; } + +/* line 354, ../sass/icon-fonts.scss */ +.snf-person-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-person-outline:before { + content: "\2a"; } + +/* line 357, ../sass/icon-fonts.scss */ +.snf-upload-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-upload-full:before { + content: "\28"; } + +/* line 360, ../sass/icon-fonts.scss */ +.snf-arrow-right-small-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-arrow-right-small-full:before { + content: "\2d"; } + +/* line 363, ../sass/icon-fonts.scss */ +.snf-copy-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-copy-outline:before { + content: "\3f"; } + +/* line 366, ../sass/icon-fonts.scss */ +.snf-copy-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-copy-full:before { + content: "\22"; } + +/* line 369, ../sass/icon-fonts.scss */ +.snf-arrow-left-small-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-arrow-left-small-full:before { + content: "\5f"; } + +/* line 372, ../sass/icon-fonts.scss */ +.snf-trash-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-trash-full:before { + content: "\3d"; } + +/* line 375, ../sass/icon-fonts.scss */ +.snf-trash-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-trash-outline:before { + content: "\24"; } + +/* line 3, ../sass/_details.scss */ +.main { + margin: 2em 0 5em; } + /* line 5, ../sass/_details.scss */ + .main h4 .title { + font-size: 24px; } + /* line 8, ../sass/_details.scss */ + .main span[class^="snf-details"] { + float: left; + margin-right: 8px; + font-size: 35px; } + /* line 13, ../sass/_details.scss */ + .main .lt { + line-height: 35px; } + /* line 16, ../sass/_details.scss */ + .main .rt { + padding-top: 5px; } + /* line 19, ../sass/_details.scss */ + .main .actions-per-item { + padding: 0; } + +/* line 24, ../sass/_details.scss */ +.object-anchor { + height: 2px; } + +/* line 27, ../sass/_details.scss */ +.object-details h4 { + font-size: 14px; + letter-spacing: 1px; } + /* line 30, ../sass/_details.scss */ + .object-details h4 .lt { + display: block; + float: left; + max-width: 60%; + word-wrap: break-word; } + /* line 36, ../sass/_details.scss */ + .object-details h4 .rt { + padding-top: 5px; + display: block; + overflow: hidden; } + /* line 41, ../sass/_details.scss */ + .object-details h4 .arrow { + position: relative; + padding: 0 8px; } + /* line 45, ../sass/_details.scss */ + .object-details h4 .arrow:hover, .object-details h4 .arrow:focus { + top: 2px; + cursor: pointer; + outline: 0 none; } + /* line 51, ../sass/_details.scss */ + .object-details h4 .label { + float: right; + margin-left: 15px; + margin-bottom: 10px; } + /* line 55, ../sass/_details.scss */ + .object-details h4 .label.important { + font-weight: bold; } + /* line 59, ../sass/_details.scss */ + .object-details h4 em { + float: none; } + /* line 61, ../sass/_details.scss */ + .object-details h4 em.os-info { + float: right; + position: relative; + bottom: 3px; } + /* line 65, ../sass/_details.scss */ + .object-details h4 em.os-info img { + height: 26px; + margin-right: 5px; } +/* line 72, ../sass/_details.scss */ +.object-details h3 { + font-size: 18px; + margin: 0 0 1em; + font-weight: 400; + line-height: 35px; } + /* line 77, ../sass/_details.scss */ + .object-details h3 em { + margin-left: 10px; + font-size: 14px; + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 50%; + vertical-align: top; } + /* line 85, ../sass/_details.scss */ + .object-details h3 span[class^="snf-details"] { + float: left; + margin-right: 8px; + font-size: 25px; + height: 35px; + line-height: 35px; } + /* line 92, ../sass/_details.scss */ + .object-details h3 .popover-dismiss { + display: inline-block; + width: 18px; + height: 18px; + background: #cccccc; + -webkit-border-radius: 9px; + -moz-border-radius: 9px; + border-radius: 9px; + text-align: center; + font-weight: bold; + vertical-align: middle; + line-height: 18px; + font-size: 16px; + vertical-align: super; + cursor: pointer; + margin-left: 10px; + color: white; } + /* line 105, ../sass/_details.scss */ + .object-details h3 .popover-dismiss:hover, .object-details h3 .popover-dismiss:focus { + background: #b3b3b3; + color: #eeeeee; } + /* line 111, ../sass/_details.scss */ + .object-details h3 .popover .popover-content { + font-size: 12px; + line-height: 130%; } +/* line 117, ../sass/_details.scss */ +.object-details .icon-link { + margin-right: 10px; } +/* line 120, ../sass/_details.scss */ +.object-details p { + margin: 10px 20px; + font-style: italic; } +/* line 125, ../sass/_details.scss */ +.object-details .length { + margin-left: 6px; + border: 0 none; + font-style: italic; } + /* line 129, ../sass/_details.scss */ + .object-details .length::before { + content: '( '; } + /* line 132, ../sass/_details.scss */ + .object-details .length::after { + content: ' )'; } +/* line 136, ../sass/_details.scss */ +.object-details > .object-details { + margin-left: -20px; + margin-right: -20px; + padding: 12px 20px; } + +/* line 144, ../sass/_details.scss */ +.object-details-content .nav-tabs > li a { + opacity: 0.7; } +/* line 147, ../sass/_details.scss */ +.object-details-content .nav-tabs > li.active > a { + opacity: 1; } +/* line 152, ../sass/_details.scss */ +.object-details-content .nav-tabs > li:not(.active) > a:hover, .object-details-content .nav-tabs > li:not(.active) > a:focus { + opacity: 1; } + +/* line 157, ../sass/_details.scss */ +.tab-pane { + overflow: auto; } + +/* line 161, ../sass/_details.scss */ +.parts-separator { + border-top: 2px solid #e0e0e0; + padding-top: 1em; } + /* line 164, ../sass/_details.scss */ + .parts-separator h2 { + font-size: 24px; + margin-bottom: 2em; + padding-top: 1em; } + /* line 168, ../sass/_details.scss */ + .parts-separator h2 em { + max-width: 50%; + display: inline; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + vertical-align: top; } + +/* line 179, ../sass/_details.scss */ +.part-two > .object-details { + border-bottom: 2px solid #e0e0e0; + background: #ececec; + padding: 14px 20px; + overflow-x: auto; } + /* line 184, ../sass/_details.scss */ + .part-two > .object-details .object-details { + padding: 5px 20px; } + /* line 187, ../sass/_details.scss */ + .part-two > .object-details .object-details:hover, .part-two > .object-details .object-details:focus { + background: #e2e2e2; } + /* line 192, ../sass/_details.scss */ + .part-two > .object-details .custom-btn span { + padding: 5px; } +/* line 197, ../sass/_details.scss */ +.part-two .object-details-content { + display: none; + padding: 0 35px; } + +/* line 230, ../sass/_details.scss */ +.show-hide-all span.snf-font-arrow-up { + padding: 0; } +/* line 234, ../sass/_details.scss */ +.show-hide-all span.snf-font-arrow-down { + padding: 0; } + +/* line 5, ../sass/_filters.scss */ +.filters-area { + margin-bottom: 40px; + margin-left: 140px; } + @media (max-width: 1200px) { + /* line 5, ../sass/_filters.scss */ + .filters-area { + margin: 0 10px 10px 0; } } + /* line 11, ../sass/_filters.scss */ + .filters-area.no-margin-left { + margin-left: 0; } + /* line 14, ../sass/_filters.scss */ + .filters-area a:focus, .filters-area input:focus { + outline: none; } + /* line 17, ../sass/_filters.scss */ + .filters-area .badge { + margin-left: 6px; + opacity: 0.9; + padding: 2px 9px; } + /* line 22, ../sass/_filters.scss */ + .filters-area ul.nav a { + padding-bottom: 10px; } + +/* line 27, ../sass/_filters.scss */ +.filter { + height: 30px; + margin: 0 10px 10px 0; + display: inline-block; + background: #ececec; + border: 1px solid #cccccc; } + /* line 33, ../sass/_filters.scss */ + .filter .form-group { + margin: 0; + height: 30px; } + /* line 38, ../sass/_filters.scss */ + .filter label, + .filter .dropdown { + height: 30px; + line-height: 30px; + border: 0 none; + padding: 0 10px; + color: #222222; + background: transparent; + font-weight: normal; + margin: 0; } + /* line 48, ../sass/_filters.scss */ + .filter label > a .selected-value, + .filter .dropdown > a .selected-value { + margin-left: 4px; } + /* line 51, ../sass/_filters.scss */ + .filter label > a .arrow, + .filter .dropdown > a .arrow { + font-weight: bold; } + /* line 56, ../sass/_filters.scss */ + .filter label.open a, + .filter .dropdown.open a { + text-decoration: none; + color: #222222; } + /* line 61, ../sass/_filters.scss */ + .filter label a, + .filter .dropdown a { + color: #222222; } + /* line 65, ../sass/_filters.scss */ + .filter .dropdown-menu, .filter .dropdown-list { + background: #ececec; + margin: 0; + width: auto; } + /* line 69, ../sass/_filters.scss */ + .filter .dropdown-menu > .active > a, .filter .dropdown-list > .active > a { + background: lightgrey; } + /* line 72, ../sass/_filters.scss */ + .filter .dropdown-menu > li:hover > a, .filter .dropdown-list > li:hover > a { + background: #dfdfdf; + color: inherit; } + /* line 76, ../sass/_filters.scss */ + .filter .dropdown-menu a, .filter .dropdown-list a { + padding-left: 12px; + padding-right: 12px; } + /* line 77, ../sass/_filters.scss */ + .filter .dropdown-menu a span, .filter .dropdown-list a span { + margin-right: 6px; } + /* line 84, ../sass/_filters.scss */ + .filter input { + border: 0 none; + background: transparent; + height: 30px; + line-height: 30px; + padding: 0 5px; + font-weight: normal; + color: #222222; } + /* line 93, ../sass/_filters.scss */ + .filter .dropdown-list > li > a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 1.42857; + color: #303030; + white-space: nowrap; } + +/* line 104, ../sass/_filters.scss */ +.input-with-btn { + border-width: 0px; + background-color: transparent; + display: inline; } + @media screen and (min-width: 400px) { + /* line 108, ../sass/_filters.scss */ + .input-with-btn input { + width: 200px; } } + @media screen and (min-width: 600px) { + /* line 108, ../sass/_filters.scss */ + .input-with-btn input { + width: 300px; } } + @media screen and (min-width: 800px) { + /* line 108, ../sass/_filters.scss */ + .input-with-btn input { + width: 500px; } } + @media screen and (min-width: 1000px) { + /* line 108, ../sass/_filters.scss */ + .input-with-btn input { + width: 700px; } } + /* line 122, ../sass/_filters.scss */ + .input-with-btn .form-group { + display: inline-block; + background: #ececec; + border: 1px solid #cccccc; + margin-bottom: 0.6em; } + /* line 128, ../sass/_filters.scss */ + .input-with-btn .filter-error { + word-wrap: break-word; } + /* line 131, ../sass/_filters.scss */ + .input-with-btn .error-sign { + display: block; + opacity: 0; + position: static; + display: inline-block; + margin-right: 6px; + margin-left: 10px; + vertical-align: bottom; } + /* line 141, ../sass/_filters.scss */ + .input-with-btn .instructions { + margin-top: 0.6em; } + /* line 143, ../sass/_filters.scss */ + .input-with-btn .instructions * { + color: #222222; } + /* line 146, ../sass/_filters.scss */ + .input-with-btn .instructions .content-area { + display: none; + background: #e0e0e0; + padding: 12px 13px 18px; } + /* line 150, ../sass/_filters.scss */ + .input-with-btn .instructions .content-area dt { + width: 200px; } + /* line 153, ../sass/_filters.scss */ + .input-with-btn .instructions .content-area dd { + margin-left: 220px; } + /* line 157, ../sass/_filters.scss */ + .input-with-btn .instructions .clarifications { + font-style: italic; } + +/* line 164, ../sass/_filters.scss */ +.filter:not(.visible-filter):not(.visible-filter-fade) { + display: none; + opacity: 0; } + +/* line 169, ../sass/_filters.scss */ +.visible-filter-fade { + opacity: 1; + transition: opacity 0.5s; } + +/* line 174, ../sass/_filters.scss */ +.filters .filters-list { + border-radius: 15px; + background: #e0e0e0; + border: 1px solid #cccccc; + height: 28px; } + /* line 179, ../sass/_filters.scss */ + .filters .filters-list > a { + color: #222222; + line-height: 28px; + font-weight: bold; + padding: 8px 7px; + background: transparent; } + /* line 186, ../sass/_filters.scss */ + .filters .filters-list .popover { + padding: 0; } + /* line 189, ../sass/_filters.scss */ + .filters .filters-list .popover-content { + padding: 0; } + /* line 192, ../sass/_filters.scss */ + .filters .filters-list .popover ul { + list-style: none; + padding: 5px 0px; + min-width: 160px; } + /* line 196, ../sass/_filters.scss */ + .filters .filters-list .popover ul li { + white-space: nowrap; } + /* line 198, ../sass/_filters.scss */ + .filters .filters-list .popover ul li a { + color: #222222; } + /* line 201, ../sass/_filters.scss */ + .filters .filters-list .popover ul li span { + margin-right: 10px; } + /* line 205, ../sass/_filters.scss */ + .filters .filters-list .popover ul .divider { + height: 1px; + margin: 9px 0; + overflow: hidden; + background-color: #e5e5e5; } + /* line 212, ../sass/_filters.scss */ + .filters .filters-list .popover.bottom > .arrow:after { + border-bottom-color: #ececec; } + +/* line 1, ../sass/_modals.scss */ +p.progress-area { + visibility: hidden; } + +/* line 6, ../sass/_modals.scss */ +.in-progress .modal-body { + background-color: #818181; } + /* line 8, ../sass/_modals.scss */ + .in-progress .modal-body p.progress-area { + visibility: visible; } + +/* line 16, ../sass/_modals.scss */ +.modal[data-item="user"]:not([data-type="contact"]) .table-selected td:nth-child(3) { + display: none; } +/* line 22, ../sass/_modals.scss */ +.modal#user-contact p { + margin-top: 18px; + position: relative; } +/* line 27, ../sass/_modals.scss */ +.modal p { + position: relative; } +/* line 30, ../sass/_modals.scss */ +.modal p > .error-sign { + top: 0; } +/* line 34, ../sass/_modals.scss */ +.modal h3 { + margin-top: 0; + font-weight: bold; } +/* line 38, ../sass/_modals.scss */ +.modal textarea { + resize: vertical; } +/* line 42, ../sass/_modals.scss */ +.modal textarea, .modal input { + width: 87%; + vertical-align: text-top; + padding: 4px 8px; + border: 1px solid #d9d9d9; + color: #222222; } + /* line 48, ../sass/_modals.scss */ + .modal textarea.body, .modal input.body { + min-height: 160px; } +/* line 53, ../sass/_modals.scss */ +.modal label { + margin-right: 6px; + width: 70px; + vertical-align: sub; } +/* line 60, ../sass/_modals.scss */ +.modal .modal-body { + background-color: white; } +/* line 65, ../sass/_modals.scss */ +.modal .modal-footer { + margin-top: 0; } + /* line 67, ../sass/_modals.scss */ + .modal .modal-footer form { + display: inline; } + /* line 70, ../sass/_modals.scss */ + .modal .modal-footer .custom-btn:first-child { + float: left; + background-color: #303030; + border-color: #303030; } + +/* line 80, ../sass/_modals.scss */ +.modal .custom-btn { + color: white; + opacity: 0.9; } + /* line 84, ../sass/_modals.scss */ + .modal .custom-btn:hover, .modal .custom-btn:focus { + opacity: 1; } +/* line 89, ../sass/_modals.scss */ +.modal[data-karma="dark"] .elem { + color: #4e4e4e; } +/* line 94, ../sass/_modals.scss */ +.modal[data-karma="neutral"] .elem { + color: #207dc9; } +/* line 100, ../sass/_modals.scss */ +.modal[data-karma="good"] .elem { + color: #007238; } +/* line 108, ../sass/_modals.scss */ +.modal[data-karma="bad"] .elem { + color: #a66b18; } +/* line 115, ../sass/_modals.scss */ +.modal[data-caution="warning"][data-karma="good"] .elem, .modal[data-caution="warning"][data-karma="neutral"] .elem { + color: #a66b18; } +/* line 122, ../sass/_modals.scss */ +.modal[data-caution="dangerous"][data-karma="bad"] .elem, .modal[data-caution="dangerous"][data-karma="neutral"] .elem { + color: #c21934; } + +/* line 129, ../sass/_modals.scss */ +.custom-btn[data-karma="dark"] { + background-color: #222222; + border-color: transparent; } + +/* line 134, ../sass/_modals.scss */ +.modal em { + font-weight: bold; + font-style: normal; } +/* line 138, ../sass/_modals.scss */ +.modal .popover { + z-index: 2000; } + /* line 140, ../sass/_modals.scss */ + .modal .popover dl { + color: black; + font-weight: normal; } + /* line 143, ../sass/_modals.scss */ + .modal .popover dl dt { + width: 90px; } + /* line 146, ../sass/_modals.scss */ + .modal .popover dl dd { + margin-left: 110px; } + /* line 150, ../sass/_modals.scss */ + .modal .popover h2 { + font-size: 16px; + color: #303030; + font-weight: bold; + text-align: center; } +/* line 157, ../sass/_modals.scss */ +.modal .popover-content { + min-width: 150px; } + +/* line 163, ../sass/_modals.scss */ +.modal-content { + padding: 20px; + color: #303030; } + /* line 166, ../sass/_modals.scss */ + .modal-content .badge { + background-color: transparent; } + +/* line 173, ../sass/_modals.scss */ +.instructions-icon { + color: #3c96e0; + font-size: 22px; + margin-left: 78px; } + /* line 177, ../sass/_modals.scss */ + .instructions-icon:hover { + text-decoration: none; } + +/* line 182, ../sass/_modals.scss */ +.extra-info { + margin-top: 10px; } + +/* line 186, ../sass/_modals.scss */ +.error-sign { + color: red; + font-size: 20px; + margin-left: 10px; + position: absolute; + top: 6px; + display: none; } + /* line 195, ../sass/_modals.scss */ + .error-sign:hover, .error-sign:focus { + color: red; + text-decoration: none; } + +/* line 202, ../sass/_modals.scss */ +.form-area { + position: relative; } + +/* line 205, ../sass/_modals.scss */ +.form-subject { + margin-bottom: 15px; } + +/* line 209, ../sass/_modals.scss */ +.toggle-more { + margin-top: -16px; + display: none; } + +/* line 216, ../sass/_modals.scss */ +.modal .table-selected th, .modal .table-selected td { + word-break: break-word; } +/* line 220, ../sass/_modals.scss */ +.modal .table-selected td:last-child .wrap { + padding-right: 36px; } +/* line 224, ../sass/_modals.scss */ +.modal .table-selected tr:nth-child(2n) { + background: #f2f2f2; } +/* line 228, ../sass/_modals.scss */ +.modal .table-selected tr a { + font-weight: bold; } +/* line 233, ../sass/_modals.scss */ +.modal .table-selected tr:hover, +.modal .table-selected tr:focus { + background: #d9d9d9; } + /* line 235, ../sass/_modals.scss */ + .modal .table-selected tr:hover a, + .modal .table-selected tr:focus a { + color: red; } +/* line 240, ../sass/_modals.scss */ +.modal .table-selected .remove { + position: absolute; + right: 14px; + color: transparent; } + /* line 244, ../sass/_modals.scss */ + .modal .table-selected .remove:hover { + cursor: pointer; + text-decoration: none; } + +/* line 1, ../sass/_tables.scss */ +table thead th { + white-space: nowrap; } + +/* line 6, ../sass/_tables.scss */ +table td, +table th { + vertical-align: top; } + +/* line 10, ../sass/_tables.scss */ +table .wrap { + position: relative; } + +/* line 15, ../sass/_tables.scss */ +.table-items .snf-search { + opacity: 0.7; + font-size: 15px; } + /* line 19, ../sass/_tables.scss */ + .table-items .snf-search:hover, .table-items .snf-search:focus { + opacity: 1; } +/* line 23, ../sass/_tables.scss */ +.table-items .login-method { + padding: 2px 16px 2px 0px; + text-align: center; } +/* line 28, ../sass/_tables.scss */ +.table-items th .badge { + margin: 0 2px 0 4px; + display: inline; + padding-top: 2px; } +/* line 33, ../sass/_tables.scss */ +.table-items td { + padding: 8px 6px 0 6px; } + +/* line 41, ../sass/_tables.scss */ +.table-selected-main:not(.table-selected) td:last-child, +.table-items:not(.table-selected) td:last-child { + max-width: 60px; + min-width: 60px; + padding: 8px 5px; } + /* line 45, ../sass/_tables.scss */ + .table-selected-main:not(.table-selected) td:last-child .details-link:hover, + .table-items:not(.table-selected) td:last-child .details-link:hover { + text-decoration: none; } + /* line 48, ../sass/_tables.scss */ + .table-selected-main:not(.table-selected) td:last-child .summary-expand, + .table-items:not(.table-selected) td:last-child .summary-expand { + position: relative; + z-index: 10; + float: right; + padding-left: 8px; + padding-right: 8px; + background-color: #005b9a; + color: #fff; } + /* line 57, ../sass/_tables.scss */ + .table-selected-main:not(.table-selected) td:last-child .summary-expand:hover, .table-selected-main:not(.table-selected) td:last-child .summary-expand:focus, + .table-items:not(.table-selected) td:last-child .summary-expand:hover, + .table-items:not(.table-selected) td:last-child .summary-expand:focus { + text-decoration: none; + background-color: #ee5161; } + /* line 62, ../sass/_tables.scss */ + .table-selected-main:not(.table-selected) td:last-child dl, + .table-items:not(.table-selected) td:last-child dl { + z-index: 0; + position: relative; + padding: 8px; + display: none; + margin: 0; } + +/* line 72, ../sass/_tables.scss */ +.table-items .headerSortUp span.caret { + border-top: 0; + border-bottom: 4px solid; } + +/* line 79, ../sass/_tables.scss */ +#table-items-selected_filter label, +#table-items-total_filter label { + color: #222222; } +/* line 82, ../sass/_tables.scss */ +#table-items-selected_filter input, +#table-items-total_filter input { + color: #222222; + background: #ececec; + border: 1px solid #cccccc; + padding: 3px 5px; } + /* line 87, ../sass/_tables.scss */ + #table-items-selected_filter input:focus, + #table-items-total_filter input:focus { + outline: 0 none; } + +/* line 93, ../sass/_tables.scss */ +#table-items-selected_wrapper { + padding: 10px; + border: 1px solid #e0e0e0; + margin-bottom: 20px; + display: none; } + +/* line 103, ../sass/_tables.scss */ +div.dataTables_length { + padding-left: 2em; + padding-top: 0.55em; } + /* line 106, ../sass/_tables.scss */ + div.dataTables_length select { + width: 55px; + display: inline-block; + margin-left: 4px; + vertical-align: baseline; + color: #222; } + +/* line 115, ../sass/_tables.scss */ +table.dataTable tbody tr { + background-color: inherit; } + /* line 117, ../sass/_tables.scss */ + table.dataTable tbody tr.even { + background-color: #ececec; } + +/* line 123, ../sass/_tables.scss */ +table.dataTable thead th, +table.dataTable thead td { + border-bottom: 1px solid white; + border-top: 1px solid #e0e0e0; } + +/* line 127, ../sass/_tables.scss */ +table.dataTable tbody tr:hover { + background-color: #e0e0e0; } + +/* line 130, ../sass/_tables.scss */ +table.dataTable tbody tr.selected { + color: #222222; + background-color: #cccccc; } + +/* line 136, ../sass/_tables.scss */ +html body .dataTables_wrapper label { + font-weight: normal; } +/* line 140, ../sass/_tables.scss */ +html body .dataTables_wrapper table th.sorting, html body .dataTables_wrapper table th.sorting_asc, html body .dataTables_wrapper table th.sorting_desc { + background-position: center left; + padding-left: 22px; } + +/* line 150, ../sass/_tables.scss */ +.dataTables_wrapper .dataTables_length, +.dataTables_wrapper .dataTables_info, +.dataTables_wrapper .dataTables_paginate { + padding-top: 0; + margin-bottom: 0.5em; + color: #222222; + line-height: 35px; } + +/* line 156, ../sass/_tables.scss */ +table.dataTable.no-footer { + border-bottom: 1px solid #eeeeee; + margin: 2em 0; } + +/* line 161, ../sass/_tables.scss */ +.dataTables_wrapper .dataTables_paginate .paginate_button { + color: #222222 !important; + padding: 0 1em; } + +/* line 168, ../sass/_tables.scss */ +.container .dataTables_wrapper .dataTables_paginate .paginate_button:hover, +.container .dataTables_wrapper .dataTables_paginate .paginate_button:focus { + background: transparent; + border-color: #222222; + color: #222222 !important; } + +/* line 174, ../sass/_tables.scss */ +.container .dataTables_wrapper .dataTables_paginate .paginate_button.disabled { + border-color: transparent; + color: #818181 !important; } + /* line 179, ../sass/_tables.scss */ + .container .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover, .container .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:focus, .container .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active { + color: #818181 !important; } + +/* line 186, ../sass/_tables.scss */ +.dataTables_wrapper .dataTables_paginate .paginate_button.current, +.dataTables_wrapper .dataTables_paginate .paginate_button.current:focus, +.dataTables_wrapper .dataTables_paginate .paginate_button.current:hover { + background: #cccccc; + color: #222222 !important; + border: transparent; } + +/* line 192, ../sass/_tables.scss */ +.dataTables_wrapper > .custom-buttons { + margin-bottom: 1em; + width: 100%; } + +/* line 197, ../sass/_tables.scss */ +.dataTables_wrapper .dataTables_processing { + background: #ffa914; + color: white; + padding: 5px 10px; + -webkit-box-shadow: inset 0 0 5px #888888; + box-shadow: inset 0 0 5px #888888; + z-index: 1; } + +/* line 205, ../sass/_tables.scss */ +.fixed { + position: fixed; } + +/* line 38, ../sass/_settings.scss */ +.ip_log tr td:nth-child(2), .ip_log tr th:nth-child(2) { + word-break: break-word; + max-width: 250px; } +/* line 38, ../sass/_settings.scss */ +.ip_log tr td:nth-child(3), .ip_log tr th:nth-child(3) { + word-break: break-word; + max-width: 150px; } +/* line 38, ../sass/_settings.scss */ +.ip_log tr td:nth-child(4), .ip_log tr th:nth-child(4) { + word-break: break-word; + max-width: 150px; } + +/* Layout & general stuff */ +/* line 4, ../sass/_extra.scss */ +html, body { + height: 100%; } + +/* line 8, ../sass/_extra.scss */ +body { + padding-top: 100px; } + +/* line 12, ../sass/_extra.scss */ +.wrapper { + padding-bottom: 50px; } + +/* +.container-solid{ + min-width: 1050px!important; +} +*/ +/* line 20, ../sass/_extra.scss */ +.container:not(.container-solid) { + max-width: 960px; } + +/* line 24, ../sass/_extra.scss */ +h1, h2, h3, h4 { + word-wrap: break-word; } + +/* line 28, ../sass/_extra.scss */ +.info { + overflow: auto; } + +/* line 33, ../sass/_extra.scss */ +.dl-horizontal dd, dt, +.tooltip-inner { + word-wrap: break-word; } + +/* line 36, ../sass/_extra.scss */ +.disabled { + cursor: default !important; } + +/* Home */ +/* line 42, ../sass/_extra.scss */ +.app-list { + position: relative; + text-align: center; + padding-top: 100px; } + /* line 46, ../sass/_extra.scss */ + .app-list a { + width: 210px; + font-size: 24px; + margin: 0 20px; + display: inline-block; + text-align: center; + opacity: 1; + border: 1px solid #222222; + color: #222222; + opacity: 1; } + /* line 29, ../sass/_bars-btns.scss */ + .app-list a span { + display: inline-block; + height: 100%; + line-height: 100%; + padding: 12px 10px; } + /* line 36, ../sass/_bars-btns.scss */ + .app-list a:hover, .app-list a:focus { + text-decoration: none; + opacity: 0.85; } + /* line 45, ../sass/_bars-btns.scss */ + .app-list a .snf-font-remove { + display: inline; } + /* line 48, ../sass/_bars-btns.scss */ + .app-list a.disabled { + background: transparent !important; + border-color: #818181 !important; + color: #818181 !important; } + /* line 15, ../sass/_bars-btns.scss */ + .app-list a.disabled:hover, .app-list a.disabled:focus { + cursor: default; + opacity: 1; } + /* line 18, ../sass/_bars-btns.scss */ + .app-list a.disabled:hover span, .app-list a.disabled:focus span { + color: #818181 !important; } + /* line 59, ../sass/_bars-btns.scss */ + .app-list a span { + border: 1px solid transparent; + width: 100%; } + /* line 65, ../sass/_bars-btns.scss */ + .app-list a:hover span, .app-list a:focus span { + border-color: #222222; } + /* line 69, ../sass/_bars-btns.scss */ + .app-list a.disabled { + @inlcude disabled; + color: #818181; } + /* line 74, ../sass/_bars-btns.scss */ + .app-list a.disabled:hover span, .app-list a.disabled:focus span { + border-color: transparent; } + /* line 52, ../sass/_extra.scss */ + .app-list a.disabled { + border-color: #a7a7a7; + color: gray; } + /* line 57, ../sass/_extra.scss */ + .app-list a.disabled:hover span, .app-list a.disabled:focus span { + border-color: transparent; } + +/* line 65, ../sass/_extra.scss */ +.nav-simple { + padding: 20px; + border-bottom: 1px solid #222222; } + /* line 68, ../sass/_extra.scss */ + .nav-simple .header { + float: left; + line-height: 40px; + font-size: 26px; } + /* line 72, ../sass/_extra.scss */ + .nav-simple .header img { + max-height: 50px; } + /* line 76, ../sass/_extra.scss */ + .nav-simple .login-info { + float: right; + position: relative; + line-height: 40px; + font-size: 16px; } + /* line 81, ../sass/_extra.scss */ + .nav-simple .login-info .has-dropdown { + display: inline; + position: relative; } + /* line 86, ../sass/_extra.scss */ + .nav-simple .login-info .has-dropdown:hover > a, .nav-simple .login-info .has-dropdown:focus > a { + background: #fefefe; } + /* line 90, ../sass/_extra.scss */ + .nav-simple .login-info .has-dropdown > a { + color: #222222; + display: inline-block; + padding: 0 10px; } + /* line 96, ../sass/_extra.scss */ + .nav-simple .login-info .dropdown-menu { + left: auto; + right: 0; + top: 27px; } + +/* Navigation */ +/* line 106, ../sass/_extra.scss */ +.navbar-default { + border: 0 none; + border-bottom: 1px solid #e0e0e0; + z-index: 1040; + margin: 0 auto; } + /* line 111, ../sass/_extra.scss */ + .navbar-default .container-fluid { + padding: 0; } + /* line 114, ../sass/_extra.scss */ + .navbar-default .home-icon { + padding: 0; + height: 50px; + width: 50px; + text-align: center; + line-height: 50px; + font-size: 2px; + background: #00a551; } + /* line 122, ../sass/_extra.scss */ + .navbar-default .home-icon img { + max-height: 50px; } + +/* line 129, ../sass/_extra.scss */ +.sub-nav { + top: 50px; + min-height: inherit; } + /* line 133, ../sass/_extra.scss */ + .sub-nav .nav > li > a { + padding-top: 8px; + padding-bottom: 8px; } + @media (max-width: 768px) { + /* line 129, ../sass/_extra.scss */ + .sub-nav { + display: none; } } + +/* line 142, ../sass/_extra.scss */ +.dropdown-menu { + overflow-y: auto; } + +/* line 147, ../sass/_extra.scss */ +.nav .has-dropdown:hover > ul.dropdown-menu, +.nav-simple .has-dropdown:hover > ul.dropdown-menu { + display: block; } + +/* More */ +/* line 157, ../sass/_extra.scss */ +svg > text:last-child { + display: none; } + +/* line 161, ../sass/_extra.scss */ +.has-dropdown .arrow { + margin-left: 6px; + vertical-align: middle; } + +/* line 166, ../sass/_extra.scss */ +.hidden-row { + display: none; } + +/* line 170, ../sass/_extra.scss */ +.with-shift *::selection { + background-color: transparent; } + +/* line 174, ../sass/_extra.scss */ +.with-shift *::-moz-selection { + background: transparent; } + +/* line 177, ../sass/_extra.scss */ +.tab-content { + background: #d9d9d9; + color: #222222; + padding: 20px; + border: 0 none; } + /* line 182, ../sass/_extra.scss */ + .tab-content .well { + margin-bottom: 0; } + +/* line 187, ../sass/_extra.scss */ +.selection-indicator { + cursor: pointer; + padding: 6px 12px 6px 6px; } + +/* Notification area */ +/* line 194, ../sass/_extra.scss */ +.notify { + padding: 30px 10px 15px; + width: 100%; + position: fixed; + bottom: 0; + background: #444444; + color: white; } + /* line 202, ../sass/_extra.scss */ + .notify .container > *:not(:last-child) { + margin-bottom: 16px; } + /* line 205, ../sass/_extra.scss */ + .notify .remove-icon { + color: transparent; + margin-left: 20px; + font-weight: bold; } + /* line 211, ../sass/_extra.scss */ + .notify .container > *:hover .remove-icon { + color: #d9534f; } + /* line 215, ../sass/_extra.scss */ + .notify .state-icon { + margin-right: 10px; } + /* line 218, ../sass/_extra.scss */ + .notify .success { + color: #449d44; } + /* line 221, ../sass/_extra.scss */ + .notify .error { + color: #d9534f; } + /* line 224, ../sass/_extra.scss */ + .notify .pending { + color: #f0ad4e; } + /* line 227, ../sass/_extra.scss */ + .notify .warning, .notify .no-notifications { + font-style: italic; + font-weight: bold; + display: inline-block; + text-align: right; } + /* line 232, ../sass/_extra.scss */ + .notify .warning > .wrap, .notify .no-notifications > .wrap { + display: block; + padding-right: 4px; } + /* line 236, ../sass/_extra.scss */ + .notify .warning a:hover, .notify .no-notifications a:hover { + cursor: pointer; } + /* line 240, ../sass/_extra.scss */ + .notify .close-notify { + position: absolute; + right: 20px; + top: 20px; + color: white; } + /* line 246, ../sass/_extra.scss */ + .notify .close-notify:hover, .notify .close-notify:focus { + color: inherit; } + /* line 250, ../sass/_extra.scss */ + .notify .dl-horizontal { + margin-left: 21px; } + /* line 252, ../sass/_extra.scss */ + .notify .dl-horizontal dt { + width: 80px; + vertical-align: top; + text-align: left; } + /* line 256, ../sass/_extra.scss */ + .notify .dl-horizontal dt span { + font-size: 20px; + vertical-align: text-bottom; + margin-right: 10px; } + /* line 262, ../sass/_extra.scss */ + .notify .dl-horizontal dd { + margin-left: 80px; } + +/* line 268, ../sass/_extra.scss */ +.lowercase { + text-transform: lowercase; } + +/* line 273, ../sass/_extra.scss */ +.shortcuts-btn .book-icon { + padding-right: 2px; + vertical-align: sub; + font-size: 17px; } + +/* line 281, ../sass/_extra.scss */ +body .shortcuts dt { + width: 119px; + margin-bottom: 12px; } +/* line 285, ../sass/_extra.scss */ +body .shortcuts dd { + margin-left: 139px; } +/* line 288, ../sass/_extra.scss */ +body .shortcuts .key { + padding: 2px 9px; + font-style: normal; + font-weight: bold; + border: 1px solid #dddddd; + background: whitesmoke; + border-radius: 6px; } + +/* line 298, ../sass/_extra.scss */ +.filters-examples dt { + font-weight: normal; + margin-bottom: 0; } +/* line 302, ../sass/_extra.scss */ +.filters-examples dd { + margin-bottom: 12px; } + /* line 304, ../sass/_extra.scss */ + .filters-examples dd .highlight { + background: whitesmoke; + padding: 2px 6px; + border-bottom: 1px solid #dddddd; } + /* line 309, ../sass/_extra.scss */ + .filters-examples dd.divider { + margin-bottom: 8px; + border-bottom: 1px solid #dddddd; } + +/* line 317, ../sass/_extra.scss */ +.notes dt { + width: 50px; } +/* line 320, ../sass/_extra.scss */ +.notes dd { + margin-left: 60px; } + +/* line 325, ../sass/_extra.scss */ +.popover { + z-index: 1999; + max-width: none; + color: #222222; + margin-bottom: 20px; } + /* line 330, ../sass/_extra.scss */ + .popover h2 { + text-align: center; + font-size: 1.3em; + font-weight: bold; + margin-top: 0; } + /* line 336, ../sass/_extra.scss */ + .popover h3 { + font-size: 1.2em; + font-weight: bold; } + /* line 340, ../sass/_extra.scss */ + .popover h4 { + font-size: 1.1em; + font-weight: bold; } + /* line 344, ../sass/_extra.scss */ + .popover dt { + margin-bottom: 8px; + overflow: visible; } + /* line 348, ../sass/_extra.scss */ + .popover .panel-default { + border-color: transparent; + box-shadow: none; } + +/* line 354, ../sass/_extra.scss */ +.sign-out { + text-align: right; } + /* line 356, ../sass/_extra.scss */ + .sign-out span { + margin-right: 10px; + vertical-align: middle; + font-size: 18px; } + +/* line 364, ../sass/_extra.scss */ +.stats section { + margin-bottom: 3em; } + /* line 366, ../sass/_extra.scss */ + .stats section h3 { + margin-bottom: 1em; } + /* line 368, ../sass/_extra.scss */ + .stats section h3 span { + margin-right: 0.5em; } + /* line 372, ../sass/_extra.scss */ + .stats section .custom-btn { + float: left; + margin-right: 32px; } + /* line 374, ../sass/_extra.scss */ + .stats section .custom-btn span { + padding-left: 0; } + /* line 377, ../sass/_extra.scss */ + .stats section .custom-btn .snf-download-full { + padding-right: 0; + padding-left: 8px; } + /* line 383, ../sass/_extra.scss */ + .stats section .spinner { + display: none; + float: left; + padding: 8px; } + +/* line 392, ../sass/_extra.scss */ +.navbar-right .dropdown-menu, .login-info .dropdown-menu { + min-width: 0; } + +@media (min-width: 1200px) { + /* line 397, ../sass/_extra.scss */ + .stick { + position: fixed; + top: 100px; + width: inherit; } } + +/* line 406, ../sass/_extra.scss */ +.themes { + position: fixed; + left: 10px; + bottom: 10px; } + +/* line 413, ../sass/_extra.scss */ +.charts .info { + overflow: hidden; } +/* line 416, ../sass/_extra.scss */ +.charts h3 { + text-align: center; + margin-bottom: 1em; } +/* line 420, ../sass/_extra.scss */ +.charts .c3-axis { + fill: #222222; } +/* line 423, ../sass/_extra.scss */ +.charts .c3 path, .charts .c3 line { + stroke: #222222; } +/* line 426, ../sass/_extra.scss */ +.charts .c3-legend-item text { + fill: #222222; } +/* line 429, ../sass/_extra.scss */ +.charts .c3-tooltip { + color: #222; } + +/* line 433, ../sass/_extra.scss */ +.popover-content { + max-width: 800px; } diff --git a/snf-admin-app/synnefo_admin/admin/static/css/main.css b/snf-admin-app/synnefo_admin/admin/static/css/main.css new file mode 100644 index 0000000000000000000000000000000000000000..ef940973e6211cd46f589eda1ff691da291af42e --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/css/main.css @@ -0,0 +1,8478 @@ +/*! normalize.css v3.0.0 | MIT License | git.io/normalize */ +/* line 9, ../sass/bootstrap/_normalize.scss */ +html { + font-family: sans-serif; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; } + +/* line 19, ../sass/bootstrap/_normalize.scss */ +body { + margin: 0; } + +/* line 41, ../sass/bootstrap/_normalize.scss */ +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +nav, +section, +summary { + display: block; } + +/* line 53, ../sass/bootstrap/_normalize.scss */ +audio, +canvas, +progress, +video { + display: inline-block; + vertical-align: baseline; } + +/* line 63, ../sass/bootstrap/_normalize.scss */ +audio:not([controls]) { + display: none; + height: 0; } + +/* line 74, ../sass/bootstrap/_normalize.scss */ +[hidden], +template { + display: none; } + +/* line 85, ../sass/bootstrap/_normalize.scss */ +a { + background: transparent; } + +/* line 94, ../sass/bootstrap/_normalize.scss */ +a:active, +a:hover { + outline: 0; } + +/* line 105, ../sass/bootstrap/_normalize.scss */ +abbr[title] { + border-bottom: 1px dotted; } + +/* line 114, ../sass/bootstrap/_normalize.scss */ +b, +strong { + font-weight: bold; } + +/* line 122, ../sass/bootstrap/_normalize.scss */ +dfn { + font-style: italic; } + +/* line 131, ../sass/bootstrap/_normalize.scss */ +h1 { + font-size: 2em; + margin: 0.67em 0; } + +/* line 140, ../sass/bootstrap/_normalize.scss */ +mark { + background: #ff0; + color: #000; } + +/* line 149, ../sass/bootstrap/_normalize.scss */ +small { + font-size: 80%; } + +/* line 158, ../sass/bootstrap/_normalize.scss */ +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; } + +/* line 165, ../sass/bootstrap/_normalize.scss */ +sup { + top: -0.5em; } + +/* line 169, ../sass/bootstrap/_normalize.scss */ +sub { + bottom: -0.25em; } + +/* line 180, ../sass/bootstrap/_normalize.scss */ +img { + border: 0; } + +/* line 188, ../sass/bootstrap/_normalize.scss */ +svg:not(:root) { + overflow: hidden; } + +/* line 199, ../sass/bootstrap/_normalize.scss */ +figure { + margin: 1em 40px; } + +/* line 207, ../sass/bootstrap/_normalize.scss */ +hr { + -moz-box-sizing: content-box; + box-sizing: content-box; + height: 0; } + +/* line 217, ../sass/bootstrap/_normalize.scss */ +pre { + overflow: auto; } + +/* line 228, ../sass/bootstrap/_normalize.scss */ +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; } + +/* line 252, ../sass/bootstrap/_normalize.scss */ +button, +input, +optgroup, +select, +textarea { + color: inherit; + font: inherit; + margin: 0; } + +/* line 262, ../sass/bootstrap/_normalize.scss */ +button { + overflow: visible; } + +/* line 274, ../sass/bootstrap/_normalize.scss */ +button, +select { + text-transform: none; } + +/* line 289, ../sass/bootstrap/_normalize.scss */ +button, +html input[type="button"], +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; + cursor: pointer; } + +/* line 299, ../sass/bootstrap/_normalize.scss */ +button[disabled], +html input[disabled] { + cursor: default; } + +/* line 308, ../sass/bootstrap/_normalize.scss */ +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; } + +/* line 318, ../sass/bootstrap/_normalize.scss */ +input { + line-height: normal; } + +/* line 331, ../sass/bootstrap/_normalize.scss */ +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; + padding: 0; } + +/* line 343, ../sass/bootstrap/_normalize.scss */ +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; } + +/* line 353, ../sass/bootstrap/_normalize.scss */ +input[type="search"] { + -webkit-appearance: textfield; + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; + box-sizing: content-box; } + +/* line 367, ../sass/bootstrap/_normalize.scss */ +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; } + +/* line 375, ../sass/bootstrap/_normalize.scss */ +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; } + +/* line 386, ../sass/bootstrap/_normalize.scss */ +legend { + border: 0; + padding: 0; } + +/* line 395, ../sass/bootstrap/_normalize.scss */ +textarea { + overflow: auto; } + +/* line 404, ../sass/bootstrap/_normalize.scss */ +optgroup { + font-weight: bold; } + +/* line 415, ../sass/bootstrap/_normalize.scss */ +table { + border-collapse: collapse; + border-spacing: 0; } + +/* line 421, ../sass/bootstrap/_normalize.scss */ +td, +th { + padding: 0; } + +@media print { + /* line 8, ../sass/bootstrap/_print.scss */ + * { + text-shadow: none !important; + color: #000 !important; + background: transparent !important; + box-shadow: none !important; } + + /* line 16, ../sass/bootstrap/_print.scss */ + a, + a:visited { + text-decoration: underline; } + + /* line 20, ../sass/bootstrap/_print.scss */ + a[href]:after { + content: " (" attr(href) ")"; } + + /* line 24, ../sass/bootstrap/_print.scss */ + abbr[title]:after { + content: " (" attr(title) ")"; } + + /* line 30, ../sass/bootstrap/_print.scss */ + a[href^="javascript:"]:after, + a[href^="#"]:after { + content: ""; } + + /* line 35, ../sass/bootstrap/_print.scss */ + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; } + + /* line 40, ../sass/bootstrap/_print.scss */ + thead { + display: table-header-group; } + + /* line 45, ../sass/bootstrap/_print.scss */ + tr, + img { + page-break-inside: avoid; } + + /* line 49, ../sass/bootstrap/_print.scss */ + img { + max-width: 100% !important; } + + /* line 55, ../sass/bootstrap/_print.scss */ + p, + h2, + h3 { + orphans: 3; + widows: 3; } + + /* line 61, ../sass/bootstrap/_print.scss */ + h2, + h3 { + page-break-after: avoid; } + + /* line 67, ../sass/bootstrap/_print.scss */ + select { + background: #fff !important; } + + /* line 72, ../sass/bootstrap/_print.scss */ + .navbar { + display: none; } + + /* line 77, ../sass/bootstrap/_print.scss */ + .table td, + .table th { + background-color: #fff !important; } + + /* line 83, ../sass/bootstrap/_print.scss */ + .btn > .caret, + .dropup > .btn > .caret { + border-top-color: #000 !important; } + + /* line 87, ../sass/bootstrap/_print.scss */ + .label { + border: 1px solid #000; } + + /* line 91, ../sass/bootstrap/_print.scss */ + .table { + border-collapse: collapse !important; } + + /* line 96, ../sass/bootstrap/_print.scss */ + .table-bordered th, + .table-bordered td { + border: 1px solid #ddd !important; } } +/* line 11, ../sass/bootstrap/_scaffolding.scss */ +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } + +/* line 15, ../sass/bootstrap/_scaffolding.scss */ +*:before, +*:after { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } + +/* line 22, ../sass/bootstrap/_scaffolding.scss */ +html { + font-size: 62.5%; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } + +/* line 27, ../sass/bootstrap/_scaffolding.scss */ +body { + font-family: "Open Sans", sans-serif; + font-size: 14px; + line-height: 1.42857; + color: white; + background-color: #303030; } + +/* line 39, ../sass/bootstrap/_scaffolding.scss */ +input, +button, +select, +textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; + color: white; } + +/* line 49, ../sass/bootstrap/_scaffolding.scss */ +a { + color: #4d99d8; + text-decoration: none; } + /* line 54, ../sass/bootstrap/_scaffolding.scss */ + a:hover, a:focus { + color: #83b8e4; } + /* line 58, ../sass/bootstrap/_scaffolding.scss */ + a:focus { + outline: 0 none; } + +/* line 69, ../sass/bootstrap/_scaffolding.scss */ +figure { + margin: 0; } + +/* line 76, ../sass/bootstrap/_scaffolding.scss */ +img { + vertical-align: middle; } + +/* line 81, ../sass/bootstrap/_scaffolding.scss */ +.img-responsive { + display: block; + max-width: 100%; + height: auto; } + +/* line 86, ../sass/bootstrap/_scaffolding.scss */ +.img-rounded { + border-radius: 6px; } + +/* line 93, ../sass/bootstrap/_scaffolding.scss */ +.img-thumbnail { + padding: 4px; + line-height: 1.42857; + background-color: #303030; + border: 1px solid #dddddd; + border-radius: 0; + -webkit-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; + display: inline-block; + max-width: 100%; + height: auto; } + +/* line 106, ../sass/bootstrap/_scaffolding.scss */ +.img-circle { + border-radius: 50%; } + +/* line 113, ../sass/bootstrap/_scaffolding.scss */ +hr { + margin-top: 20px; + margin-bottom: 20px; + border: 0; + border-top: 1px solid #d9d9d9; } + +/* line 125, ../sass/bootstrap/_scaffolding.scss */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; } + +/* line 10, ../sass/bootstrap/_type.scss */ +h1, h2, h3, h4, h5, h6, +.h1, .h2, .h3, .h4, .h5, .h6 { + font-family: inherit; + font-weight: 500; + line-height: 1.1; + color: inherit; } + /* line 17, ../sass/bootstrap/_type.scss */ + h1 small, + h1 .small, h2 small, + h2 .small, h3 small, + h3 .small, h4 small, + h4 .small, h5 small, + h5 .small, h6 small, + h6 .small, + .h1 small, + .h1 .small, .h2 small, + .h2 .small, .h3 small, + .h3 .small, .h4 small, + .h4 .small, .h5 small, + .h5 .small, .h6 small, + .h6 .small { + font-weight: normal; + line-height: 1; + color: #4e4e4e; } + +/* line 26, ../sass/bootstrap/_type.scss */ +h1, .h1, +h2, .h2, +h3, .h3 { + margin-top: 20px; + margin-bottom: 10px; } + /* line 31, ../sass/bootstrap/_type.scss */ + h1 small, + h1 .small, .h1 small, + .h1 .small, + h2 small, + h2 .small, .h2 small, + .h2 .small, + h3 small, + h3 .small, .h3 small, + .h3 .small { + font-size: 65%; } + +/* line 37, ../sass/bootstrap/_type.scss */ +h4, .h4, +h5, .h5, +h6, .h6 { + margin-top: 10px; + margin-bottom: 10px; } + /* line 42, ../sass/bootstrap/_type.scss */ + h4 small, + h4 .small, .h4 small, + .h4 .small, + h5 small, + h5 .small, .h5 small, + .h5 .small, + h6 small, + h6 .small, .h6 small, + .h6 .small { + font-size: 75%; } + +/* line 47, ../sass/bootstrap/_type.scss */ +h1, .h1 { + font-size: 36px; } + +/* line 48, ../sass/bootstrap/_type.scss */ +h2, .h2 { + font-size: 30px; } + +/* line 49, ../sass/bootstrap/_type.scss */ +h3, .h3 { + font-size: 24px; } + +/* line 50, ../sass/bootstrap/_type.scss */ +h4, .h4 { + font-size: 18px; } + +/* line 51, ../sass/bootstrap/_type.scss */ +h5, .h5 { + font-size: 14px; } + +/* line 52, ../sass/bootstrap/_type.scss */ +h6, .h6 { + font-size: 12px; } + +/* line 58, ../sass/bootstrap/_type.scss */ +p { + margin: 0 0 10px; } + +/* line 62, ../sass/bootstrap/_type.scss */ +.lead { + margin-bottom: 20px; + font-size: 16px; + font-weight: 200; + line-height: 1.4; } + @media (min-width: 768px) { + /* line 62, ../sass/bootstrap/_type.scss */ + .lead { + font-size: 21px; } } + +/* line 79, ../sass/bootstrap/_type.scss */ +small, +.small { + font-size: 85%; } + +/* line 82, ../sass/bootstrap/_type.scss */ +cite { + font-style: normal; } + +/* line 85, ../sass/bootstrap/_type.scss */ +.text-left { + text-align: left; } + +/* line 86, ../sass/bootstrap/_type.scss */ +.text-right { + text-align: right; } + +/* line 87, ../sass/bootstrap/_type.scss */ +.text-center { + text-align: center; } + +/* line 88, ../sass/bootstrap/_type.scss */ +.text-justify { + text-align: justify; } + +/* line 91, ../sass/bootstrap/_type.scss */ +.text-muted { + color: #4e4e4e; } + +/* line 606, ../sass/bootstrap/_mixins.scss */ +.text-primary { + color: white; } + +/* line 609, ../sass/bootstrap/_mixins.scss */ +a.text-primary:hover { + color: #e6e6e6; } + +/* line 606, ../sass/bootstrap/_mixins.scss */ +.text-success { + color: #3c763d; } + +/* line 609, ../sass/bootstrap/_mixins.scss */ +a.text-success:hover { + color: #2b542c; } + +/* line 606, ../sass/bootstrap/_mixins.scss */ +.text-info { + color: #31708f; } + +/* line 609, ../sass/bootstrap/_mixins.scss */ +a.text-info:hover { + color: #245269; } + +/* line 606, ../sass/bootstrap/_mixins.scss */ +.text-warning { + color: #8a6d3b; } + +/* line 609, ../sass/bootstrap/_mixins.scss */ +a.text-warning:hover { + color: #66512c; } + +/* line 606, ../sass/bootstrap/_mixins.scss */ +.text-danger { + color: #a94442; } + +/* line 609, ../sass/bootstrap/_mixins.scss */ +a.text-danger:hover { + color: #843534; } + +/* line 108, ../sass/bootstrap/_type.scss */ +.bg-primary { + color: #fff; } + +/* line 594, ../sass/bootstrap/_mixins.scss */ +.bg-primary { + background-color: white; } + +/* line 597, ../sass/bootstrap/_mixins.scss */ +a.bg-primary:hover { + background-color: #e6e6e6; } + +/* line 594, ../sass/bootstrap/_mixins.scss */ +.bg-success { + background-color: #dff0d8; } + +/* line 597, ../sass/bootstrap/_mixins.scss */ +a.bg-success:hover { + background-color: #c1e2b3; } + +/* line 594, ../sass/bootstrap/_mixins.scss */ +.bg-info { + background-color: #d9edf7; } + +/* line 597, ../sass/bootstrap/_mixins.scss */ +a.bg-info:hover { + background-color: #afd9ee; } + +/* line 594, ../sass/bootstrap/_mixins.scss */ +.bg-warning { + background-color: #fcf8e3; } + +/* line 597, ../sass/bootstrap/_mixins.scss */ +a.bg-warning:hover { + background-color: #f7ecb5; } + +/* line 594, ../sass/bootstrap/_mixins.scss */ +.bg-danger { + background-color: #f2dede; } + +/* line 597, ../sass/bootstrap/_mixins.scss */ +a.bg-danger:hover { + background-color: #e4b9b9; } + +/* line 127, ../sass/bootstrap/_type.scss */ +.page-header { + padding-bottom: 9px; + margin: 40px 0 20px; + border-bottom: 1px solid #d9d9d9; } + +/* line 139, ../sass/bootstrap/_type.scss */ +ul, +ol { + margin-top: 0; + margin-bottom: 10px; } + /* line 143, ../sass/bootstrap/_type.scss */ + ul ul, + ul ol, + ol ul, + ol ol { + margin-bottom: 0; } + +/* line 151, ../sass/bootstrap/_type.scss */ +.list-unstyled, .list-inline { + padding-left: 0; + list-style: none; } + +/* line 157, ../sass/bootstrap/_type.scss */ +.list-inline { + margin-left: -5px; } + /* line 161, ../sass/bootstrap/_type.scss */ + .list-inline > li { + display: inline-block; + padding-left: 5px; + padding-right: 5px; } + +/* line 169, ../sass/bootstrap/_type.scss */ +dl { + margin-top: 0; + margin-bottom: 0; } + +/* line 174, ../sass/bootstrap/_type.scss */ +dt, +dd { + line-height: 1.42857; } + +/* line 177, ../sass/bootstrap/_type.scss */ +dt { + font-weight: bold; } + +/* line 180, ../sass/bootstrap/_type.scss */ +dd { + margin-left: 0; } + +@media (min-width: 768px) { + /* line 191, ../sass/bootstrap/_type.scss */ + .dl-horizontal dt { + float: left; + width: 160px; + clear: left; + text-align: right; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } + /* line 198, ../sass/bootstrap/_type.scss */ + .dl-horizontal dd { + margin-left: 180px; } + /* line 21, ../sass/bootstrap/_mixins.scss */ + .dl-horizontal dd:before, .dl-horizontal dd:after { + content: " "; + display: table; } + /* line 25, ../sass/bootstrap/_mixins.scss */ + .dl-horizontal dd:after { + clear: both; } } +/* line 211, ../sass/bootstrap/_type.scss */ +abbr[title], +abbr[data-original-title] { + cursor: help; + border-bottom: 1px dotted #4e4e4e; } + +/* line 215, ../sass/bootstrap/_type.scss */ +.initialism { + font-size: 90%; + text-transform: uppercase; } + +/* line 221, ../sass/bootstrap/_type.scss */ +blockquote { + padding: 10px 20px; + margin: 0 0 20px; + font-size: 17.5px; + border-left: 5px solid #d9d9d9; } + /* line 230, ../sass/bootstrap/_type.scss */ + blockquote p:last-child, + blockquote ul:last-child, + blockquote ol:last-child { + margin-bottom: 0; } + /* line 239, ../sass/bootstrap/_type.scss */ + blockquote footer, + blockquote small, + blockquote .small { + display: block; + font-size: 80%; + line-height: 1.42857; + color: #4e4e4e; } + /* line 245, ../sass/bootstrap/_type.scss */ + blockquote footer:before, + blockquote small:before, + blockquote .small:before { + content: '\2014 \00A0'; } + +/* line 255, ../sass/bootstrap/_type.scss */ +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + border-right: 5px solid #d9d9d9; + border-left: 0; + text-align: right; } + /* line 266, ../sass/bootstrap/_type.scss */ + .blockquote-reverse footer:before, + .blockquote-reverse small:before, + .blockquote-reverse .small:before, + blockquote.pull-right footer:before, + blockquote.pull-right small:before, + blockquote.pull-right .small:before { + content: ''; } + /* line 267, ../sass/bootstrap/_type.scss */ + .blockquote-reverse footer:after, + .blockquote-reverse small:after, + .blockquote-reverse .small:after, + blockquote.pull-right footer:after, + blockquote.pull-right small:after, + blockquote.pull-right .small:after { + content: '\00A0 \2014'; } + +/* line 275, ../sass/bootstrap/_type.scss */ +blockquote:before, +blockquote:after { + content: ""; } + +/* line 280, ../sass/bootstrap/_type.scss */ +address { + margin-bottom: 20px; + font-style: normal; + line-height: 1.42857; } + +/* line 10, ../sass/bootstrap/_code.scss */ +code, +kbd, +pre, +samp { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; } + +/* line 15, ../sass/bootstrap/_code.scss */ +code { + padding: 2px 4px; + font-size: 90%; + color: #c7254e; + background-color: #f9f2f4; + white-space: nowrap; + border-radius: 0; } + +/* line 25, ../sass/bootstrap/_code.scss */ +kbd { + padding: 2px 4px; + font-size: 90%; + color: white; + background-color: #333333; + border-radius: 3px; + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); } + +/* line 35, ../sass/bootstrap/_code.scss */ +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 1.42857; + word-break: break-all; + word-wrap: break-word; + color: #303030; + background-color: whitesmoke; + border: 1px solid #cccccc; + border-radius: 0; } + /* line 49, ../sass/bootstrap/_code.scss */ + pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; } + +/* line 60, ../sass/bootstrap/_code.scss */ +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; } + +/* line 10, ../sass/bootstrap/_grid.scss */ +.container { + margin-right: auto; + margin-left: auto; + padding-left: 15px; + padding-right: 15px; } + /* line 21, ../sass/bootstrap/_mixins.scss */ + .container:before, .container:after { + content: " "; + display: table; } + /* line 25, ../sass/bootstrap/_mixins.scss */ + .container:after { + clear: both; } + @media (min-width: 768px) { + /* line 10, ../sass/bootstrap/_grid.scss */ + .container { + width: 810px; } } + @media (min-width: 992px) { + /* line 10, ../sass/bootstrap/_grid.scss */ + .container { + width: 1010px; } } + @media (min-width: 1200px) { + /* line 10, ../sass/bootstrap/_grid.scss */ + .container { + width: 1170px; } } + +/* line 30, ../sass/bootstrap/_grid.scss */ +.container-fluid { + margin-right: auto; + margin-left: auto; + padding-left: 15px; + padding-right: 15px; } + /* line 21, ../sass/bootstrap/_mixins.scss */ + .container-fluid:before, .container-fluid:after { + content: " "; + display: table; } + /* line 25, ../sass/bootstrap/_mixins.scss */ + .container-fluid:after { + clear: both; } + +/* line 39, ../sass/bootstrap/_grid.scss */ +.row { + margin-left: -15px; + margin-right: -15px; } + /* line 21, ../sass/bootstrap/_mixins.scss */ + .row:before, .row:after { + content: " "; + display: table; } + /* line 25, ../sass/bootstrap/_mixins.scss */ + .row:after { + clear: both; } + +/* line 799, ../sass/bootstrap/_mixins.scss */ +.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 { + position: relative; + min-height: 1px; + padding-left: 15px; + padding-right: 15px; } + +/* line 818, ../sass/bootstrap/_mixins.scss */ +.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 { + float: left; } + +/* line 826, ../sass/bootstrap/_mixins.scss */ +.col-xs-1 { + width: 8.33333%; } + +/* line 826, ../sass/bootstrap/_mixins.scss */ +.col-xs-2 { + width: 16.66667%; } + +/* line 826, ../sass/bootstrap/_mixins.scss */ +.col-xs-3 { + width: 25%; } + +/* line 826, ../sass/bootstrap/_mixins.scss */ +.col-xs-4 { + width: 33.33333%; } + +/* line 826, ../sass/bootstrap/_mixins.scss */ +.col-xs-5 { + width: 41.66667%; } + +/* line 826, ../sass/bootstrap/_mixins.scss */ +.col-xs-6 { + width: 50%; } + +/* line 826, ../sass/bootstrap/_mixins.scss */ +.col-xs-7 { + width: 58.33333%; } + +/* line 826, ../sass/bootstrap/_mixins.scss */ +.col-xs-8 { + width: 66.66667%; } + +/* line 826, ../sass/bootstrap/_mixins.scss */ +.col-xs-9 { + width: 75%; } + +/* line 826, ../sass/bootstrap/_mixins.scss */ +.col-xs-10 { + width: 83.33333%; } + +/* line 826, ../sass/bootstrap/_mixins.scss */ +.col-xs-11 { + width: 91.66667%; } + +/* line 826, ../sass/bootstrap/_mixins.scss */ +.col-xs-12 { + width: 100%; } + +/* line 836, ../sass/bootstrap/_mixins.scss */ +.col-xs-pull-0 { + right: 0%; } + +/* line 836, ../sass/bootstrap/_mixins.scss */ +.col-xs-pull-1 { + right: 8.33333%; } + +/* line 836, ../sass/bootstrap/_mixins.scss */ +.col-xs-pull-2 { + right: 16.66667%; } + +/* line 836, ../sass/bootstrap/_mixins.scss */ +.col-xs-pull-3 { + right: 25%; } + +/* line 836, ../sass/bootstrap/_mixins.scss */ +.col-xs-pull-4 { + right: 33.33333%; } + +/* line 836, ../sass/bootstrap/_mixins.scss */ +.col-xs-pull-5 { + right: 41.66667%; } + +/* line 836, ../sass/bootstrap/_mixins.scss */ +.col-xs-pull-6 { + right: 50%; } + +/* line 836, ../sass/bootstrap/_mixins.scss */ +.col-xs-pull-7 { + right: 58.33333%; } + +/* line 836, ../sass/bootstrap/_mixins.scss */ +.col-xs-pull-8 { + right: 66.66667%; } + +/* line 836, ../sass/bootstrap/_mixins.scss */ +.col-xs-pull-9 { + right: 75%; } + +/* line 836, ../sass/bootstrap/_mixins.scss */ +.col-xs-pull-10 { + right: 83.33333%; } + +/* line 836, ../sass/bootstrap/_mixins.scss */ +.col-xs-pull-11 { + right: 91.66667%; } + +/* line 836, ../sass/bootstrap/_mixins.scss */ +.col-xs-pull-12 { + right: 100%; } + +/* line 831, ../sass/bootstrap/_mixins.scss */ +.col-xs-push-0 { + left: 0%; } + +/* line 831, ../sass/bootstrap/_mixins.scss */ +.col-xs-push-1 { + left: 8.33333%; } + +/* line 831, ../sass/bootstrap/_mixins.scss */ +.col-xs-push-2 { + left: 16.66667%; } + +/* line 831, ../sass/bootstrap/_mixins.scss */ +.col-xs-push-3 { + left: 25%; } + +/* line 831, ../sass/bootstrap/_mixins.scss */ +.col-xs-push-4 { + left: 33.33333%; } + +/* line 831, ../sass/bootstrap/_mixins.scss */ +.col-xs-push-5 { + left: 41.66667%; } + +/* line 831, ../sass/bootstrap/_mixins.scss */ +.col-xs-push-6 { + left: 50%; } + +/* line 831, ../sass/bootstrap/_mixins.scss */ +.col-xs-push-7 { + left: 58.33333%; } + +/* line 831, ../sass/bootstrap/_mixins.scss */ +.col-xs-push-8 { + left: 66.66667%; } + +/* line 831, ../sass/bootstrap/_mixins.scss */ +.col-xs-push-9 { + left: 75%; } + +/* line 831, ../sass/bootstrap/_mixins.scss */ +.col-xs-push-10 { + left: 83.33333%; } + +/* line 831, ../sass/bootstrap/_mixins.scss */ +.col-xs-push-11 { + left: 91.66667%; } + +/* line 831, ../sass/bootstrap/_mixins.scss */ +.col-xs-push-12 { + left: 100%; } + +/* line 841, ../sass/bootstrap/_mixins.scss */ +.col-xs-offset-0 { + margin-left: 0%; } + +/* line 841, ../sass/bootstrap/_mixins.scss */ +.col-xs-offset-1 { + margin-left: 8.33333%; } + +/* line 841, ../sass/bootstrap/_mixins.scss */ +.col-xs-offset-2 { + margin-left: 16.66667%; } + +/* line 841, ../sass/bootstrap/_mixins.scss */ +.col-xs-offset-3 { + margin-left: 25%; } + +/* line 841, ../sass/bootstrap/_mixins.scss */ +.col-xs-offset-4 { + margin-left: 33.33333%; } + +/* line 841, ../sass/bootstrap/_mixins.scss */ +.col-xs-offset-5 { + margin-left: 41.66667%; } + +/* line 841, ../sass/bootstrap/_mixins.scss */ +.col-xs-offset-6 { + margin-left: 50%; } + +/* line 841, ../sass/bootstrap/_mixins.scss */ +.col-xs-offset-7 { + margin-left: 58.33333%; } + +/* line 841, ../sass/bootstrap/_mixins.scss */ +.col-xs-offset-8 { + margin-left: 66.66667%; } + +/* line 841, ../sass/bootstrap/_mixins.scss */ +.col-xs-offset-9 { + margin-left: 75%; } + +/* line 841, ../sass/bootstrap/_mixins.scss */ +.col-xs-offset-10 { + margin-left: 83.33333%; } + +/* line 841, ../sass/bootstrap/_mixins.scss */ +.col-xs-offset-11 { + margin-left: 91.66667%; } + +/* line 841, ../sass/bootstrap/_mixins.scss */ +.col-xs-offset-12 { + margin-left: 100%; } + +@media (min-width: 768px) { + /* line 818, ../sass/bootstrap/_mixins.scss */ + .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 { + float: left; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-sm-1 { + width: 8.33333%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-sm-2 { + width: 16.66667%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-sm-3 { + width: 25%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-sm-4 { + width: 33.33333%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-sm-5 { + width: 41.66667%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-sm-6 { + width: 50%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-sm-7 { + width: 58.33333%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-sm-8 { + width: 66.66667%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-sm-9 { + width: 75%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-sm-10 { + width: 83.33333%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-sm-11 { + width: 91.66667%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-sm-12 { + width: 100%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-sm-pull-0 { + right: 0%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-sm-pull-1 { + right: 8.33333%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-sm-pull-2 { + right: 16.66667%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-sm-pull-3 { + right: 25%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-sm-pull-4 { + right: 33.33333%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-sm-pull-5 { + right: 41.66667%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-sm-pull-6 { + right: 50%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-sm-pull-7 { + right: 58.33333%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-sm-pull-8 { + right: 66.66667%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-sm-pull-9 { + right: 75%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-sm-pull-10 { + right: 83.33333%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-sm-pull-11 { + right: 91.66667%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-sm-pull-12 { + right: 100%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-sm-push-0 { + left: 0%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-sm-push-1 { + left: 8.33333%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-sm-push-2 { + left: 16.66667%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-sm-push-3 { + left: 25%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-sm-push-4 { + left: 33.33333%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-sm-push-5 { + left: 41.66667%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-sm-push-6 { + left: 50%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-sm-push-7 { + left: 58.33333%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-sm-push-8 { + left: 66.66667%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-sm-push-9 { + left: 75%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-sm-push-10 { + left: 83.33333%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-sm-push-11 { + left: 91.66667%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-sm-push-12 { + left: 100%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-sm-offset-0 { + margin-left: 0%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-sm-offset-1 { + margin-left: 8.33333%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-sm-offset-2 { + margin-left: 16.66667%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-sm-offset-3 { + margin-left: 25%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-sm-offset-4 { + margin-left: 33.33333%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-sm-offset-5 { + margin-left: 41.66667%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-sm-offset-6 { + margin-left: 50%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-sm-offset-7 { + margin-left: 58.33333%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-sm-offset-8 { + margin-left: 66.66667%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-sm-offset-9 { + margin-left: 75%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-sm-offset-10 { + margin-left: 83.33333%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-sm-offset-11 { + margin-left: 91.66667%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-sm-offset-12 { + margin-left: 100%; } } +@media (min-width: 992px) { + /* line 818, ../sass/bootstrap/_mixins.scss */ + .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 { + float: left; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-md-1 { + width: 8.33333%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-md-2 { + width: 16.66667%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-md-3 { + width: 25%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-md-4 { + width: 33.33333%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-md-5 { + width: 41.66667%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-md-6 { + width: 50%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-md-7 { + width: 58.33333%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-md-8 { + width: 66.66667%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-md-9 { + width: 75%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-md-10 { + width: 83.33333%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-md-11 { + width: 91.66667%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-md-12 { + width: 100%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-md-pull-0 { + right: 0%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-md-pull-1 { + right: 8.33333%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-md-pull-2 { + right: 16.66667%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-md-pull-3 { + right: 25%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-md-pull-4 { + right: 33.33333%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-md-pull-5 { + right: 41.66667%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-md-pull-6 { + right: 50%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-md-pull-7 { + right: 58.33333%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-md-pull-8 { + right: 66.66667%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-md-pull-9 { + right: 75%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-md-pull-10 { + right: 83.33333%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-md-pull-11 { + right: 91.66667%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-md-pull-12 { + right: 100%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-md-push-0 { + left: 0%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-md-push-1 { + left: 8.33333%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-md-push-2 { + left: 16.66667%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-md-push-3 { + left: 25%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-md-push-4 { + left: 33.33333%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-md-push-5 { + left: 41.66667%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-md-push-6 { + left: 50%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-md-push-7 { + left: 58.33333%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-md-push-8 { + left: 66.66667%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-md-push-9 { + left: 75%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-md-push-10 { + left: 83.33333%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-md-push-11 { + left: 91.66667%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-md-push-12 { + left: 100%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-md-offset-0 { + margin-left: 0%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-md-offset-1 { + margin-left: 8.33333%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-md-offset-2 { + margin-left: 16.66667%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-md-offset-3 { + margin-left: 25%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-md-offset-4 { + margin-left: 33.33333%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-md-offset-5 { + margin-left: 41.66667%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-md-offset-6 { + margin-left: 50%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-md-offset-7 { + margin-left: 58.33333%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-md-offset-8 { + margin-left: 66.66667%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-md-offset-9 { + margin-left: 75%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-md-offset-10 { + margin-left: 83.33333%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-md-offset-11 { + margin-left: 91.66667%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-md-offset-12 { + margin-left: 100%; } } +@media (min-width: 1200px) { + /* line 818, ../sass/bootstrap/_mixins.scss */ + .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 { + float: left; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-lg-1 { + width: 8.33333%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-lg-2 { + width: 16.66667%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-lg-3 { + width: 25%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-lg-4 { + width: 33.33333%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-lg-5 { + width: 41.66667%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-lg-6 { + width: 50%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-lg-7 { + width: 58.33333%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-lg-8 { + width: 66.66667%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-lg-9 { + width: 75%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-lg-10 { + width: 83.33333%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-lg-11 { + width: 91.66667%; } + + /* line 826, ../sass/bootstrap/_mixins.scss */ + .col-lg-12 { + width: 100%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-lg-pull-0 { + right: 0%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-lg-pull-1 { + right: 8.33333%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-lg-pull-2 { + right: 16.66667%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-lg-pull-3 { + right: 25%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-lg-pull-4 { + right: 33.33333%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-lg-pull-5 { + right: 41.66667%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-lg-pull-6 { + right: 50%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-lg-pull-7 { + right: 58.33333%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-lg-pull-8 { + right: 66.66667%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-lg-pull-9 { + right: 75%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-lg-pull-10 { + right: 83.33333%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-lg-pull-11 { + right: 91.66667%; } + + /* line 836, ../sass/bootstrap/_mixins.scss */ + .col-lg-pull-12 { + right: 100%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-lg-push-0 { + left: 0%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-lg-push-1 { + left: 8.33333%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-lg-push-2 { + left: 16.66667%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-lg-push-3 { + left: 25%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-lg-push-4 { + left: 33.33333%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-lg-push-5 { + left: 41.66667%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-lg-push-6 { + left: 50%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-lg-push-7 { + left: 58.33333%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-lg-push-8 { + left: 66.66667%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-lg-push-9 { + left: 75%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-lg-push-10 { + left: 83.33333%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-lg-push-11 { + left: 91.66667%; } + + /* line 831, ../sass/bootstrap/_mixins.scss */ + .col-lg-push-12 { + left: 100%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-lg-offset-0 { + margin-left: 0%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-lg-offset-1 { + margin-left: 8.33333%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-lg-offset-2 { + margin-left: 16.66667%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-lg-offset-3 { + margin-left: 25%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-lg-offset-4 { + margin-left: 33.33333%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-lg-offset-5 { + margin-left: 41.66667%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-lg-offset-6 { + margin-left: 50%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-lg-offset-7 { + margin-left: 58.33333%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-lg-offset-8 { + margin-left: 66.66667%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-lg-offset-9 { + margin-left: 75%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-lg-offset-10 { + margin-left: 83.33333%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-lg-offset-11 { + margin-left: 91.66667%; } + + /* line 841, ../sass/bootstrap/_mixins.scss */ + .col-lg-offset-12 { + margin-left: 100%; } } +/* line 6, ../sass/bootstrap/_tables.scss */ +table { + max-width: 100%; + background-color: transparent; } + +/* line 10, ../sass/bootstrap/_tables.scss */ +th { + text-align: left; } + +/* line 17, ../sass/bootstrap/_tables.scss */ +.table { + width: 100%; + margin-bottom: 20px; } + /* line 26, ../sass/bootstrap/_tables.scss */ + .table > thead > tr > th, + .table > thead > tr > td, + .table > tbody > tr > th, + .table > tbody > tr > td, + .table > tfoot > tr > th, + .table > tfoot > tr > td { + padding: 10px; + line-height: 1.42857; + vertical-align: top; + border-top: 1px solid #dddddd; } + /* line 35, ../sass/bootstrap/_tables.scss */ + .table > thead > tr > th { + vertical-align: bottom; + border-bottom: 2px solid #dddddd; } + /* line 45, ../sass/bootstrap/_tables.scss */ + .table > caption + thead > tr:first-child > th, + .table > caption + thead > tr:first-child > td, + .table > colgroup + thead > tr:first-child > th, + .table > colgroup + thead > tr:first-child > td, + .table > thead:first-child > tr:first-child > th, + .table > thead:first-child > tr:first-child > td { + border-top: 0; } + /* line 51, ../sass/bootstrap/_tables.scss */ + .table > tbody + tbody { + border-top: 2px solid #dddddd; } + /* line 56, ../sass/bootstrap/_tables.scss */ + .table .table { + background-color: #303030; } + +/* line 70, ../sass/bootstrap/_tables.scss */ +.table-condensed > thead > tr > th, +.table-condensed > thead > tr > td, +.table-condensed > tbody > tr > th, +.table-condensed > tbody > tr > td, +.table-condensed > tfoot > tr > th, +.table-condensed > tfoot > tr > td { + padding: 5px; } + +/* line 82, ../sass/bootstrap/_tables.scss */ +.table-bordered { + border: 1px solid #dddddd; } + /* line 89, ../sass/bootstrap/_tables.scss */ + .table-bordered > thead > tr > th, + .table-bordered > thead > tr > td, + .table-bordered > tbody > tr > th, + .table-bordered > tbody > tr > td, + .table-bordered > tfoot > tr > th, + .table-bordered > tfoot > tr > td { + border: 1px solid #dddddd; } + /* line 96, ../sass/bootstrap/_tables.scss */ + .table-bordered > thead > tr > th, + .table-bordered > thead > tr > td { + border-bottom-width: 2px; } + +/* line 110, ../sass/bootstrap/_tables.scss */ +.table-striped > tbody > tr:nth-child(odd) > td, +.table-striped > tbody > tr:nth-child(odd) > th { + background-color: #f9f9f9; } + +/* line 124, ../sass/bootstrap/_tables.scss */ +.table-hover > tbody > tr:hover > td, +.table-hover > tbody > tr:hover > th { + background-color: whitesmoke; } + +/* line 135, ../sass/bootstrap/_tables.scss */ +table col[class*="col-"] { + position: static; + float: none; + display: table-column; } + +/* line 143, ../sass/bootstrap/_tables.scss */ +table td[class*="col-"], +table th[class*="col-"] { + position: static; + float: none; + display: table-cell; } + +/* line 449, ../sass/bootstrap/_mixins.scss */ +.table > thead > tr > td.active, +.table > thead > tr > th.active, .table > thead > tr.active > td, .table > thead > tr.active > th, +.table > tbody > tr > td.active, +.table > tbody > tr > th.active, +.table > tbody > tr.active > td, +.table > tbody > tr.active > th, +.table > tfoot > tr > td.active, +.table > tfoot > tr > th.active, +.table > tfoot > tr.active > td, +.table > tfoot > tr.active > th { + background-color: whitesmoke; } + +/* line 460, ../sass/bootstrap/_mixins.scss */ +.table-hover > tbody > tr > td.active:hover, +.table-hover > tbody > tr > th.active:hover, .table-hover > tbody > tr.active:hover > td, .table-hover > tbody > tr.active:hover > th { + background-color: #e8e8e8; } + +/* line 449, ../sass/bootstrap/_mixins.scss */ +.table > thead > tr > td.success, +.table > thead > tr > th.success, .table > thead > tr.success > td, .table > thead > tr.success > th, +.table > tbody > tr > td.success, +.table > tbody > tr > th.success, +.table > tbody > tr.success > td, +.table > tbody > tr.success > th, +.table > tfoot > tr > td.success, +.table > tfoot > tr > th.success, +.table > tfoot > tr.success > td, +.table > tfoot > tr.success > th { + background-color: #dff0d8; } + +/* line 460, ../sass/bootstrap/_mixins.scss */ +.table-hover > tbody > tr > td.success:hover, +.table-hover > tbody > tr > th.success:hover, .table-hover > tbody > tr.success:hover > td, .table-hover > tbody > tr.success:hover > th { + background-color: #d0e9c6; } + +/* line 449, ../sass/bootstrap/_mixins.scss */ +.table > thead > tr > td.info, +.table > thead > tr > th.info, .table > thead > tr.info > td, .table > thead > tr.info > th, +.table > tbody > tr > td.info, +.table > tbody > tr > th.info, +.table > tbody > tr.info > td, +.table > tbody > tr.info > th, +.table > tfoot > tr > td.info, +.table > tfoot > tr > th.info, +.table > tfoot > tr.info > td, +.table > tfoot > tr.info > th { + background-color: #d9edf7; } + +/* line 460, ../sass/bootstrap/_mixins.scss */ +.table-hover > tbody > tr > td.info:hover, +.table-hover > tbody > tr > th.info:hover, .table-hover > tbody > tr.info:hover > td, .table-hover > tbody > tr.info:hover > th { + background-color: #c4e3f3; } + +/* line 449, ../sass/bootstrap/_mixins.scss */ +.table > thead > tr > td.warning, +.table > thead > tr > th.warning, .table > thead > tr.warning > td, .table > thead > tr.warning > th, +.table > tbody > tr > td.warning, +.table > tbody > tr > th.warning, +.table > tbody > tr.warning > td, +.table > tbody > tr.warning > th, +.table > tfoot > tr > td.warning, +.table > tfoot > tr > th.warning, +.table > tfoot > tr.warning > td, +.table > tfoot > tr.warning > th { + background-color: #fcf8e3; } + +/* line 460, ../sass/bootstrap/_mixins.scss */ +.table-hover > tbody > tr > td.warning:hover, +.table-hover > tbody > tr > th.warning:hover, .table-hover > tbody > tr.warning:hover > td, .table-hover > tbody > tr.warning:hover > th { + background-color: #faf2cc; } + +/* line 449, ../sass/bootstrap/_mixins.scss */ +.table > thead > tr > td.danger, +.table > thead > tr > th.danger, .table > thead > tr.danger > td, .table > thead > tr.danger > th, +.table > tbody > tr > td.danger, +.table > tbody > tr > th.danger, +.table > tbody > tr.danger > td, +.table > tbody > tr.danger > th, +.table > tfoot > tr > td.danger, +.table > tfoot > tr > th.danger, +.table > tfoot > tr.danger > td, +.table > tfoot > tr.danger > th { + background-color: #f2dede; } + +/* line 460, ../sass/bootstrap/_mixins.scss */ +.table-hover > tbody > tr > td.danger:hover, +.table-hover > tbody > tr > th.danger:hover, .table-hover > tbody > tr.danger:hover > td, .table-hover > tbody > tr.danger:hover > th { + background-color: #ebcccc; } + +@media (max-width: 767px) { + /* line 172, ../sass/bootstrap/_tables.scss */ + .table-responsive { + width: 100%; + margin-bottom: 15px; + overflow-y: hidden; + overflow-x: scroll; + -ms-overflow-style: -ms-autohiding-scrollbar; + border: 1px solid #dddddd; + -webkit-overflow-scrolling: touch; } + /* line 182, ../sass/bootstrap/_tables.scss */ + .table-responsive > .table { + margin-bottom: 0; } + /* line 191, ../sass/bootstrap/_tables.scss */ + .table-responsive > .table > thead > tr > th, + .table-responsive > .table > thead > tr > td, + .table-responsive > .table > tbody > tr > th, + .table-responsive > .table > tbody > tr > td, + .table-responsive > .table > tfoot > tr > th, + .table-responsive > .table > tfoot > tr > td { + white-space: nowrap; } + /* line 199, ../sass/bootstrap/_tables.scss */ + .table-responsive > .table-bordered { + border: 0; } + /* line 208, ../sass/bootstrap/_tables.scss */ + .table-responsive > .table-bordered > thead > tr > th:first-child, + .table-responsive > .table-bordered > thead > tr > td:first-child, + .table-responsive > .table-bordered > tbody > tr > th:first-child, + .table-responsive > .table-bordered > tbody > tr > td:first-child, + .table-responsive > .table-bordered > tfoot > tr > th:first-child, + .table-responsive > .table-bordered > tfoot > tr > td:first-child { + border-left: 0; } + /* line 212, ../sass/bootstrap/_tables.scss */ + .table-responsive > .table-bordered > thead > tr > th:last-child, + .table-responsive > .table-bordered > thead > tr > td:last-child, + .table-responsive > .table-bordered > tbody > tr > th:last-child, + .table-responsive > .table-bordered > tbody > tr > td:last-child, + .table-responsive > .table-bordered > tfoot > tr > th:last-child, + .table-responsive > .table-bordered > tfoot > tr > td:last-child { + border-right: 0; } + /* line 225, ../sass/bootstrap/_tables.scss */ + .table-responsive > .table-bordered > tbody > tr:last-child > th, + .table-responsive > .table-bordered > tbody > tr:last-child > td, + .table-responsive > .table-bordered > tfoot > tr:last-child > th, + .table-responsive > .table-bordered > tfoot > tr:last-child > td { + border-bottom: 0; } } +/* line 10, ../sass/bootstrap/_forms.scss */ +fieldset { + padding: 0; + margin: 0; + border: 0; + min-width: 0; } + +/* line 20, ../sass/bootstrap/_forms.scss */ +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: 20px; + font-size: 21px; + line-height: inherit; + color: #303030; + border: 0; + border-bottom: 1px solid #e5e5e5; } + +/* line 32, ../sass/bootstrap/_forms.scss */ +label { + display: inline-block; + margin-bottom: 5px; + font-weight: bold; } + +/* line 46, ../sass/bootstrap/_forms.scss */ +input[type="search"] { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } + +/* line 52, ../sass/bootstrap/_forms.scss */ +input[type="radio"], +input[type="checkbox"] { + margin: 4px 0 0; + margin-top: 1px \9; + /* IE8-9 */ + line-height: normal; } + +/* line 59, ../sass/bootstrap/_forms.scss */ +input[type="file"] { + display: block; } + +/* line 64, ../sass/bootstrap/_forms.scss */ +input[type="range"] { + display: block; + width: 100%; } + +/* line 71, ../sass/bootstrap/_forms.scss */ +select[multiple], +select[size] { + height: auto; } + +/* line 78, ../sass/bootstrap/_forms.scss */ +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { + outline: 0 none; } + +/* line 83, ../sass/bootstrap/_forms.scss */ +output { + display: block; + padding-top: 7px; + font-size: 14px; + line-height: 1.42857; + color: #555555; } + +/* line 114, ../sass/bootstrap/_forms.scss */ +.form-control { + display: block; + width: 100%; + height: 34px; + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857; + color: #555555; + background-color: white; + background-image: none; + border: 1px solid #cccccc; + border-radius: 0; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -webkit-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; + transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; } + /* line 916, ../sass/bootstrap/_mixins.scss */ + .form-control:focus { + border-color: #66afe9; + outline: 0; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); } + /* line 57, ../sass/bootstrap/_mixins.scss */ + .form-control::-moz-placeholder { + color: #4e4e4e; + opacity: 1; } + /* line 59, ../sass/bootstrap/_mixins.scss */ + .form-control:-ms-input-placeholder { + color: #4e4e4e; } + /* line 60, ../sass/bootstrap/_mixins.scss */ + .form-control::-webkit-input-placeholder { + color: #4e4e4e; } + /* line 142, ../sass/bootstrap/_forms.scss */ + .form-control[disabled], .form-control[readonly], fieldset[disabled] .form-control { + cursor: not-allowed; + background-color: #d9d9d9; + opacity: 1; } + +/* line 152, ../sass/bootstrap/_forms.scss */ +textarea.form-control { + height: auto; } + +/* line 164, ../sass/bootstrap/_forms.scss */ +input[type="search"] { + -webkit-appearance: none; } + +/* line 174, ../sass/bootstrap/_forms.scss */ +input[type="date"] { + line-height: 34px; } + +/* line 184, ../sass/bootstrap/_forms.scss */ +.form-group { + margin-bottom: 15px; } + +/* line 194, ../sass/bootstrap/_forms.scss */ +.radio, +.checkbox { + display: block; + min-height: 20px; + margin-top: 10px; + margin-bottom: 10px; + padding-left: 20px; } + /* line 200, ../sass/bootstrap/_forms.scss */ + .radio label, + .checkbox label { + display: inline; + font-weight: normal; + cursor: pointer; } + +/* line 209, ../sass/bootstrap/_forms.scss */ +.radio input[type="radio"], +.radio-inline input[type="radio"], +.checkbox input[type="checkbox"], +.checkbox-inline input[type="checkbox"] { + float: left; + margin-left: -20px; } + +/* line 214, ../sass/bootstrap/_forms.scss */ +.radio + .radio, +.checkbox + .checkbox { + margin-top: -5px; } + +/* line 220, ../sass/bootstrap/_forms.scss */ +.radio-inline, +.checkbox-inline { + display: inline-block; + padding-left: 20px; + margin-bottom: 0; + vertical-align: middle; + font-weight: normal; + cursor: pointer; } + +/* line 229, ../sass/bootstrap/_forms.scss */ +.radio-inline + .radio-inline, +.checkbox-inline + .checkbox-inline { + margin-top: 0; + margin-left: 10px; } + +/* line 244, ../sass/bootstrap/_forms.scss */ +input[type="radio"][disabled], fieldset[disabled] input[type="radio"], +input[type="checkbox"][disabled], fieldset[disabled] +input[type="checkbox"], +.radio[disabled], fieldset[disabled] +.radio, +.radio-inline[disabled], fieldset[disabled] +.radio-inline, +.checkbox[disabled], fieldset[disabled] +.checkbox, +.checkbox-inline[disabled], fieldset[disabled] +.checkbox-inline { + cursor: not-allowed; } + +/* line 931, ../sass/bootstrap/_mixins.scss */ +.input-sm, .input-group-sm > .form-control, +.input-group-sm > .input-group-addon, +.input-group-sm > .input-group-btn > .btn { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; } + +/* line 939, ../sass/bootstrap/_mixins.scss */ +select.input-sm, .input-group-sm > select.form-control, +.input-group-sm > select.input-group-addon, +.input-group-sm > .input-group-btn > select.btn { + height: 30px; + line-height: 30px; } + +/* line 945, ../sass/bootstrap/_mixins.scss */ +textarea.input-sm, .input-group-sm > textarea.form-control, +.input-group-sm > textarea.input-group-addon, +.input-group-sm > .input-group-btn > textarea.btn, +select[multiple].input-sm, +.input-group-sm > select[multiple].form-control, +.input-group-sm > select[multiple].input-group-addon, +.input-group-sm > .input-group-btn > select[multiple].btn { + height: auto; } + +/* line 931, ../sass/bootstrap/_mixins.scss */ +.input-lg, .input-group-lg > .form-control, +.input-group-lg > .input-group-addon, +.input-group-lg > .input-group-btn > .btn { + height: 46px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.33; + border-radius: 6px; } + +/* line 939, ../sass/bootstrap/_mixins.scss */ +select.input-lg, .input-group-lg > select.form-control, +.input-group-lg > select.input-group-addon, +.input-group-lg > .input-group-btn > select.btn { + height: 46px; + line-height: 46px; } + +/* line 945, ../sass/bootstrap/_mixins.scss */ +textarea.input-lg, .input-group-lg > textarea.form-control, +.input-group-lg > textarea.input-group-addon, +.input-group-lg > .input-group-btn > textarea.btn, +select[multiple].input-lg, +.input-group-lg > select[multiple].form-control, +.input-group-lg > select[multiple].input-group-addon, +.input-group-lg > .input-group-btn > select[multiple].btn { + height: auto; } + +/* line 264, ../sass/bootstrap/_forms.scss */ +.has-feedback { + position: relative; } + /* line 269, ../sass/bootstrap/_forms.scss */ + .has-feedback .form-control { + padding-right: 42.5px; } + /* line 274, ../sass/bootstrap/_forms.scss */ + .has-feedback .form-control-feedback { + position: absolute; + top: 25px; + right: 0; + display: block; + width: 34px; + height: 34px; + line-height: 34px; + text-align: center; } + +/* line 876, ../sass/bootstrap/_mixins.scss */ +.has-success .help-block, +.has-success .control-label, +.has-success .radio, +.has-success .checkbox, +.has-success .radio-inline, +.has-success .checkbox-inline { + color: #3c763d; } +/* line 880, ../sass/bootstrap/_mixins.scss */ +.has-success .form-control { + border-color: #3c763d; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); } + /* line 883, ../sass/bootstrap/_mixins.scss */ + .has-success .form-control:focus { + border-color: #2b542c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168; } +/* line 890, ../sass/bootstrap/_mixins.scss */ +.has-success .input-group-addon { + color: #3c763d; + border-color: #3c763d; + background-color: #dff0d8; } +/* line 896, ../sass/bootstrap/_mixins.scss */ +.has-success .form-control-feedback { + color: #3c763d; } + +/* line 876, ../sass/bootstrap/_mixins.scss */ +.has-warning .help-block, +.has-warning .control-label, +.has-warning .radio, +.has-warning .checkbox, +.has-warning .radio-inline, +.has-warning .checkbox-inline { + color: #8a6d3b; } +/* line 880, ../sass/bootstrap/_mixins.scss */ +.has-warning .form-control { + border-color: #8a6d3b; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); } + /* line 883, ../sass/bootstrap/_mixins.scss */ + .has-warning .form-control:focus { + border-color: #66512c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b; } +/* line 890, ../sass/bootstrap/_mixins.scss */ +.has-warning .input-group-addon { + color: #8a6d3b; + border-color: #8a6d3b; + background-color: #fcf8e3; } +/* line 896, ../sass/bootstrap/_mixins.scss */ +.has-warning .form-control-feedback { + color: #8a6d3b; } + +/* line 876, ../sass/bootstrap/_mixins.scss */ +.has-error .help-block, +.has-error .control-label, +.has-error .radio, +.has-error .checkbox, +.has-error .radio-inline, +.has-error .checkbox-inline { + color: #a94442; } +/* line 880, ../sass/bootstrap/_mixins.scss */ +.has-error .form-control { + border-color: #a94442; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); } + /* line 883, ../sass/bootstrap/_mixins.scss */ + .has-error .form-control:focus { + border-color: #843534; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483; } +/* line 890, ../sass/bootstrap/_mixins.scss */ +.has-error .input-group-addon { + color: #a94442; + border-color: #a94442; + background-color: #f2dede; } +/* line 896, ../sass/bootstrap/_mixins.scss */ +.has-error .form-control-feedback { + color: #a94442; } + +/* line 303, ../sass/bootstrap/_forms.scss */ +.form-control-static { + margin-bottom: 0; } + +/* line 313, ../sass/bootstrap/_forms.scss */ +.help-block { + display: block; + margin-top: 5px; + margin-bottom: 10px; + color: white; } + +@media (min-width: 768px) { + /* line 338, ../sass/bootstrap/_forms.scss */ + .form-inline .form-group, .navbar-form .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; } + /* line 345, ../sass/bootstrap/_forms.scss */ + .form-inline .form-control, .navbar-form .form-control { + display: inline-block; + width: auto; + vertical-align: middle; } + /* line 351, ../sass/bootstrap/_forms.scss */ + .form-inline .input-group > .form-control, .navbar-form .input-group > .form-control { + width: 100%; } + /* line 355, ../sass/bootstrap/_forms.scss */ + .form-inline .control-label, .navbar-form .control-label { + margin-bottom: 0; + vertical-align: middle; } + /* line 364, ../sass/bootstrap/_forms.scss */ + .form-inline .radio, .navbar-form .radio, + .form-inline .checkbox, + .navbar-form .checkbox { + display: inline-block; + margin-top: 0; + margin-bottom: 0; + padding-left: 0; + vertical-align: middle; } + /* line 372, ../sass/bootstrap/_forms.scss */ + .form-inline .radio input[type="radio"], .navbar-form .radio input[type="radio"], + .form-inline .checkbox input[type="checkbox"], + .navbar-form .checkbox input[type="checkbox"] { + float: none; + margin-left: 0; } + /* line 381, ../sass/bootstrap/_forms.scss */ + .form-inline .has-feedback .form-control-feedback, .navbar-form .has-feedback .form-control-feedback { + top: 0; } } + +/* line 400, ../sass/bootstrap/_forms.scss */ +.form-horizontal .control-label, +.form-horizontal .radio, +.form-horizontal .checkbox, +.form-horizontal .radio-inline, +.form-horizontal .checkbox-inline { + margin-top: 0; + margin-bottom: 0; + padding-top: 7px; } +/* line 408, ../sass/bootstrap/_forms.scss */ +.form-horizontal .radio, +.form-horizontal .checkbox { + min-height: 27px; } +/* line 413, ../sass/bootstrap/_forms.scss */ +.form-horizontal .form-group { + margin-left: -15px; + margin-right: -15px; } + /* line 21, ../sass/bootstrap/_mixins.scss */ + .form-horizontal .form-group:before, .form-horizontal .form-group:after { + content: " "; + display: table; } + /* line 25, ../sass/bootstrap/_mixins.scss */ + .form-horizontal .form-group:after { + clear: both; } +/* line 417, ../sass/bootstrap/_forms.scss */ +.form-horizontal .form-control-static { + padding-top: 7px; } +@media (min-width: 768px) { + /* line 423, ../sass/bootstrap/_forms.scss */ + .form-horizontal .control-label { + text-align: right; } } +/* line 432, ../sass/bootstrap/_forms.scss */ +.form-horizontal .has-feedback .form-control-feedback { + top: 0; + right: 15px; } + +/* line 9, ../sass/bootstrap/_buttons.scss */ +.btn { + display: inline-block; + margin-bottom: 0; + font-weight: normal; + text-align: center; + vertical-align: middle; + cursor: pointer; + background-image: none; + border: 1px solid transparent; + white-space: nowrap; + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857; + border-radius: 0; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } + /* line 25, ../sass/bootstrap/_buttons.scss */ + .btn:focus, .btn:active:focus, .btn.active:focus { + outline: 0 none; } + /* line 31, ../sass/bootstrap/_buttons.scss */ + .btn:hover, .btn:focus { + color: #333333; + text-decoration: none; } + /* line 37, ../sass/bootstrap/_buttons.scss */ + .btn:active, .btn.active { + outline: 0; + background-image: none; + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); } + /* line 45, ../sass/bootstrap/_buttons.scss */ + .btn.disabled, .btn[disabled], fieldset[disabled] .btn { + cursor: not-allowed; + pointer-events: none; + opacity: 0.65; + filter: alpha(opacity=65); + -webkit-box-shadow: none; + box-shadow: none; } + +/* line 57, ../sass/bootstrap/_buttons.scss */ +.btn-default { + color: #333333; + background-color: white; + border-color: #cccccc; } + /* line 508, ../sass/bootstrap/_mixins.scss */ + .btn-default:hover, .btn-default:focus, .btn-default:active, .btn-default.active { + color: #333333; + background-color: #ebebeb; + border-color: #adadad; } + /* line 513, ../sass/bootstrap/_mixins.scss */ + .open .btn-default.dropdown-toggle { + color: #333333; + background-color: #ebebeb; + border-color: #adadad; } + /* line 519, ../sass/bootstrap/_mixins.scss */ + .btn-default:active, .btn-default.active { + background-image: none; } + /* line 522, ../sass/bootstrap/_mixins.scss */ + .open .btn-default.dropdown-toggle { + background-image: none; } + /* line 532, ../sass/bootstrap/_mixins.scss */ + .btn-default.disabled, .btn-default.disabled:hover, .btn-default.disabled:focus, .btn-default.disabled:active, .btn-default.disabled.active, .btn-default[disabled], .btn-default[disabled]:hover, .btn-default[disabled]:focus, .btn-default[disabled]:active, .btn-default[disabled].active, fieldset[disabled] .btn-default, fieldset[disabled] .btn-default:hover, fieldset[disabled] .btn-default:focus, fieldset[disabled] .btn-default:active, fieldset[disabled] .btn-default.active { + background-color: white; + border-color: #cccccc; } + /* line 538, ../sass/bootstrap/_mixins.scss */ + .btn-default .badge { + color: white; + background-color: #333333; } + +/* line 60, ../sass/bootstrap/_buttons.scss */ +.btn-primary { + color: white; + background-color: white; + border-color: #f2f2f2; } + /* line 508, ../sass/bootstrap/_mixins.scss */ + .btn-primary:hover, .btn-primary:focus, .btn-primary:active, .btn-primary.active { + color: white; + background-color: #ebebeb; + border-color: #d4d4d4; } + /* line 513, ../sass/bootstrap/_mixins.scss */ + .open .btn-primary.dropdown-toggle { + color: white; + background-color: #ebebeb; + border-color: #d4d4d4; } + /* line 519, ../sass/bootstrap/_mixins.scss */ + .btn-primary:active, .btn-primary.active { + background-image: none; } + /* line 522, ../sass/bootstrap/_mixins.scss */ + .open .btn-primary.dropdown-toggle { + background-image: none; } + /* line 532, ../sass/bootstrap/_mixins.scss */ + .btn-primary.disabled, .btn-primary.disabled:hover, .btn-primary.disabled:focus, .btn-primary.disabled:active, .btn-primary.disabled.active, .btn-primary[disabled], .btn-primary[disabled]:hover, .btn-primary[disabled]:focus, .btn-primary[disabled]:active, .btn-primary[disabled].active, fieldset[disabled] .btn-primary, fieldset[disabled] .btn-primary:hover, fieldset[disabled] .btn-primary:focus, fieldset[disabled] .btn-primary:active, fieldset[disabled] .btn-primary.active { + background-color: white; + border-color: #f2f2f2; } + /* line 538, ../sass/bootstrap/_mixins.scss */ + .btn-primary .badge { + color: white; + background-color: white; } + +/* line 64, ../sass/bootstrap/_buttons.scss */ +.btn-success { + color: white; + background-color: #5cb85c; + border-color: #4cae4c; } + /* line 508, ../sass/bootstrap/_mixins.scss */ + .btn-success:hover, .btn-success:focus, .btn-success:active, .btn-success.active { + color: white; + background-color: #47a447; + border-color: #398439; } + /* line 513, ../sass/bootstrap/_mixins.scss */ + .open .btn-success.dropdown-toggle { + color: white; + background-color: #47a447; + border-color: #398439; } + /* line 519, ../sass/bootstrap/_mixins.scss */ + .btn-success:active, .btn-success.active { + background-image: none; } + /* line 522, ../sass/bootstrap/_mixins.scss */ + .open .btn-success.dropdown-toggle { + background-image: none; } + /* line 532, ../sass/bootstrap/_mixins.scss */ + .btn-success.disabled, .btn-success.disabled:hover, .btn-success.disabled:focus, .btn-success.disabled:active, .btn-success.disabled.active, .btn-success[disabled], .btn-success[disabled]:hover, .btn-success[disabled]:focus, .btn-success[disabled]:active, .btn-success[disabled].active, fieldset[disabled] .btn-success, fieldset[disabled] .btn-success:hover, fieldset[disabled] .btn-success:focus, fieldset[disabled] .btn-success:active, fieldset[disabled] .btn-success.active { + background-color: #5cb85c; + border-color: #4cae4c; } + /* line 538, ../sass/bootstrap/_mixins.scss */ + .btn-success .badge { + color: #5cb85c; + background-color: white; } + +/* line 68, ../sass/bootstrap/_buttons.scss */ +.btn-info { + color: white; + background-color: #5bc0de; + border-color: #46b8da; } + /* line 508, ../sass/bootstrap/_mixins.scss */ + .btn-info:hover, .btn-info:focus, .btn-info:active, .btn-info.active { + color: white; + background-color: #39b3d7; + border-color: #269abc; } + /* line 513, ../sass/bootstrap/_mixins.scss */ + .open .btn-info.dropdown-toggle { + color: white; + background-color: #39b3d7; + border-color: #269abc; } + /* line 519, ../sass/bootstrap/_mixins.scss */ + .btn-info:active, .btn-info.active { + background-image: none; } + /* line 522, ../sass/bootstrap/_mixins.scss */ + .open .btn-info.dropdown-toggle { + background-image: none; } + /* line 532, ../sass/bootstrap/_mixins.scss */ + .btn-info.disabled, .btn-info.disabled:hover, .btn-info.disabled:focus, .btn-info.disabled:active, .btn-info.disabled.active, .btn-info[disabled], .btn-info[disabled]:hover, .btn-info[disabled]:focus, .btn-info[disabled]:active, .btn-info[disabled].active, fieldset[disabled] .btn-info, fieldset[disabled] .btn-info:hover, fieldset[disabled] .btn-info:focus, fieldset[disabled] .btn-info:active, fieldset[disabled] .btn-info.active { + background-color: #5bc0de; + border-color: #46b8da; } + /* line 538, ../sass/bootstrap/_mixins.scss */ + .btn-info .badge { + color: #5bc0de; + background-color: white; } + +/* line 72, ../sass/bootstrap/_buttons.scss */ +.btn-warning { + color: white; + background-color: #f0ad4e; + border-color: #eea236; } + /* line 508, ../sass/bootstrap/_mixins.scss */ + .btn-warning:hover, .btn-warning:focus, .btn-warning:active, .btn-warning.active { + color: white; + background-color: #ed9c28; + border-color: #d58512; } + /* line 513, ../sass/bootstrap/_mixins.scss */ + .open .btn-warning.dropdown-toggle { + color: white; + background-color: #ed9c28; + border-color: #d58512; } + /* line 519, ../sass/bootstrap/_mixins.scss */ + .btn-warning:active, .btn-warning.active { + background-image: none; } + /* line 522, ../sass/bootstrap/_mixins.scss */ + .open .btn-warning.dropdown-toggle { + background-image: none; } + /* line 532, ../sass/bootstrap/_mixins.scss */ + .btn-warning.disabled, .btn-warning.disabled:hover, .btn-warning.disabled:focus, .btn-warning.disabled:active, .btn-warning.disabled.active, .btn-warning[disabled], .btn-warning[disabled]:hover, .btn-warning[disabled]:focus, .btn-warning[disabled]:active, .btn-warning[disabled].active, fieldset[disabled] .btn-warning, fieldset[disabled] .btn-warning:hover, fieldset[disabled] .btn-warning:focus, fieldset[disabled] .btn-warning:active, fieldset[disabled] .btn-warning.active { + background-color: #f0ad4e; + border-color: #eea236; } + /* line 538, ../sass/bootstrap/_mixins.scss */ + .btn-warning .badge { + color: #f0ad4e; + background-color: white; } + +/* line 76, ../sass/bootstrap/_buttons.scss */ +.btn-danger { + color: white; + background-color: #d9534f; + border-color: #d43f3a; } + /* line 508, ../sass/bootstrap/_mixins.scss */ + .btn-danger:hover, .btn-danger:focus, .btn-danger:active, .btn-danger.active { + color: white; + background-color: #d2322d; + border-color: #ac2925; } + /* line 513, ../sass/bootstrap/_mixins.scss */ + .open .btn-danger.dropdown-toggle { + color: white; + background-color: #d2322d; + border-color: #ac2925; } + /* line 519, ../sass/bootstrap/_mixins.scss */ + .btn-danger:active, .btn-danger.active { + background-image: none; } + /* line 522, ../sass/bootstrap/_mixins.scss */ + .open .btn-danger.dropdown-toggle { + background-image: none; } + /* line 532, ../sass/bootstrap/_mixins.scss */ + .btn-danger.disabled, .btn-danger.disabled:hover, .btn-danger.disabled:focus, .btn-danger.disabled:active, .btn-danger.disabled.active, .btn-danger[disabled], .btn-danger[disabled]:hover, .btn-danger[disabled]:focus, .btn-danger[disabled]:active, .btn-danger[disabled].active, fieldset[disabled] .btn-danger, fieldset[disabled] .btn-danger:hover, fieldset[disabled] .btn-danger:focus, fieldset[disabled] .btn-danger:active, fieldset[disabled] .btn-danger.active { + background-color: #d9534f; + border-color: #d43f3a; } + /* line 538, ../sass/bootstrap/_mixins.scss */ + .btn-danger .badge { + color: #d9534f; + background-color: white; } + +/* line 85, ../sass/bootstrap/_buttons.scss */ +.btn-link { + color: #4d99d8; + font-weight: normal; + cursor: pointer; + border-radius: 0; } + /* line 94, ../sass/bootstrap/_buttons.scss */ + .btn-link, .btn-link:active, .btn-link[disabled], fieldset[disabled] .btn-link { + background-color: transparent; + -webkit-box-shadow: none; + box-shadow: none; } + /* line 101, ../sass/bootstrap/_buttons.scss */ + .btn-link, .btn-link:hover, .btn-link:focus, .btn-link:active { + border-color: transparent; } + /* line 105, ../sass/bootstrap/_buttons.scss */ + .btn-link:hover, .btn-link:focus { + color: #83b8e4; + text-decoration: underline; + background-color: transparent; } + /* line 113, ../sass/bootstrap/_buttons.scss */ + .btn-link[disabled]:hover, .btn-link[disabled]:focus, fieldset[disabled] .btn-link:hover, fieldset[disabled] .btn-link:focus { + color: #818181; + text-decoration: none; } + +/* line 124, ../sass/bootstrap/_buttons.scss */ +.btn-lg, .btn-group-lg > .btn { + padding: 10px 16px; + font-size: 18px; + line-height: 1.33; + border-radius: 6px; } + +/* line 128, ../sass/bootstrap/_buttons.scss */ +.btn-sm, .btn-group-sm > .btn { + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; } + +/* line 132, ../sass/bootstrap/_buttons.scss */ +.btn-xs, .btn-group-xs > .btn { + padding: 1px 5px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; } + +/* line 140, ../sass/bootstrap/_buttons.scss */ +.btn-block { + display: block; + width: 100%; + padding-left: 0; + padding-right: 0; } + +/* line 148, ../sass/bootstrap/_buttons.scss */ +.btn-block + .btn-block { + margin-top: 5px; } + +/* line 156, ../sass/bootstrap/_buttons.scss */ +input[type="submit"].btn-block, +input[type="reset"].btn-block, +input[type="button"].btn-block { + width: 100%; } + +/* line 10, ../sass/bootstrap/_component-animations.scss */ +.fade { + opacity: 0; + -webkit-transition: opacity 0.15s linear; + transition: opacity 0.15s linear; } + /* line 13, ../sass/bootstrap/_component-animations.scss */ + .fade.in { + opacity: 1; } + +/* line 18, ../sass/bootstrap/_component-animations.scss */ +.collapse { + display: none; } + /* line 20, ../sass/bootstrap/_component-animations.scss */ + .collapse.in { + display: block; } + +/* line 24, ../sass/bootstrap/_component-animations.scss */ +.collapsing { + position: relative; + height: 0; + overflow: hidden; + -webkit-transition: height 0.35s ease; + transition: height 0.35s ease; } + +/* line 7, ../sass/bootstrap/_dropdowns.scss */ +.caret { + display: inline-block; + width: 0; + height: 0; + margin-left: 2px; + vertical-align: middle; + border-top: 4px solid; + border-right: 4px solid transparent; + border-left: 4px solid transparent; } + +/* line 19, ../sass/bootstrap/_dropdowns.scss */ +.dropdown { + position: relative; } + +/* line 24, ../sass/bootstrap/_dropdowns.scss */ +.dropdown-toggle:focus { + outline: 0; } + +/* line 29, ../sass/bootstrap/_dropdowns.scss */ +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + list-style: none; + font-size: 14px; + background-color: white; + border: 1px solid #cccccc; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 0; + -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + background-clip: padding-box; } + /* line 51, ../sass/bootstrap/_dropdowns.scss */ + .dropdown-menu.pull-right { + right: 0; + left: auto; } + /* line 57, ../sass/bootstrap/_dropdowns.scss */ + .dropdown-menu .divider { + height: 1px; + margin: 9px 0; + overflow: hidden; + background-color: #e5e5e5; } + /* line 62, ../sass/bootstrap/_dropdowns.scss */ + .dropdown-menu > li > a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 1.42857; + color: #303030; + white-space: nowrap; } + +/* line 76, ../sass/bootstrap/_dropdowns.scss */ +.dropdown-menu > li > a:hover, .dropdown-menu > li > a:focus { + text-decoration: none; + color: #303030; + background-color: #d9d9d9; } + +/* line 87, ../sass/bootstrap/_dropdowns.scss */ +.dropdown-menu > .active > a, .dropdown-menu > .active > a:hover, .dropdown-menu > .active > a:focus { + color: white; + text-decoration: none; + outline: 0; + background-color: #ee5161; } + +/* line 102, ../sass/bootstrap/_dropdowns.scss */ +.dropdown-menu > .disabled > a, .dropdown-menu > .disabled > a:hover, .dropdown-menu > .disabled > a:focus { + color: #4e4e4e; } + +/* line 109, ../sass/bootstrap/_dropdowns.scss */ +.dropdown-menu > .disabled > a:hover, .dropdown-menu > .disabled > a:focus { + text-decoration: none; + background-color: transparent; + background-image: none; + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + cursor: not-allowed; } + +/* line 121, ../sass/bootstrap/_dropdowns.scss */ +.open > .dropdown-menu { + display: block; } +/* line 126, ../sass/bootstrap/_dropdowns.scss */ +.open > a { + outline: 0; } + +/* line 135, ../sass/bootstrap/_dropdowns.scss */ +.dropdown-menu-right { + left: auto; + right: 0; } + +/* line 145, ../sass/bootstrap/_dropdowns.scss */ +.dropdown-menu-left { + left: 0; + right: auto; } + +/* line 151, ../sass/bootstrap/_dropdowns.scss */ +.dropdown-header { + display: block; + padding: 3px 20px; + font-size: 12px; + line-height: 1.42857; + color: #4e4e4e; } + +/* line 160, ../sass/bootstrap/_dropdowns.scss */ +.dropdown-backdrop { + position: fixed; + left: 0; + right: 0; + bottom: 0; + top: 0; + z-index: 990; } + +/* line 170, ../sass/bootstrap/_dropdowns.scss */ +.pull-right > .dropdown-menu { + right: 0; + left: auto; } + +/* line 183, ../sass/bootstrap/_dropdowns.scss */ +.dropup .caret, +.navbar-fixed-bottom .dropdown .caret { + border-top: 0; + border-bottom: 4px solid; + content: ""; } +/* line 189, ../sass/bootstrap/_dropdowns.scss */ +.dropup .dropdown-menu, +.navbar-fixed-bottom .dropdown .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 1px; } + +@media (min-width: 768px) { + /* line 203, ../sass/bootstrap/_dropdowns.scss */ + .navbar-right .dropdown-menu { + right: 0; + left: auto; } + /* line 208, ../sass/bootstrap/_dropdowns.scss */ + .navbar-right .dropdown-menu-left { + left: 0; + right: auto; } } +/* line 7, ../sass/bootstrap/_button-groups.scss */ +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-block; + vertical-align: middle; } + /* line 11, ../sass/bootstrap/_button-groups.scss */ + .btn-group > .btn, + .btn-group-vertical > .btn { + position: relative; + float: left; } + /* line 18, ../sass/bootstrap/_button-groups.scss */ + .btn-group > .btn:hover, .btn-group > .btn:focus, .btn-group > .btn:active, .btn-group > .btn.active, + .btn-group-vertical > .btn:hover, + .btn-group-vertical > .btn:focus, + .btn-group-vertical > .btn:active, + .btn-group-vertical > .btn.active { + z-index: 2; } + /* line 21, ../sass/bootstrap/_button-groups.scss */ + .btn-group > .btn:focus, + .btn-group-vertical > .btn:focus { + outline: none; } + +/* line 33, ../sass/bootstrap/_button-groups.scss */ +.btn-group .btn + .btn, +.btn-group .btn + .btn-group, +.btn-group .btn-group + .btn, +.btn-group .btn-group + .btn-group { + margin-left: -1px; } + +/* line 39, ../sass/bootstrap/_button-groups.scss */ +.btn-toolbar { + margin-left: -5px; } + /* line 21, ../sass/bootstrap/_mixins.scss */ + .btn-toolbar:before, .btn-toolbar:after { + content: " "; + display: table; } + /* line 25, ../sass/bootstrap/_mixins.scss */ + .btn-toolbar:after { + clear: both; } + /* line 44, ../sass/bootstrap/_button-groups.scss */ + .btn-toolbar .btn-group, + .btn-toolbar .input-group { + float: left; } + /* line 49, ../sass/bootstrap/_button-groups.scss */ + .btn-toolbar > .btn, + .btn-toolbar > .btn-group, + .btn-toolbar > .input-group { + margin-left: 5px; } + +/* line 54, ../sass/bootstrap/_button-groups.scss */ +.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { + border-radius: 0; } + +/* line 59, ../sass/bootstrap/_button-groups.scss */ +.btn-group > .btn:first-child { + margin-left: 0; } + /* line 61, ../sass/bootstrap/_button-groups.scss */ + .btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) { + border-bottom-right-radius: 0; + border-top-right-radius: 0; } + +/* line 67, ../sass/bootstrap/_button-groups.scss */ +.btn-group > .btn:last-child:not(:first-child), +.btn-group > .dropdown-toggle:not(:first-child) { + border-bottom-left-radius: 0; + border-top-left-radius: 0; } + +/* line 72, ../sass/bootstrap/_button-groups.scss */ +.btn-group > .btn-group { + float: left; } + +/* line 75, ../sass/bootstrap/_button-groups.scss */ +.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; } + +/* line 80, ../sass/bootstrap/_button-groups.scss */ +.btn-group > .btn-group:first-child > .btn:last-child, +.btn-group > .btn-group:first-child > .dropdown-toggle { + border-bottom-right-radius: 0; + border-top-right-radius: 0; } + +/* line 84, ../sass/bootstrap/_button-groups.scss */ +.btn-group > .btn-group:last-child > .btn:first-child { + border-bottom-left-radius: 0; + border-top-left-radius: 0; } + +/* line 90, ../sass/bootstrap/_button-groups.scss */ +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; } + +/* line 108, ../sass/bootstrap/_button-groups.scss */ +.btn-group > .btn + .dropdown-toggle { + padding-left: 8px; + padding-right: 8px; } + +/* line 112, ../sass/bootstrap/_button-groups.scss */ +.btn-group > .btn-lg + .dropdown-toggle, .btn-group-lg.btn-group > .btn + .dropdown-toggle { + padding-left: 12px; + padding-right: 12px; } + +/* line 119, ../sass/bootstrap/_button-groups.scss */ +.btn-group.open .dropdown-toggle { + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); } + /* line 123, ../sass/bootstrap/_button-groups.scss */ + .btn-group.open .dropdown-toggle.btn-link { + -webkit-box-shadow: none; + box-shadow: none; } + +/* line 130, ../sass/bootstrap/_button-groups.scss */ +.btn .caret { + margin-left: 0; } + +/* line 134, ../sass/bootstrap/_button-groups.scss */ +.btn-lg .caret, .btn-group-lg > .btn .caret { + border-width: 5px 5px 0; + border-bottom-width: 0; } + +/* line 139, ../sass/bootstrap/_button-groups.scss */ +.dropup .btn-lg .caret, .dropup .btn-group-lg > .btn .caret { + border-width: 0 5px 5px; } + +/* line 150, ../sass/bootstrap/_button-groups.scss */ +.btn-group-vertical > .btn, +.btn-group-vertical > .btn-group, +.btn-group-vertical > .btn-group > .btn { + display: block; + float: none; + width: 100%; + max-width: 100%; } +/* line 21, ../sass/bootstrap/_mixins.scss */ +.btn-group-vertical > .btn-group:before, .btn-group-vertical > .btn-group:after { + content: " "; + display: table; } +/* line 25, ../sass/bootstrap/_mixins.scss */ +.btn-group-vertical > .btn-group:after { + clear: both; } +/* line 160, ../sass/bootstrap/_button-groups.scss */ +.btn-group-vertical > .btn-group > .btn { + float: none; } +/* line 168, ../sass/bootstrap/_button-groups.scss */ +.btn-group-vertical > .btn + .btn, +.btn-group-vertical > .btn + .btn-group, +.btn-group-vertical > .btn-group + .btn, +.btn-group-vertical > .btn-group + .btn-group { + margin-top: -1px; + margin-left: 0; } + +/* line 175, ../sass/bootstrap/_button-groups.scss */ +.btn-group-vertical > .btn:not(:first-child):not(:last-child) { + border-radius: 0; } +/* line 178, ../sass/bootstrap/_button-groups.scss */ +.btn-group-vertical > .btn:first-child:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; } +/* line 182, ../sass/bootstrap/_button-groups.scss */ +.btn-group-vertical > .btn:last-child:not(:first-child) { + border-bottom-left-radius: 0; + border-top-right-radius: 0; + border-top-left-radius: 0; } + +/* line 187, ../sass/bootstrap/_button-groups.scss */ +.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; } + +/* line 192, ../sass/bootstrap/_button-groups.scss */ +.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child, +.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; } + +/* line 196, ../sass/bootstrap/_button-groups.scss */ +.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { + border-top-right-radius: 0; + border-top-left-radius: 0; } + +/* line 205, ../sass/bootstrap/_button-groups.scss */ +.btn-group-justified { + display: table; + width: 100%; + table-layout: fixed; + border-collapse: separate; } + /* line 211, ../sass/bootstrap/_button-groups.scss */ + .btn-group-justified > .btn, + .btn-group-justified > .btn-group { + float: none; + display: table-cell; + width: 1%; } + /* line 216, ../sass/bootstrap/_button-groups.scss */ + .btn-group-justified > .btn-group .btn { + width: 100%; } + +/* line 224, ../sass/bootstrap/_button-groups.scss */ +[data-toggle="buttons"] > .btn > input[type="radio"], +[data-toggle="buttons"] > .btn > input[type="checkbox"] { + display: none; } + +/* line 7, ../sass/bootstrap/_input-groups.scss */ +.input-group { + position: relative; + display: table; + border-collapse: separate; } + /* line 13, ../sass/bootstrap/_input-groups.scss */ + .input-group[class*="col-"] { + float: none; + padding-left: 0; + padding-right: 0; } + /* line 19, ../sass/bootstrap/_input-groups.scss */ + .input-group .form-control { + position: relative; + z-index: 2; + float: left; + width: 100%; + margin-bottom: 0; } + +/* line 52, ../sass/bootstrap/_input-groups.scss */ +.input-group-addon, +.input-group-btn, +.input-group .form-control { + display: table-cell; } + /* line 55, ../sass/bootstrap/_input-groups.scss */ + .input-group-addon:not(:first-child):not(:last-child), + .input-group-btn:not(:first-child):not(:last-child), + .input-group .form-control:not(:first-child):not(:last-child) { + border-radius: 0; } + +/* line 61, ../sass/bootstrap/_input-groups.scss */ +.input-group-addon, +.input-group-btn { + width: 1%; + white-space: nowrap; + vertical-align: middle; } + +/* line 69, ../sass/bootstrap/_input-groups.scss */ +.input-group-addon { + padding: 6px 12px; + font-size: 14px; + font-weight: normal; + line-height: 1; + color: #555555; + text-align: center; + background-color: #d9d9d9; + border: 1px solid #cccccc; + border-radius: 0; } + /* line 81, ../sass/bootstrap/_input-groups.scss */ + .input-group-addon.input-sm, + .input-group-sm > .input-group-addon, + .input-group-sm > .input-group-btn > .input-group-addon.btn { + padding: 5px 10px; + font-size: 12px; + border-radius: 3px; } + /* line 86, ../sass/bootstrap/_input-groups.scss */ + .input-group-addon.input-lg, + .input-group-lg > .input-group-addon, + .input-group-lg > .input-group-btn > .input-group-addon.btn { + padding: 10px 16px; + font-size: 18px; + border-radius: 6px; } + /* line 94, ../sass/bootstrap/_input-groups.scss */ + .input-group-addon input[type="radio"], + .input-group-addon input[type="checkbox"] { + margin-top: 0; } + +/* line 106, ../sass/bootstrap/_input-groups.scss */ +.input-group .form-control:first-child, +.input-group-addon:first-child, +.input-group-btn:first-child > .btn, +.input-group-btn:first-child > .btn-group > .btn, +.input-group-btn:first-child > .dropdown-toggle, +.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle), +.input-group-btn:last-child > .btn-group:not(:last-child) > .btn { + border-bottom-right-radius: 0; + border-top-right-radius: 0; } + +/* line 109, ../sass/bootstrap/_input-groups.scss */ +.input-group-addon:first-child { + border-right: 0; } + +/* line 118, ../sass/bootstrap/_input-groups.scss */ +.input-group .form-control:last-child, +.input-group-addon:last-child, +.input-group-btn:last-child > .btn, +.input-group-btn:last-child > .btn-group > .btn, +.input-group-btn:last-child > .dropdown-toggle, +.input-group-btn:first-child > .btn:not(:first-child), +.input-group-btn:first-child > .btn-group:not(:first-child) > .btn { + border-bottom-left-radius: 0; + border-top-left-radius: 0; } + +/* line 121, ../sass/bootstrap/_input-groups.scss */ +.input-group-addon:last-child { + border-left: 0; } + +/* line 127, ../sass/bootstrap/_input-groups.scss */ +.input-group-btn { + position: relative; + font-size: 0; + white-space: nowrap; } + /* line 136, ../sass/bootstrap/_input-groups.scss */ + .input-group-btn > .btn { + position: relative; } + /* line 138, ../sass/bootstrap/_input-groups.scss */ + .input-group-btn > .btn + .btn { + margin-left: -1px; } + /* line 144, ../sass/bootstrap/_input-groups.scss */ + .input-group-btn > .btn:hover, .input-group-btn > .btn:focus, .input-group-btn > .btn:active { + z-index: 2; } + /* line 152, ../sass/bootstrap/_input-groups.scss */ + .input-group-btn:first-child > .btn, + .input-group-btn:first-child > .btn-group { + margin-right: -1px; } + /* line 158, ../sass/bootstrap/_input-groups.scss */ + .input-group-btn:last-child > .btn, + .input-group-btn:last-child > .btn-group { + margin-left: -1px; } + +/* line 9, ../sass/bootstrap/_navs.scss */ +.nav { + margin-bottom: 0; + padding-left: 0; + list-style: none; } + /* line 21, ../sass/bootstrap/_mixins.scss */ + .nav:before, .nav:after { + content: " "; + display: table; } + /* line 25, ../sass/bootstrap/_mixins.scss */ + .nav:after { + clear: both; } + /* line 15, ../sass/bootstrap/_navs.scss */ + .nav > li { + position: relative; + display: block; } + /* line 19, ../sass/bootstrap/_navs.scss */ + .nav > li > a { + position: relative; + display: block; + padding: 10px 15px; } + /* line 24, ../sass/bootstrap/_navs.scss */ + .nav > li > a:hover, .nav > li > a:focus { + text-decoration: none; + background-color: #d9d9d9; } + /* line 31, ../sass/bootstrap/_navs.scss */ + .nav > li.disabled > a { + color: #4e4e4e; } + /* line 35, ../sass/bootstrap/_navs.scss */ + .nav > li.disabled > a:hover, .nav > li.disabled > a:focus { + color: #4e4e4e; + text-decoration: none; + background-color: transparent; + cursor: not-allowed; } + /* line 48, ../sass/bootstrap/_navs.scss */ + .nav .open > a, .nav .open > a:hover, .nav .open > a:focus { + background-color: #d9d9d9; + border-color: #4d99d8; } + /* line 59, ../sass/bootstrap/_navs.scss */ + .nav .nav-divider { + height: 1px; + margin: 9px 0; + overflow: hidden; + background-color: #e5e5e5; } + /* line 66, ../sass/bootstrap/_navs.scss */ + .nav > li > a > img { + max-width: none; } + +/* line 76, ../sass/bootstrap/_navs.scss */ +.nav-tabs { + border-bottom: 1px solid transparent; } + /* line 78, ../sass/bootstrap/_navs.scss */ + .nav-tabs > li { + float: left; + margin-bottom: -1px; } + /* line 84, ../sass/bootstrap/_navs.scss */ + .nav-tabs > li > a { + margin-right: 2px; + line-height: 1.42857; + border: 1px solid transparent; + border-radius: 0 0 0 0; + color: white; } + /* line 91, ../sass/bootstrap/_navs.scss */ + .nav-tabs > li > a:hover, .nav-tabs > li > a:focus { + background: inherit; + border-color: inherit inherit transparent; } + /* line 102, ../sass/bootstrap/_navs.scss */ + .nav-tabs > li.active > a, .nav-tabs > li.active > a:hover, .nav-tabs > li.active > a:focus { + color: white; + background-color: #4e4e4e; + border: 1px solid inherit; + border-bottom-color: transparent; + cursor: default; } + +/* line 122, ../sass/bootstrap/_navs.scss */ +.nav-pills > li { + float: left; } + /* line 126, ../sass/bootstrap/_navs.scss */ + .nav-pills > li > a { + border-radius: 0; } + /* line 129, ../sass/bootstrap/_navs.scss */ + .nav-pills > li + li { + margin-left: 2px; } + /* line 137, ../sass/bootstrap/_navs.scss */ + .nav-pills > li.active > a, .nav-pills > li.active > a:hover, .nav-pills > li.active > a:focus { + color: white; + background-color: #ee5161; } + +/* line 148, ../sass/bootstrap/_navs.scss */ +.nav-stacked > li { + float: none; } + /* line 150, ../sass/bootstrap/_navs.scss */ + .nav-stacked > li + li { + margin-top: 2px; + margin-left: 0; } + +/* line 164, ../sass/bootstrap/_navs.scss */ +.nav-justified, .nav-tabs.nav-justified { + width: 100%; } + /* line 167, ../sass/bootstrap/_navs.scss */ + .nav-justified > li, .nav-tabs.nav-justified > li { + float: none; } + /* line 169, ../sass/bootstrap/_navs.scss */ + .nav-justified > li > a, .nav-tabs.nav-justified > li > a { + text-align: center; + margin-bottom: 5px; } + /* line 175, ../sass/bootstrap/_navs.scss */ + .nav-justified > .dropdown .dropdown-menu { + top: auto; + left: auto; } + @media (min-width: 768px) { + /* line 181, ../sass/bootstrap/_navs.scss */ + .nav-justified > li, .nav-tabs.nav-justified > li { + display: table-cell; + width: 1%; } + /* line 184, ../sass/bootstrap/_navs.scss */ + .nav-justified > li > a, .nav-tabs.nav-justified > li > a { + margin-bottom: 0; } } + +/* line 194, ../sass/bootstrap/_navs.scss */ +.nav-tabs-justified, .nav-tabs.nav-justified { + border-bottom: 0; } + /* line 197, ../sass/bootstrap/_navs.scss */ + .nav-tabs-justified > li > a, .nav-tabs.nav-justified > li > a { + margin-right: 0; + border-radius: 0; } + /* line 205, ../sass/bootstrap/_navs.scss */ + .nav-tabs-justified > .active > a, .nav-tabs.nav-justified > .active > a, + .nav-tabs-justified > .active > a:hover, + .nav-tabs.nav-justified > .active > a:hover, + .nav-tabs-justified > .active > a:focus, + .nav-tabs.nav-justified > .active > a:focus { + border: 1px solid #dddddd; } + @media (min-width: 768px) { + /* line 210, ../sass/bootstrap/_navs.scss */ + .nav-tabs-justified > li > a, .nav-tabs.nav-justified > li > a { + border-bottom: 1px solid #dddddd; + border-radius: 0 0 0 0; } + /* line 216, ../sass/bootstrap/_navs.scss */ + .nav-tabs-justified > .active > a, .nav-tabs.nav-justified > .active > a, + .nav-tabs-justified > .active > a:hover, + .nav-tabs.nav-justified > .active > a:hover, + .nav-tabs-justified > .active > a:focus, + .nav-tabs.nav-justified > .active > a:focus { + border-bottom-color: #303030; } } + +/* line 228, ../sass/bootstrap/_navs.scss */ +.tab-content > .tab-pane { + display: none; } +/* line 231, ../sass/bootstrap/_navs.scss */ +.tab-content > .active { + display: block; } + +/* line 241, ../sass/bootstrap/_navs.scss */ +.nav-tabs .dropdown-menu { + margin-top: -1px; + border-top-right-radius: 0; + border-top-left-radius: 0; } + +/* line 11, ../sass/bootstrap/_navbar.scss */ +.navbar { + position: relative; + min-height: 50px; + margin-bottom: 20px; + border: 1px solid transparent; } + /* line 21, ../sass/bootstrap/_mixins.scss */ + .navbar:before, .navbar:after { + content: " "; + display: table; } + /* line 25, ../sass/bootstrap/_mixins.scss */ + .navbar:after { + clear: both; } + @media (min-width: 768px) { + /* line 11, ../sass/bootstrap/_navbar.scss */ + .navbar { + border-radius: 0; } } + +/* line 21, ../sass/bootstrap/_mixins.scss */ +.navbar-header:before, .navbar-header:after { + content: " "; + display: table; } +/* line 25, ../sass/bootstrap/_mixins.scss */ +.navbar-header:after { + clear: both; } +@media (min-width: 768px) { + /* line 31, ../sass/bootstrap/_navbar.scss */ + .navbar-header { + float: left; } } + +/* line 50, ../sass/bootstrap/_navbar.scss */ +.navbar-collapse { + max-height: 340px; + overflow-x: visible; + padding-right: 0; + padding-left: 0; + border-top: 1px solid transparent; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); + -webkit-overflow-scrolling: touch; } + /* line 21, ../sass/bootstrap/_mixins.scss */ + .navbar-collapse:before, .navbar-collapse:after { + content: " "; + display: table; } + /* line 25, ../sass/bootstrap/_mixins.scss */ + .navbar-collapse:after { + clear: both; } + /* line 60, ../sass/bootstrap/_navbar.scss */ + .navbar-collapse.in { + overflow-y: auto; } + @media (min-width: 768px) { + /* line 50, ../sass/bootstrap/_navbar.scss */ + .navbar-collapse { + width: auto; + border-top: 0; + box-shadow: none; } + /* line 69, ../sass/bootstrap/_navbar.scss */ + .navbar-collapse.collapse { + display: block !important; + height: auto !important; + padding-bottom: 0; + overflow: visible !important; } + /* line 76, ../sass/bootstrap/_navbar.scss */ + .navbar-collapse.in { + overflow-y: visible; } + /* line 84, ../sass/bootstrap/_navbar.scss */ + .navbar-fixed-top .navbar-collapse, .navbar-static-top .navbar-collapse, .navbar-fixed-bottom .navbar-collapse { + padding-left: 0; + padding-right: 0; } } + +/* line 99, ../sass/bootstrap/_navbar.scss */ +.container > .navbar-header, +.container > .navbar-collapse, +.container-fluid > .navbar-header, +.container-fluid > .navbar-collapse { + margin-right: 0; + margin-left: 0; } + @media (min-width: 768px) { + /* line 99, ../sass/bootstrap/_navbar.scss */ + .container > .navbar-header, + .container > .navbar-collapse, + .container-fluid > .navbar-header, + .container-fluid > .navbar-collapse { + margin-right: 0; + margin-left: 0; } } + +/* line 118, ../sass/bootstrap/_navbar.scss */ +.navbar-static-top { + z-index: 1000; + border-width: 0 0 1px; } + @media (min-width: 768px) { + /* line 118, ../sass/bootstrap/_navbar.scss */ + .navbar-static-top { + border-radius: 0; } } + +/* line 129, ../sass/bootstrap/_navbar.scss */ +.navbar-fixed-top, +.navbar-fixed-bottom { + position: fixed; + right: 0; + left: 0; + z-index: 1030; } + @media (min-width: 768px) { + /* line 129, ../sass/bootstrap/_navbar.scss */ + .navbar-fixed-top, + .navbar-fixed-bottom { + border-radius: 0; } } + +/* line 140, ../sass/bootstrap/_navbar.scss */ +.navbar-fixed-top { + top: 0; + border-width: 0 0 1px; } + +/* line 144, ../sass/bootstrap/_navbar.scss */ +.navbar-fixed-bottom { + bottom: 0; + margin-bottom: 0; + border-width: 1px 0 0; } + +/* line 153, ../sass/bootstrap/_navbar.scss */ +.navbar-brand { + float: left; + padding: 15px 0; + font-size: 18px; + line-height: 20px; + height: 50px; } + /* line 161, ../sass/bootstrap/_navbar.scss */ + .navbar-brand:hover, .navbar-brand:focus { + text-decoration: none; } + @media (min-width: 768px) { + /* line 167, ../sass/bootstrap/_navbar.scss */ + .navbar > .container .navbar-brand, .navbar > .container-fluid .navbar-brand { + margin-left: 0; } } + +/* line 179, ../sass/bootstrap/_navbar.scss */ +.navbar-toggle { + position: relative; + float: right; + margin-right: 0; + padding: 9px 10px; + margin-top: 8px; + margin-bottom: 8px; + background-color: transparent; + background-image: none; + border: 1px solid transparent; + border-radius: 0; } + /* line 192, ../sass/bootstrap/_navbar.scss */ + .navbar-toggle:focus { + outline: none; } + /* line 197, ../sass/bootstrap/_navbar.scss */ + .navbar-toggle .icon-bar { + display: block; + width: 22px; + height: 2px; + border-radius: 1px; } + /* line 203, ../sass/bootstrap/_navbar.scss */ + .navbar-toggle .icon-bar + .icon-bar { + margin-top: 4px; } + @media (min-width: 768px) { + /* line 179, ../sass/bootstrap/_navbar.scss */ + .navbar-toggle { + display: none; } } + +/* line 218, ../sass/bootstrap/_navbar.scss */ +.navbar-nav { + margin: 7.5px 0; } + /* line 221, ../sass/bootstrap/_navbar.scss */ + .navbar-nav > li > a { + padding-top: 10px; + padding-bottom: 10px; + line-height: 20px; } + @media (max-width: 767px) { + /* line 229, ../sass/bootstrap/_navbar.scss */ + .navbar-nav .open .dropdown-menu { + position: static; + float: none; + width: auto; + margin-top: 0; + background-color: transparent; + border: 0; + box-shadow: none; } + /* line 238, ../sass/bootstrap/_navbar.scss */ + .navbar-nav .open .dropdown-menu > li > a, + .navbar-nav .open .dropdown-menu .dropdown-header { + padding: 5px 15px 5px 25px; } + /* line 241, ../sass/bootstrap/_navbar.scss */ + .navbar-nav .open .dropdown-menu > li > a { + line-height: 20px; } + /* line 244, ../sass/bootstrap/_navbar.scss */ + .navbar-nav .open .dropdown-menu > li > a:hover, .navbar-nav .open .dropdown-menu > li > a:focus { + background-image: none; } } + @media (min-width: 768px) { + /* line 218, ../sass/bootstrap/_navbar.scss */ + .navbar-nav { + float: left; + margin: 0; } + /* line 256, ../sass/bootstrap/_navbar.scss */ + .navbar-nav > li { + float: left; } + /* line 258, ../sass/bootstrap/_navbar.scss */ + .navbar-nav > li > a { + padding-top: 15px; + padding-bottom: 15px; } + /* line 264, ../sass/bootstrap/_navbar.scss */ + .navbar-nav.navbar-right:last-child { + margin-right: 0; } } + +@media (min-width: 768px) { + /* line 278, ../sass/bootstrap/_navbar.scss */ + .navbar-left { + float: left !important; } + + /* line 281, ../sass/bootstrap/_navbar.scss */ + .navbar-right { + float: right !important; } } +/* line 292, ../sass/bootstrap/_navbar.scss */ +.navbar-form { + margin-left: 0; + margin-right: 0; + padding: 10px 0; + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + margin-top: 8px; + margin-bottom: 8px; } + @media (max-width: 767px) { + /* line 304, ../sass/bootstrap/_navbar.scss */ + .navbar-form .form-group { + margin-bottom: 5px; } } + @media (min-width: 768px) { + /* line 292, ../sass/bootstrap/_navbar.scss */ + .navbar-form { + width: auto; + border: 0; + margin-left: 0; + margin-right: 0; + padding-top: 0; + padding-bottom: 0; + -webkit-box-shadow: none; + box-shadow: none; } + /* line 324, ../sass/bootstrap/_navbar.scss */ + .navbar-form.navbar-right:last-child { + margin-right: 0; } } + +/* line 334, ../sass/bootstrap/_navbar.scss */ +.navbar-nav > li > .dropdown-menu { + margin-top: 0; + border-top-right-radius: 0; + border-top-left-radius: 0; } + +/* line 339, ../sass/bootstrap/_navbar.scss */ +.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; } + +/* line 348, ../sass/bootstrap/_navbar.scss */ +.navbar-btn { + margin-top: 8px; + margin-bottom: 8px; } + /* line 351, ../sass/bootstrap/_navbar.scss */ + .navbar-btn.btn-sm, .btn-group-sm > .navbar-btn.btn { + margin-top: 10px; + margin-bottom: 10px; } + /* line 354, ../sass/bootstrap/_navbar.scss */ + .navbar-btn.btn-xs, .btn-group-xs > .navbar-btn.btn { + margin-top: 14px; + margin-bottom: 14px; } + +/* line 364, ../sass/bootstrap/_navbar.scss */ +.navbar-text { + margin-top: 15px; + margin-bottom: 15px; } + @media (min-width: 768px) { + /* line 364, ../sass/bootstrap/_navbar.scss */ + .navbar-text { + float: left; + margin-left: 0; + margin-right: 0; } + /* line 373, ../sass/bootstrap/_navbar.scss */ + .navbar-text.navbar-right:last-child { + margin-right: 0; } } + +/* line 383, ../sass/bootstrap/_navbar.scss */ +.navbar-default { + background-color: #222222; + border-color: transparent; } + /* line 387, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-brand { + color: white; } + /* line 390, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-brand:hover, .navbar-default .navbar-brand:focus { + color: #e6e6e6; + background-color: #008b44; } + /* line 396, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-text { + color: #777777; } + /* line 401, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-nav > li > a { + color: white; } + /* line 405, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-nav > li > a:hover, .navbar-default .navbar-nav > li > a:focus { + color: white; + background-color: #333333; } + /* line 411, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-nav > .has-dropdown:not(.active):hover > a:first-child { + color: white; + background-color: #333333; } + /* line 419, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-nav > .active > a, .navbar-default .navbar-nav > .active > a:hover, .navbar-default .navbar-nav > .active > a:focus { + color: white; + background-color: #ee5161; } + /* line 427, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-nav > .disabled > a, .navbar-default .navbar-nav > .disabled > a:hover, .navbar-default .navbar-nav > .disabled > a:focus { + color: #cccccc; + background-color: transparent; } + /* line 434, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-toggle { + border-color: #dddddd; } + /* line 437, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-toggle:hover, .navbar-default .navbar-toggle:focus { + background-color: #dddddd; } + /* line 440, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-toggle .icon-bar { + background-color: #888888; } + /* line 446, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-collapse, + .navbar-default .navbar-form { + border-color: transparent; } + /* line 456, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-nav > .open > a, .navbar-default .navbar-nav > .open > a:hover, .navbar-default .navbar-nav > .open > a:focus { + background-color: #ee5161; + color: white; } + @media (max-width: 767px) { + /* line 465, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-nav .open .dropdown-menu > li > a { + color: white; } + /* line 468, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus { + color: white; + background-color: #333333; } + /* line 476, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-nav .open .dropdown-menu > .active > a, .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus { + color: white; + background-color: #ee5161; } + /* line 484, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a, .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus { + color: #cccccc; + background-color: transparent; } } + /* line 498, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-link { + color: white; } + /* line 500, ../sass/bootstrap/_navbar.scss */ + .navbar-default .navbar-link:hover { + color: white; } + +/* line 509, ../sass/bootstrap/_navbar.scss */ +.navbar-inverse { + background-color: #4e4e4e; + border-color: transparent; } + /* line 513, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-brand { + color: white; } + /* line 516, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-brand:hover, .navbar-inverse .navbar-brand:focus { + color: white; + background-color: transparent; } + /* line 522, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-text { + color: white; } + /* line 527, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-nav > li > a { + color: white; } + /* line 531, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-nav > li > a:hover, .navbar-inverse .navbar-nav > li > a:focus { + color: #303030; + background-color: #d9d9d9; } + /* line 537, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-nav > li.has-dropdown:hover > a:first-child { + color: #303030; + background-color: #d9d9d9; } + /* line 545, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-nav > .active > a, .navbar-inverse .navbar-nav > .active > a:hover, .navbar-inverse .navbar-nav > .active > a:focus { + color: white; + background-color: #353535; } + /* line 553, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-nav > .disabled > a, .navbar-inverse .navbar-nav > .disabled > a:hover, .navbar-inverse .navbar-nav > .disabled > a:focus { + color: #444444; + background-color: transparent; } + /* line 561, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-toggle { + border-color: #333333; } + /* line 564, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-toggle:hover, .navbar-inverse .navbar-toggle:focus { + background-color: #333333; } + /* line 567, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-toggle .icon-bar { + background-color: white; } + /* line 573, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-collapse, + .navbar-inverse .navbar-form { + border-color: #3c3c3c; } + /* line 582, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-nav > .open > a, .navbar-inverse .navbar-nav > .open > a:hover, .navbar-inverse .navbar-nav > .open > a:focus { + background-color: #353535; + color: white; } + @media (max-width: 767px) { + /* line 591, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header { + border-color: transparent; } + /* line 594, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-nav .open .dropdown-menu .divider { + background-color: transparent; } + /* line 597, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a { + color: white; } + /* line 600, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover, .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus { + color: #303030; + background-color: #d9d9d9; } + /* line 608, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a, .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover, .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus { + color: white; + background-color: #353535; } + /* line 616, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a, .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover, .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus { + color: #444444; + background-color: transparent; } } + /* line 625, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-link { + color: white; } + /* line 627, ../sass/bootstrap/_navbar.scss */ + .navbar-inverse .navbar-link:hover { + color: #303030; } + +/* line 6, ../sass/bootstrap/_pager.scss */ +.pager { + padding-left: 0; + margin: 20px 0; + list-style: none; + text-align: center; } + /* line 21, ../sass/bootstrap/_mixins.scss */ + .pager:before, .pager:after { + content: " "; + display: table; } + /* line 25, ../sass/bootstrap/_mixins.scss */ + .pager:after { + clear: both; } + /* line 12, ../sass/bootstrap/_pager.scss */ + .pager li { + display: inline; } + /* line 15, ../sass/bootstrap/_pager.scss */ + .pager li > a, + .pager li > span { + display: inline-block; + padding: 5px 14px; + background-color: white; + border: 1px solid #dddddd; + border-radius: 15px; } + /* line 24, ../sass/bootstrap/_pager.scss */ + .pager li > a:hover, + .pager li > a:focus { + text-decoration: none; + background-color: #d9d9d9; } + /* line 32, ../sass/bootstrap/_pager.scss */ + .pager .next > a, + .pager .next > span { + float: right; } + /* line 39, ../sass/bootstrap/_pager.scss */ + .pager .previous > a, + .pager .previous > span { + float: left; } + /* line 48, ../sass/bootstrap/_pager.scss */ + .pager .disabled > a, + .pager .disabled > a:hover, + .pager .disabled > a:focus, + .pager .disabled > span { + color: #4e4e4e; + background-color: white; + cursor: not-allowed; } + +/* line 5, ../sass/bootstrap/_labels.scss */ +.label { + display: inline; + padding: .2em .6em .3em; + font-size: 13px; + line-height: 1; + color: #303030; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25em; + background: #eeeeee; } + /* line 20, ../sass/bootstrap/_labels.scss */ + .label[href]:hover, .label[href]:focus { + color: white; + text-decoration: none; + cursor: pointer; } + /* line 28, ../sass/bootstrap/_labels.scss */ + .label:empty { + display: none; } + /* line 33, ../sass/bootstrap/_labels.scss */ + .btn .label { + position: relative; + top: -1px; } + +/* line 42, ../sass/bootstrap/_labels.scss */ +.label-default { + background-color: #eeeeee; + color: white; } + /* line 584, ../sass/bootstrap/_mixins.scss */ + .label-default[href]:hover, .label-default[href]:focus { + background-color: #d4d4d4; } + +/* line 46, ../sass/bootstrap/_labels.scss */ +.label-primary { + background-color: white; + color: white; } + /* line 584, ../sass/bootstrap/_mixins.scss */ + .label-primary[href]:hover, .label-primary[href]:focus { + background-color: #e6e6e6; } + +/* line 50, ../sass/bootstrap/_labels.scss */ +.label-success { + background-color: #5cb85c; + color: white; } + /* line 584, ../sass/bootstrap/_mixins.scss */ + .label-success[href]:hover, .label-success[href]:focus { + background-color: #449d44; } + +/* line 54, ../sass/bootstrap/_labels.scss */ +.label-info { + background-color: #5bc0de; + color: white; } + /* line 584, ../sass/bootstrap/_mixins.scss */ + .label-info[href]:hover, .label-info[href]:focus { + background-color: #31b0d5; } + +/* line 58, ../sass/bootstrap/_labels.scss */ +.label-warning { + background-color: #f0ad4e; + color: white; } + /* line 584, ../sass/bootstrap/_mixins.scss */ + .label-warning[href]:hover, .label-warning[href]:focus { + background-color: #ec971f; } + +/* line 62, ../sass/bootstrap/_labels.scss */ +.label-danger { + background-color: #d9534f; + color: white; } + /* line 584, ../sass/bootstrap/_mixins.scss */ + .label-danger[href]:hover, .label-danger[href]:focus { + background-color: #c9302c; } + +/* line 7, ../sass/bootstrap/_badges.scss */ +.badge { + display: inline-block; + min-width: 10px; + padding: 3px 7px; + font-size: 12px; + font-weight: bold; + color: inherit; + line-height: 1; + vertical-align: baseline; + white-space: nowrap; + text-align: center; + background-color: #4e4e4e; + border-radius: 0; } + /* line 22, ../sass/bootstrap/_badges.scss */ + .badge:empty { + display: none; } + /* line 27, ../sass/bootstrap/_badges.scss */ + .btn .badge { + position: relative; + top: -1px; } + /* line 31, ../sass/bootstrap/_badges.scss */ + .btn-xs .badge, .btn-group-xs > .btn .badge { + top: 0; + padding: 1px 5px; } + +/* line 40, ../sass/bootstrap/_badges.scss */ +a.badge:hover, a.badge:focus { + color: inherit; + text-decoration: none; + cursor: pointer; } + +/* line 49, ../sass/bootstrap/_badges.scss */ +a.list-group-item.active > .badge, +.nav-pills > .active > a > .badge { + color: #4d99d8; + background-color: white; } + +/* line 53, ../sass/bootstrap/_badges.scss */ +.nav-pills > li > a > .badge { + margin-left: 3px; } + +/* line 9, ../sass/bootstrap/_alerts.scss */ +.alert { + padding: 15px; + margin-bottom: 20px; + border: 1px solid transparent; + border-radius: 0; } + /* line 16, ../sass/bootstrap/_alerts.scss */ + .alert h4 { + margin-top: 0; + color: inherit; } + /* line 22, ../sass/bootstrap/_alerts.scss */ + .alert .alert-link { + font-weight: bold; } + /* line 28, ../sass/bootstrap/_alerts.scss */ + .alert > p, + .alert > ul { + margin-bottom: 0; } + /* line 31, ../sass/bootstrap/_alerts.scss */ + .alert > p + p { + margin-top: 5px; } + +/* line 40, ../sass/bootstrap/_alerts.scss */ +.alert-dismissable { + padding-right: 35px; } + /* line 44, ../sass/bootstrap/_alerts.scss */ + .alert-dismissable .close { + position: relative; + top: -2px; + right: -21px; + color: inherit; } + +/* line 56, ../sass/bootstrap/_alerts.scss */ +.alert-success { + background-color: #dff0d8; + border-color: #d6e9c6; + color: #3c763d; } + /* line 430, ../sass/bootstrap/_mixins.scss */ + .alert-success hr { + border-top-color: #c9e2b3; } + /* line 433, ../sass/bootstrap/_mixins.scss */ + .alert-success .alert-link { + color: #2b542c; } + +/* line 59, ../sass/bootstrap/_alerts.scss */ +.alert-info { + background-color: #d9edf7; + border-color: #bce8f1; + color: #31708f; } + /* line 430, ../sass/bootstrap/_mixins.scss */ + .alert-info hr { + border-top-color: #a6e1ec; } + /* line 433, ../sass/bootstrap/_mixins.scss */ + .alert-info .alert-link { + color: #245269; } + +/* line 62, ../sass/bootstrap/_alerts.scss */ +.alert-warning { + background-color: #fcf8e3; + border-color: #faebcc; + color: #8a6d3b; } + /* line 430, ../sass/bootstrap/_mixins.scss */ + .alert-warning hr { + border-top-color: #f7e1b5; } + /* line 433, ../sass/bootstrap/_mixins.scss */ + .alert-warning .alert-link { + color: #66512c; } + +/* line 65, ../sass/bootstrap/_alerts.scss */ +.alert-danger { + background-color: #f2dede; + border-color: #ebccd1; + color: #a94442; } + /* line 430, ../sass/bootstrap/_mixins.scss */ + .alert-danger hr { + border-top-color: #e4b9c0; } + /* line 433, ../sass/bootstrap/_mixins.scss */ + .alert-danger .alert-link { + color: #843534; } + +/* line 7, ../sass/bootstrap/_panels.scss */ +.panel { + margin-bottom: 20px; + background-color: white; + border: 1px solid transparent; + border-radius: 0; + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); } + +/* line 16, ../sass/bootstrap/_panels.scss */ +.panel-body { + padding: 15px; } + /* line 21, ../sass/bootstrap/_mixins.scss */ + .panel-body:before, .panel-body:after { + content: " "; + display: table; } + /* line 25, ../sass/bootstrap/_mixins.scss */ + .panel-body:after { + clear: both; } + +/* line 22, ../sass/bootstrap/_panels.scss */ +.panel-heading { + padding: 10px 15px; + border-bottom: 1px solid transparent; + border-top-right-radius: -1; + border-top-left-radius: -1; } + /* line 27, ../sass/bootstrap/_panels.scss */ + .panel-heading > .dropdown .dropdown-toggle { + color: inherit; } + +/* line 33, ../sass/bootstrap/_panels.scss */ +.panel-title { + margin-top: 0; + margin-bottom: 0; + font-size: 16px; + color: inherit; } + /* line 39, ../sass/bootstrap/_panels.scss */ + .panel-title > a { + color: inherit; } + +/* line 45, ../sass/bootstrap/_panels.scss */ +.panel-footer { + padding: 10px 15px; + background-color: whitesmoke; + border-top: 1px solid #dddddd; + border-bottom-right-radius: -1; + border-bottom-left-radius: -1; } + +/* line 59, ../sass/bootstrap/_panels.scss */ +.panel > .list-group { + margin-bottom: 0; } + /* line 62, ../sass/bootstrap/_panels.scss */ + .panel > .list-group .list-group-item { + border-width: 1px 0; + border-radius: 0; } + /* line 69, ../sass/bootstrap/_panels.scss */ + .panel > .list-group:first-child .list-group-item:first-child { + border-top: 0; + border-top-right-radius: -1; + border-top-left-radius: -1; } + /* line 76, ../sass/bootstrap/_panels.scss */ + .panel > .list-group:last-child .list-group-item:last-child { + border-bottom: 0; + border-bottom-right-radius: -1; + border-bottom-left-radius: -1; } + +/* line 85, ../sass/bootstrap/_panels.scss */ +.panel-heading + .list-group .list-group-item:first-child { + border-top-width: 0; } + +/* line 98, ../sass/bootstrap/_panels.scss */ +.panel > .table, +.panel > .table-responsive > .table { + margin-bottom: 0; } +/* line 103, ../sass/bootstrap/_panels.scss */ +.panel > .table:first-child, +.panel > .table-responsive:first-child > .table:first-child { + border-top-right-radius: -1; + border-top-left-radius: -1; } + /* line 110, ../sass/bootstrap/_panels.scss */ + .panel > .table:first-child > thead:first-child > tr:first-child td:first-child, + .panel > .table:first-child > thead:first-child > tr:first-child th:first-child, + .panel > .table:first-child > tbody:first-child > tr:first-child td:first-child, + .panel > .table:first-child > tbody:first-child > tr:first-child th:first-child, + .panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child, + .panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child, + .panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child, + .panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child { + border-top-left-radius: -1; } + /* line 114, ../sass/bootstrap/_panels.scss */ + .panel > .table:first-child > thead:first-child > tr:first-child td:last-child, + .panel > .table:first-child > thead:first-child > tr:first-child th:last-child, + .panel > .table:first-child > tbody:first-child > tr:first-child td:last-child, + .panel > .table:first-child > tbody:first-child > tr:first-child th:last-child, + .panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child, + .panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child, + .panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child, + .panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child { + border-top-right-radius: -1; } +/* line 122, ../sass/bootstrap/_panels.scss */ +.panel > .table:last-child, +.panel > .table-responsive:last-child > .table:last-child { + border-bottom-right-radius: -1; + border-bottom-left-radius: -1; } + /* line 129, ../sass/bootstrap/_panels.scss */ + .panel > .table:last-child > tbody:last-child > tr:last-child td:first-child, + .panel > .table:last-child > tbody:last-child > tr:last-child th:first-child, + .panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child, + .panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child, + .panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child, + .panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child, + .panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child, + .panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child { + border-bottom-left-radius: -1; } + /* line 133, ../sass/bootstrap/_panels.scss */ + .panel > .table:last-child > tbody:last-child > tr:last-child td:last-child, + .panel > .table:last-child > tbody:last-child > tr:last-child th:last-child, + .panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child, + .panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child, + .panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child, + .panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child, + .panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child, + .panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child { + border-bottom-right-radius: -1; } +/* line 140, ../sass/bootstrap/_panels.scss */ +.panel > .panel-body + .table, +.panel > .panel-body + .table-responsive { + border-top: 1px solid #dddddd; } +/* line 144, ../sass/bootstrap/_panels.scss */ +.panel > .table > tbody:first-child > tr:first-child th, +.panel > .table > tbody:first-child > tr:first-child td { + border-top: 0; } +/* line 148, ../sass/bootstrap/_panels.scss */ +.panel > .table-bordered, +.panel > .table-responsive > .table-bordered { + border: 0; } + /* line 155, ../sass/bootstrap/_panels.scss */ + .panel > .table-bordered > thead > tr > th:first-child, + .panel > .table-bordered > thead > tr > td:first-child, + .panel > .table-bordered > tbody > tr > th:first-child, + .panel > .table-bordered > tbody > tr > td:first-child, + .panel > .table-bordered > tfoot > tr > th:first-child, + .panel > .table-bordered > tfoot > tr > td:first-child, + .panel > .table-responsive > .table-bordered > thead > tr > th:first-child, + .panel > .table-responsive > .table-bordered > thead > tr > td:first-child, + .panel > .table-responsive > .table-bordered > tbody > tr > th:first-child, + .panel > .table-responsive > .table-bordered > tbody > tr > td:first-child, + .panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child, + .panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child { + border-left: 0; } + /* line 159, ../sass/bootstrap/_panels.scss */ + .panel > .table-bordered > thead > tr > th:last-child, + .panel > .table-bordered > thead > tr > td:last-child, + .panel > .table-bordered > tbody > tr > th:last-child, + .panel > .table-bordered > tbody > tr > td:last-child, + .panel > .table-bordered > tfoot > tr > th:last-child, + .panel > .table-bordered > tfoot > tr > td:last-child, + .panel > .table-responsive > .table-bordered > thead > tr > th:last-child, + .panel > .table-responsive > .table-bordered > thead > tr > td:last-child, + .panel > .table-responsive > .table-bordered > tbody > tr > th:last-child, + .panel > .table-responsive > .table-bordered > tbody > tr > td:last-child, + .panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child, + .panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child { + border-right: 0; } + /* line 168, ../sass/bootstrap/_panels.scss */ + .panel > .table-bordered > thead > tr:first-child > td, + .panel > .table-bordered > thead > tr:first-child > th, + .panel > .table-bordered > tbody > tr:first-child > td, + .panel > .table-bordered > tbody > tr:first-child > th, + .panel > .table-responsive > .table-bordered > thead > tr:first-child > td, + .panel > .table-responsive > .table-bordered > thead > tr:first-child > th, + .panel > .table-responsive > .table-bordered > tbody > tr:first-child > td, + .panel > .table-responsive > .table-bordered > tbody > tr:first-child > th { + border-bottom: 0; } + /* line 177, ../sass/bootstrap/_panels.scss */ + .panel > .table-bordered > tbody > tr:last-child > td, + .panel > .table-bordered > tbody > tr:last-child > th, + .panel > .table-bordered > tfoot > tr:last-child > td, + .panel > .table-bordered > tfoot > tr:last-child > th, + .panel > .table-responsive > .table-bordered > tbody > tr:last-child > td, + .panel > .table-responsive > .table-bordered > tbody > tr:last-child > th, + .panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td, + .panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th { + border-bottom: 0; } +/* line 183, ../sass/bootstrap/_panels.scss */ +.panel > .table-responsive { + border: 0; + margin-bottom: 0; } + +/* line 195, ../sass/bootstrap/_panels.scss */ +.panel-group { + margin-bottom: 20px; } + /* line 199, ../sass/bootstrap/_panels.scss */ + .panel-group .panel { + margin-bottom: 0; + border-radius: 0; + overflow: hidden; } + /* line 203, ../sass/bootstrap/_panels.scss */ + .panel-group .panel + .panel { + margin-top: 5px; } + /* line 208, ../sass/bootstrap/_panels.scss */ + .panel-group .panel-heading { + border-bottom: 0; } + /* line 210, ../sass/bootstrap/_panels.scss */ + .panel-group .panel-heading + .panel-collapse .panel-body { + border-top: 1px solid #dddddd; } + /* line 214, ../sass/bootstrap/_panels.scss */ + .panel-group .panel-footer { + border-top: 0; } + /* line 216, ../sass/bootstrap/_panels.scss */ + .panel-group .panel-footer + .panel-collapse .panel-body { + border-bottom: 1px solid #dddddd; } + +/* line 224, ../sass/bootstrap/_panels.scss */ +.panel-default { + border-color: #dddddd; } + /* line 407, ../sass/bootstrap/_mixins.scss */ + .panel-default > .panel-heading { + color: #303030; + background-color: whitesmoke; + border-color: #dddddd; } + /* line 412, ../sass/bootstrap/_mixins.scss */ + .panel-default > .panel-heading + .panel-collapse .panel-body { + border-top-color: #dddddd; } + /* line 417, ../sass/bootstrap/_mixins.scss */ + .panel-default > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: #dddddd; } + +/* line 227, ../sass/bootstrap/_panels.scss */ +.panel-primary { + border-color: white; } + /* line 407, ../sass/bootstrap/_mixins.scss */ + .panel-primary > .panel-heading { + color: white; + background-color: white; + border-color: white; } + /* line 412, ../sass/bootstrap/_mixins.scss */ + .panel-primary > .panel-heading + .panel-collapse .panel-body { + border-top-color: white; } + /* line 417, ../sass/bootstrap/_mixins.scss */ + .panel-primary > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: white; } + +/* line 230, ../sass/bootstrap/_panels.scss */ +.panel-success { + border-color: #d6e9c6; } + /* line 407, ../sass/bootstrap/_mixins.scss */ + .panel-success > .panel-heading { + color: #3c763d; + background-color: #dff0d8; + border-color: #d6e9c6; } + /* line 412, ../sass/bootstrap/_mixins.scss */ + .panel-success > .panel-heading + .panel-collapse .panel-body { + border-top-color: #d6e9c6; } + /* line 417, ../sass/bootstrap/_mixins.scss */ + .panel-success > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: #d6e9c6; } + +/* line 233, ../sass/bootstrap/_panels.scss */ +.panel-info { + border-color: #bce8f1; } + /* line 407, ../sass/bootstrap/_mixins.scss */ + .panel-info > .panel-heading { + color: #31708f; + background-color: #d9edf7; + border-color: #bce8f1; } + /* line 412, ../sass/bootstrap/_mixins.scss */ + .panel-info > .panel-heading + .panel-collapse .panel-body { + border-top-color: #bce8f1; } + /* line 417, ../sass/bootstrap/_mixins.scss */ + .panel-info > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: #bce8f1; } + +/* line 236, ../sass/bootstrap/_panels.scss */ +.panel-warning { + border-color: #faebcc; } + /* line 407, ../sass/bootstrap/_mixins.scss */ + .panel-warning > .panel-heading { + color: #8a6d3b; + background-color: #fcf8e3; + border-color: #faebcc; } + /* line 412, ../sass/bootstrap/_mixins.scss */ + .panel-warning > .panel-heading + .panel-collapse .panel-body { + border-top-color: #faebcc; } + /* line 417, ../sass/bootstrap/_mixins.scss */ + .panel-warning > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: #faebcc; } + +/* line 239, ../sass/bootstrap/_panels.scss */ +.panel-danger { + border-color: #ebccd1; } + /* line 407, ../sass/bootstrap/_mixins.scss */ + .panel-danger > .panel-heading { + color: #a94442; + background-color: #f2dede; + border-color: #ebccd1; } + /* line 412, ../sass/bootstrap/_mixins.scss */ + .panel-danger > .panel-heading + .panel-collapse .panel-body { + border-top-color: #ebccd1; } + /* line 417, ../sass/bootstrap/_mixins.scss */ + .panel-danger > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: #ebccd1; } + +/* line 7, ../sass/bootstrap/_wells.scss */ +.well { + min-height: 20px; + padding: 0; + margin-bottom: 20px; + background-color: inherit; + border: 1px solid inherit; + border-radius: 0; } + /* line 14, ../sass/bootstrap/_wells.scss */ + .well blockquote { + border-color: #ddd; + border-color: rgba(0, 0, 0, 0.15); } + +/* line 21, ../sass/bootstrap/_wells.scss */ +.well-lg { + padding: 24px; + border-radius: 6px; } + +/* line 25, ../sass/bootstrap/_wells.scss */ +.well-sm { + padding: 9px; + border-radius: 3px; } + +/* line 6, ../sass/bootstrap/_close.scss */ +.close { + float: right; + font-size: 21px; + font-weight: bold; + line-height: 1; + color: black; + text-shadow: 0 1px 0 white; + opacity: 0.2; + filter: alpha(opacity=20); } + /* line 16, ../sass/bootstrap/_close.scss */ + .close:hover, .close:focus { + color: black; + text-decoration: none; + cursor: pointer; + opacity: 0.5; + filter: alpha(opacity=50); } + +/* line 29, ../sass/bootstrap/_close.scss */ +button.close { + padding: 0; + cursor: pointer; + background: transparent; + border: 0; + -webkit-appearance: none; } + +/* line 11, ../sass/bootstrap/_modals.scss */ +.modal-open { + overflow: hidden; } + +/* line 16, ../sass/bootstrap/_modals.scss */ +.modal { + display: none; + overflow: auto; + overflow-y: scroll; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1050; + -webkit-overflow-scrolling: touch; + outline: 0; } + /* line 33, ../sass/bootstrap/_modals.scss */ + .modal.fade .modal-dialog { + -webkit-transform: translate(0, -25%); + -ms-transform: translate(0, -25%); + transform: translate(0, -25%); + -webkit-transition: -webkit-transform 0.3s ease-out; + -moz-transition: -moz-transform 0.3s ease-out; + -o-transition: -o-transform 0.3s ease-out; + transition: transform 0.3s ease-out; } + /* line 37, ../sass/bootstrap/_modals.scss */ + .modal.in .modal-dialog { + -webkit-transform: translate(0, 0); + -ms-transform: translate(0, 0); + transform: translate(0, 0); } + +/* line 41, ../sass/bootstrap/_modals.scss */ +.modal-dialog { + position: relative; + width: auto; + margin: 10px; } + +/* line 48, ../sass/bootstrap/_modals.scss */ +.modal-content { + position: relative; + background-color: white; + border: 1px solid #999999; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 0; + -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); + box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); + background-clip: padding-box; + outline: none; } + +/* line 61, ../sass/bootstrap/_modals.scss */ +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + background-color: black; } + /* line 70, ../sass/bootstrap/_modals.scss */ + .modal-backdrop.fade { + opacity: 0; + filter: alpha(opacity=0); } + /* line 71, ../sass/bootstrap/_modals.scss */ + .modal-backdrop.in { + opacity: 0.5; + filter: alpha(opacity=50); } + +/* line 76, ../sass/bootstrap/_modals.scss */ +.modal-header { + padding: 15px; + border-bottom: 1px solid transparent; + min-height: 16.42857px; } + +/* line 82, ../sass/bootstrap/_modals.scss */ +.modal-header .close { + margin-top: -2px; } + +/* line 87, ../sass/bootstrap/_modals.scss */ +.modal-title { + margin: 0; + line-height: 1.42857; } + +/* line 94, ../sass/bootstrap/_modals.scss */ +.modal-body { + position: relative; + padding: 20px; } + +/* line 100, ../sass/bootstrap/_modals.scss */ +.modal-footer { + margin-top: 15px; + padding: 19px 20px 20px; + text-align: right; + border-top: 1px solid transparent; } + /* line 21, ../sass/bootstrap/_mixins.scss */ + .modal-footer:before, .modal-footer:after { + content: " "; + display: table; } + /* line 25, ../sass/bootstrap/_mixins.scss */ + .modal-footer:after { + clear: both; } + /* line 108, ../sass/bootstrap/_modals.scss */ + .modal-footer .btn + .btn { + margin-left: 5px; + margin-bottom: 0; } + /* line 113, ../sass/bootstrap/_modals.scss */ + .modal-footer .btn-group .btn + .btn { + margin-left: -1px; } + /* line 117, ../sass/bootstrap/_modals.scss */ + .modal-footer .btn-block + .btn-block { + margin-left: 0; } + +@media (min-width: 768px) { + /* line 125, ../sass/bootstrap/_modals.scss */ + .modal-dialog { + width: 760px; + margin: 30px auto; } + + /* line 129, ../sass/bootstrap/_modals.scss */ + .modal-content { + -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); } + + /* line 134, ../sass/bootstrap/_modals.scss */ + .modal-sm { + width: 300px; } } +@media (min-width: 992px) { + /* line 138, ../sass/bootstrap/_modals.scss */ + .modal-lg { + width: 900px; } } +/* line 7, ../sass/bootstrap/_tooltip.scss */ +.tooltip { + position: absolute; + z-index: 1030; + display: block; + visibility: visible; + font-size: 12px; + line-height: 1.4; + opacity: 0; + filter: alpha(opacity=0); } + /* line 16, ../sass/bootstrap/_tooltip.scss */ + .tooltip.in { + opacity: 0.9; + filter: alpha(opacity=90); } + /* line 17, ../sass/bootstrap/_tooltip.scss */ + .tooltip.top { + margin-top: -3px; + padding: 5px 0; } + /* line 18, ../sass/bootstrap/_tooltip.scss */ + .tooltip.right { + margin-left: 3px; + padding: 0 5px; } + /* line 19, ../sass/bootstrap/_tooltip.scss */ + .tooltip.bottom { + margin-top: 3px; + padding: 5px 0; } + /* line 20, ../sass/bootstrap/_tooltip.scss */ + .tooltip.left { + margin-left: -3px; + padding: 0 5px; } + +/* line 24, ../sass/bootstrap/_tooltip.scss */ +.tooltip-inner { + max-width: 200px; + padding: 3px 8px; + color: white; + text-align: center; + text-decoration: none; + background-color: black; + border-radius: 0; } + +/* line 35, ../sass/bootstrap/_tooltip.scss */ +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; } + +/* line 43, ../sass/bootstrap/_tooltip.scss */ +.tooltip.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-width: 5px 5px 0; + border-top-color: black; } +/* line 50, ../sass/bootstrap/_tooltip.scss */ +.tooltip.top-left .tooltip-arrow { + bottom: 0; + left: 5px; + border-width: 5px 5px 0; + border-top-color: black; } +/* line 56, ../sass/bootstrap/_tooltip.scss */ +.tooltip.top-right .tooltip-arrow { + bottom: 0; + right: 5px; + border-width: 5px 5px 0; + border-top-color: black; } +/* line 62, ../sass/bootstrap/_tooltip.scss */ +.tooltip.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-width: 5px 5px 5px 0; + border-right-color: black; } +/* line 69, ../sass/bootstrap/_tooltip.scss */ +.tooltip.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-width: 5px 0 5px 5px; + border-left-color: black; } +/* line 76, ../sass/bootstrap/_tooltip.scss */ +.tooltip.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-width: 0 5px 5px; + border-bottom-color: black; } +/* line 83, ../sass/bootstrap/_tooltip.scss */ +.tooltip.bottom-left .tooltip-arrow { + top: 0; + left: 5px; + border-width: 0 5px 5px; + border-bottom-color: black; } +/* line 89, ../sass/bootstrap/_tooltip.scss */ +.tooltip.bottom-right .tooltip-arrow { + top: 0; + right: 5px; + border-width: 0 5px 5px; + border-bottom-color: black; } + +/* line 6, ../sass/bootstrap/_popovers.scss */ +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1010; + display: none; + max-width: 276px; + padding: 1px; + text-align: left; + background-color: white; + background-clip: padding-box; + border: 1px solid #cccccc; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 3px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + white-space: normal; } + /* line 26, ../sass/bootstrap/_popovers.scss */ + .popover.top { + margin-top: -10px; } + /* line 27, ../sass/bootstrap/_popovers.scss */ + .popover.right { + margin-left: 10px; } + /* line 28, ../sass/bootstrap/_popovers.scss */ + .popover.bottom { + margin-top: 10px; } + /* line 29, ../sass/bootstrap/_popovers.scss */ + .popover.left { + margin-left: -10px; } + +/* line 32, ../sass/bootstrap/_popovers.scss */ +.popover-title { + margin: 0; + padding: 8px 14px; + font-size: 14px; + font-weight: normal; + line-height: 18px; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + border-radius: 3px 3px 0 0; } + +/* line 43, ../sass/bootstrap/_popovers.scss */ +.popover-content { + padding: 5px; } + +/* line 53, ../sass/bootstrap/_popovers.scss */ +.popover > .arrow, .popover > .arrow:after { + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; } + +/* line 62, ../sass/bootstrap/_popovers.scss */ +.popover > .arrow { + border-width: 11px; } + +/* line 65, ../sass/bootstrap/_popovers.scss */ +.popover > .arrow:after { + border-width: 10px; + content: ""; } + +/* line 71, ../sass/bootstrap/_popovers.scss */ +.popover.top > .arrow { + left: 50%; + margin-left: -11px; + border-bottom-width: 0; + border-top-color: #999999; + border-top-color: fadein(rgba(0, 0, 0, 0.2), 5%); + bottom: -11px; } + /* line 78, ../sass/bootstrap/_popovers.scss */ + .popover.top > .arrow:after { + content: " "; + bottom: 1px; + margin-left: -10px; + border-bottom-width: 0; + border-top-color: white; } +/* line 86, ../sass/bootstrap/_popovers.scss */ +.popover.right > .arrow { + top: 50%; + left: -11px; + margin-top: -11px; + border-left-width: 0; + border-right-color: #999999; + border-right-color: fadein(rgba(0, 0, 0, 0.2), 5%); } + /* line 93, ../sass/bootstrap/_popovers.scss */ + .popover.right > .arrow:after { + content: " "; + left: 1px; + bottom: -10px; + border-left-width: 0; + border-right-color: white; } +/* line 101, ../sass/bootstrap/_popovers.scss */ +.popover.bottom > .arrow { + left: 50%; + margin-left: -11px; + border-top-width: 0; + border-bottom-color: #999999; + border-bottom-color: fadein(rgba(0, 0, 0, 0.2), 5%); + top: -11px; } + /* line 108, ../sass/bootstrap/_popovers.scss */ + .popover.bottom > .arrow:after { + content: " "; + top: 1px; + margin-left: -10px; + border-top-width: 0; + border-bottom-color: white; } +/* line 117, ../sass/bootstrap/_popovers.scss */ +.popover.left > .arrow { + top: 50%; + right: -11px; + margin-top: -11px; + border-right-width: 0; + border-left-color: #999999; + border-left-color: fadein(rgba(0, 0, 0, 0.2), 5%); } + /* line 124, ../sass/bootstrap/_popovers.scss */ + .popover.left > .arrow:after { + content: " "; + right: 1px; + border-right-width: 0; + border-left-color: white; + bottom: -10px; } + +/* line 21, ../sass/bootstrap/_mixins.scss */ +.clearfix:before, .clearfix:after { + content: " "; + display: table; } +/* line 25, ../sass/bootstrap/_mixins.scss */ +.clearfix:after { + clear: both; } + +/* line 12, ../sass/bootstrap/_utilities.scss */ +.center-block { + display: block; + margin-left: auto; + margin-right: auto; } + +/* line 15, ../sass/bootstrap/_utilities.scss */ +.pull-right { + float: right !important; } + +/* line 18, ../sass/bootstrap/_utilities.scss */ +.pull-left { + float: left !important; } + +/* line 27, ../sass/bootstrap/_utilities.scss */ +.hide { + display: none !important; } + +/* line 30, ../sass/bootstrap/_utilities.scss */ +.show { + display: block !important; } + +/* line 33, ../sass/bootstrap/_utilities.scss */ +.invisible { + visibility: hidden; } + +/* line 36, ../sass/bootstrap/_utilities.scss */ +.text-hide { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; } + +/* line 45, ../sass/bootstrap/_utilities.scss */ +.hidden { + display: none !important; + visibility: hidden !important; } + +/* line 54, ../sass/bootstrap/_utilities.scss */ +.affix { + position: fixed; } + +@-ms-viewport { + width: device-width; } + +/* line 648, ../sass/bootstrap/_mixins.scss */ +.visible-xs, .visible-sm, .visible-md, .visible-lg { + display: none !important; } + +@media (max-width: 767px) { + /* line 637, ../sass/bootstrap/_mixins.scss */ + .visible-xs { + display: block !important; } + + /* line 640, ../sass/bootstrap/_mixins.scss */ + table.visible-xs { + display: table; } + + /* line 641, ../sass/bootstrap/_mixins.scss */ + tr.visible-xs { + display: table-row !important; } + + /* line 643, ../sass/bootstrap/_mixins.scss */ + th.visible-xs, + td.visible-xs { + display: table-cell !important; } } +@media (min-width: 768px) and (max-width: 991px) { + /* line 637, ../sass/bootstrap/_mixins.scss */ + .visible-sm { + display: block !important; } + + /* line 640, ../sass/bootstrap/_mixins.scss */ + table.visible-sm { + display: table; } + + /* line 641, ../sass/bootstrap/_mixins.scss */ + tr.visible-sm { + display: table-row !important; } + + /* line 643, ../sass/bootstrap/_mixins.scss */ + th.visible-sm, + td.visible-sm { + display: table-cell !important; } } +@media (min-width: 992px) and (max-width: 1199px) { + /* line 637, ../sass/bootstrap/_mixins.scss */ + .visible-md { + display: block !important; } + + /* line 640, ../sass/bootstrap/_mixins.scss */ + table.visible-md { + display: table; } + + /* line 641, ../sass/bootstrap/_mixins.scss */ + tr.visible-md { + display: table-row !important; } + + /* line 643, ../sass/bootstrap/_mixins.scss */ + th.visible-md, + td.visible-md { + display: table-cell !important; } } +@media (min-width: 1200px) { + /* line 637, ../sass/bootstrap/_mixins.scss */ + .visible-lg { + display: block !important; } + + /* line 640, ../sass/bootstrap/_mixins.scss */ + table.visible-lg { + display: table; } + + /* line 641, ../sass/bootstrap/_mixins.scss */ + tr.visible-lg { + display: table-row !important; } + + /* line 643, ../sass/bootstrap/_mixins.scss */ + th.visible-lg, + td.visible-lg { + display: table-cell !important; } } +@media (max-width: 767px) { + /* line 648, ../sass/bootstrap/_mixins.scss */ + .hidden-xs { + display: none !important; } } +@media (min-width: 768px) and (max-width: 991px) { + /* line 648, ../sass/bootstrap/_mixins.scss */ + .hidden-sm { + display: none !important; } } +@media (min-width: 992px) and (max-width: 1199px) { + /* line 648, ../sass/bootstrap/_mixins.scss */ + .hidden-md { + display: none !important; } } +@media (min-width: 1200px) { + /* line 648, ../sass/bootstrap/_mixins.scss */ + .hidden-lg { + display: none !important; } } +/* line 648, ../sass/bootstrap/_mixins.scss */ +.visible-print { + display: none !important; } + +@media print { + /* line 637, ../sass/bootstrap/_mixins.scss */ + .visible-print { + display: block !important; } + + /* line 640, ../sass/bootstrap/_mixins.scss */ + table.visible-print { + display: table; } + + /* line 641, ../sass/bootstrap/_mixins.scss */ + tr.visible-print { + display: table-row !important; } + + /* line 643, ../sass/bootstrap/_mixins.scss */ + th.visible-print, + td.visible-print { + display: table-cell !important; } } +@media print { + /* line 648, ../sass/bootstrap/_mixins.scss */ + .hidden-print { + display: none !important; } } +/* line 1, ../sass/_loaders.scss */ +.spinner { + text-align: center; } + +/* line 5, ../sass/_loaders.scss */ +.spinner > div { + width: 8px; + height: 8px; + background-color: white; + border-radius: 100%; + display: inline-block; + -webkit-animation: bouncedelay 1.4s infinite ease-in-out; + animation: bouncedelay 1.4s infinite ease-in-out; + /* Prevent first frame from flickering when animation starts */ + -webkit-animation-fill-mode: both; + animation-fill-mode: both; } + +/* line 19, ../sass/_loaders.scss */ +.spinner .bounce1 { + -webkit-animation-delay: -0.32s; + animation-delay: -0.32s; } + +/* line 24, ../sass/_loaders.scss */ +.spinner .bounce2 { + -webkit-animation-delay: -0.16s; + animation-delay: -0.16s; } + +@-webkit-keyframes bouncedelay { + /* line 30, ../sass/_loaders.scss */ + 0%, 80%, 100% { + -webkit-transform: scale(0); } + + /* line 31, ../sass/_loaders.scss */ + 40% { + -webkit-transform: scale(1); } } + +@keyframes bouncedelay { + /* line 35, ../sass/_loaders.scss */ + 0%, 80%, 100% { + transform: scale(0); + -webkit-transform: scale(0); } + + /* line 38, ../sass/_loaders.scss */ + 40% { + transform: scale(1); + -webkit-transform: scale(1); } } + +/* +Disabled buttons are transparent with light gray border +and light gray font colors +*/ +/* line 116, ../sass/_bars-btns.scss */ +.line-btn { + display: inline-block; + text-align: center; + opacity: 1; + background-color: #222222; + border-bottom: 2px solid #222222; + color: white; } + /* line 29, ../sass/_bars-btns.scss */ + .line-btn span { + display: inline-block; + height: 100%; + line-height: 100%; + padding: 8px; } + /* line 36, ../sass/_bars-btns.scss */ + .line-btn:hover, .line-btn:focus { + text-decoration: none; + opacity: 0.85; } + /* line 45, ../sass/_bars-btns.scss */ + .line-btn .snf-font-remove { + display: inline; } + /* line 48, ../sass/_bars-btns.scss */ + .line-btn.disabled { + background: transparent !important; + border-color: #818181 !important; + color: #818181 !important; } + /* line 15, ../sass/_bars-btns.scss */ + .line-btn.disabled:hover, .line-btn.disabled:focus { + cursor: default; + opacity: 1; } + /* line 18, ../sass/_bars-btns.scss */ + .line-btn.disabled:hover span, .line-btn.disabled:focus span { + color: #818181 !important; } + /* line 109, ../sass/_bars-btns.scss */ + .line-btn:hover, .line-btn:focus { + opacity: 1; + border-bottom-color: white; + color: white; } + +/* line 120, ../sass/_bars-btns.scss */ +.outline-btn { + display: inline-block; + text-align: center; + opacity: 1; + border: 1px solid white; + color: white; } + /* line 29, ../sass/_bars-btns.scss */ + .outline-btn span { + display: inline-block; + height: 100%; + line-height: 100%; + padding: 8px; } + /* line 36, ../sass/_bars-btns.scss */ + .outline-btn:hover, .outline-btn:focus { + text-decoration: none; + opacity: 0.85; } + /* line 45, ../sass/_bars-btns.scss */ + .outline-btn .snf-font-remove { + display: inline; } + /* line 48, ../sass/_bars-btns.scss */ + .outline-btn.disabled { + background: transparent !important; + border-color: #818181 !important; + color: #818181 !important; } + /* line 15, ../sass/_bars-btns.scss */ + .outline-btn.disabled:hover, .outline-btn.disabled:focus { + cursor: default; + opacity: 1; } + /* line 18, ../sass/_bars-btns.scss */ + .outline-btn.disabled:hover span, .outline-btn.disabled:focus span { + color: #818181 !important; } + /* line 59, ../sass/_bars-btns.scss */ + .outline-btn span { + border: 1px solid transparent; + width: 100%; } + /* line 65, ../sass/_bars-btns.scss */ + .outline-btn:hover span, .outline-btn:focus span { + border-color: white; } + /* line 69, ../sass/_bars-btns.scss */ + .outline-btn.disabled { + @inlcude disabled; + color: #818181; } + /* line 74, ../sass/_bars-btns.scss */ + .outline-btn.disabled:hover span, .outline-btn.disabled:focus span { + border-color: transparent; } + +/* line 124, ../sass/_bars-btns.scss */ +.custom-btn { + display: inline-block; + text-align: center; + opacity: 1; + border: 1px solid #3c96e0; + color: white; + background-color: #3c96e0; } + /* line 29, ../sass/_bars-btns.scss */ + .custom-btn span { + display: inline-block; + height: 100%; + line-height: 100%; + padding: 8px; } + /* line 36, ../sass/_bars-btns.scss */ + .custom-btn:hover, .custom-btn:focus { + text-decoration: none; + opacity: 0.85; } + /* line 45, ../sass/_bars-btns.scss */ + .custom-btn .snf-font-remove { + display: inline; } + /* line 48, ../sass/_bars-btns.scss */ + .custom-btn.disabled { + background: transparent !important; + border-color: #818181 !important; + color: #818181 !important; } + /* line 15, ../sass/_bars-btns.scss */ + .custom-btn.disabled:hover, .custom-btn.disabled:focus { + cursor: default; + opacity: 1; } + /* line 18, ../sass/_bars-btns.scss */ + .custom-btn.disabled:hover span, .custom-btn.disabled:focus span { + color: #818181 !important; } + /* line 87, ../sass/_bars-btns.scss */ + .custom-btn span { + border: 1px solid transparent; + background: transparent; } + /* line 93, ../sass/_bars-btns.scss */ + .custom-btn:hover span, .custom-btn:focus span { + color: white; } + /* line 97, ../sass/_bars-btns.scss */ + .custom-btn.disabled { + background: transparent !important; + border-color: #818181 !important; + color: #818181 !important; } + /* line 15, ../sass/_bars-btns.scss */ + .custom-btn.disabled:hover, .custom-btn.disabled:focus { + cursor: default; + opacity: 1; } + /* line 18, ../sass/_bars-btns.scss */ + .custom-btn.disabled:hover span, .custom-btn.disabled:focus span { + color: #818181 !important; } + /* line 126, ../sass/_bars-btns.scss */ + .custom-btn[data-karma="neutral"] { + background-color: #3c96e0; + border-color: #3c96e0; } + /* line 130, ../sass/_bars-btns.scss */ + .custom-btn[data-karma="good"] { + background-color: #00a551; + border-color: #00a551; } + /* line 135, ../sass/_bars-btns.scss */ + .custom-btn[data-karma="bad"] { + background-color: #d2881f; + border-color: #d2881f; } + /* line 140, ../sass/_bars-btns.scss */ + .custom-btn[data-caution="warning"][data-karma="good"], .custom-btn[data-caution="warning"][data-karma="neutral"] { + background-color: #d2881f; + border-color: #d2881f; } + /* line 145, ../sass/_bars-btns.scss */ + .custom-btn[data-caution="dangerous"][data-karma="bad"], .custom-btn[data-caution="dangerous"][data-karma="neutral"] { + background-color: #e42a48; + border-color: #e42a48; } + +/* line 151, ../sass/_bars-btns.scss */ +.search-btn { + display: inline-block; + text-align: center; + opacity: 1; + background-color: #222222; + border-bottom: 2px solid #222222; + color: white; + position: relative; + top: -2px; + margin-left: 20px; + cursor: pointer; } + /* line 29, ../sass/_bars-btns.scss */ + .search-btn span { + display: inline-block; + height: 100%; + line-height: 100%; + padding: 8px; } + /* line 36, ../sass/_bars-btns.scss */ + .search-btn:hover, .search-btn:focus { + text-decoration: none; + opacity: 0.85; } + /* line 45, ../sass/_bars-btns.scss */ + .search-btn .snf-font-remove { + display: inline; } + /* line 48, ../sass/_bars-btns.scss */ + .search-btn.disabled { + background: transparent !important; + border-color: #818181 !important; + color: #818181 !important; } + /* line 15, ../sass/_bars-btns.scss */ + .search-btn.disabled:hover, .search-btn.disabled:focus { + cursor: default; + opacity: 1; } + /* line 18, ../sass/_bars-btns.scss */ + .search-btn.disabled:hover span, .search-btn.disabled:focus span { + color: #818181 !important; } + /* line 109, ../sass/_bars-btns.scss */ + .search-btn:hover, .search-btn:focus { + opacity: 1; + border-bottom-color: white; + color: white; } + /* line 156, ../sass/_bars-btns.scss */ + .search-btn span { + padding: 7px; } + +/* line 162, ../sass/_bars-btns.scss */ +.search-mode-btn { + float: right; + line-height: 30px; } + /* line 165, ../sass/_bars-btns.scss */ + .search-mode-btn:hover { + cursor: pointer; } + +/* line 170, ../sass/_bars-btns.scss */ +.instructions .line-btn { + padding: 8px 10px; } + /* line 172, ../sass/_bars-btns.scss */ + .instructions .line-btn span { + padding: 0 4px; } + /* line 176, ../sass/_bars-btns.scss */ + .instructions .line-btn:hover .arrow { + font-weight: bold; } + /* line 180, ../sass/_bars-btns.scss */ + .instructions .line-btn.open:hover { + border-bottom-color: transparent; } + /* line 183, ../sass/_bars-btns.scss */ + .instructions .line-btn .arrow { + vertical-align: middle; } + +/* Sidebar */ +/* line 193, ../sass/_bars-btns.scss */ +.sidebar { + margin: 0 30px 0 0; + width: 110px; + height: auto; + float: left; } + /* line 198, ../sass/_bars-btns.scss */ + .sidebar .btn-group-vertical { + width: 100%; } + @media (max-width: 1200px) { + /* line 193, ../sass/_bars-btns.scss */ + .sidebar { + width: auto; + margin: 20px auto; + float: none; } + /* line 206, ../sass/_bars-btns.scss */ + .sidebar .btn-group-vertical a { + margin-right: 10px; + display: inline-block; } } + +/* +Positioning or customizing buttons +*/ +/* line 219, ../sass/_bars-btns.scss */ +.sidebar .custom-btn { + display: block; + margin: 0 0 1em; } + /* line 222, ../sass/_bars-btns.scss */ + .sidebar .custom-btn span { + padding: 8px; } + +/* line 228, ../sass/_bars-btns.scss */ +body .custom-buttons { + float: left; + margin-right: 10px; } + /* line 231, ../sass/_bars-btns.scss */ + body .custom-buttons .line-btn { + margin-right: 1em; } + /* line 234, ../sass/_bars-btns.scss */ + body .custom-buttons .disabled { + display: none; } + +/* +Extra-button is used to show total selected rows +*/ +/* line 251, ../sass/_bars-btns.scss */ +body .custom-buttons .extra-btn { + float: right; + margin-right: 0; } + /* line 254, ../sass/_bars-btns.scss */ + body .custom-buttons .extra-btn span { + display: inline-block; } + /* line 257, ../sass/_bars-btns.scss */ + body .custom-buttons .extra-btn .badge { + background: transparent; + line-height: 0.8; + display: inline; + padding: 0 5px 0 0; + font-weight: normal; + font-size: 1em; } + /* line 264, ../sass/_bars-btns.scss */ + body .custom-buttons .extra-btn .badge::before { + content: "("; } + /* line 267, ../sass/_bars-btns.scss */ + body .custom-buttons .extra-btn .badge::after { + content: ")"; } + +/* line 273, ../sass/_bars-btns.scss */ +.show-hide-all { + float: right; } + /* line 275, ../sass/_bars-btns.scss */ + .show-hide-all em { + font-style: normal; } + /* line 278, ../sass/_bars-btns.scss */ + .show-hide-all.line-btn { + padding: 8px; } + /* line 280, ../sass/_bars-btns.scss */ + .show-hide-all.line-btn span { + display: inline; } + +/* line 287, ../sass/_bars-btns.scss */ +.actions-per-item .custom-btn { + margin: 10px 10px 10px 0; } + +/* line 292, ../sass/_bars-btns.scss */ +.charts .chart { + display: none; } +/* line 296, ../sass/_bars-btns.scss */ +.charts .sidebar a { + display: inline-block; + text-align: center; + opacity: 1; + border: 1px solid white; + color: white; + display: block; + margin: 20px auto; } + /* line 29, ../sass/_bars-btns.scss */ + .charts .sidebar a span { + display: inline-block; + height: 100%; + line-height: 100%; + padding: 8px; } + /* line 36, ../sass/_bars-btns.scss */ + .charts .sidebar a:hover, .charts .sidebar a:focus { + text-decoration: none; + opacity: 0.85; } + /* line 45, ../sass/_bars-btns.scss */ + .charts .sidebar a .snf-font-remove { + display: inline; } + /* line 48, ../sass/_bars-btns.scss */ + .charts .sidebar a.disabled { + background: transparent !important; + border-color: #818181 !important; + color: #818181 !important; } + /* line 15, ../sass/_bars-btns.scss */ + .charts .sidebar a.disabled:hover, .charts .sidebar a.disabled:focus { + cursor: default; + opacity: 1; } + /* line 18, ../sass/_bars-btns.scss */ + .charts .sidebar a.disabled:hover span, .charts .sidebar a.disabled:focus span { + color: #818181 !important; } + /* line 59, ../sass/_bars-btns.scss */ + .charts .sidebar a span { + border: 1px solid transparent; + width: 100%; } + /* line 65, ../sass/_bars-btns.scss */ + .charts .sidebar a:hover span, .charts .sidebar a:focus span { + border-color: white; } + /* line 69, ../sass/_bars-btns.scss */ + .charts .sidebar a.disabled { + @inlcude disabled; + color: #818181; } + /* line 74, ../sass/_bars-btns.scss */ + .charts .sidebar a.disabled:hover span, .charts .sidebar a.disabled:focus span { + border-color: transparent; } +/* line 301, ../sass/_bars-btns.scss */ +.charts .sidebar a.active { + display: inline-block; + text-align: center; + opacity: 1; + border: 1px solid #3c96e0; + color: white; + background-color: #3c96e0; + display: block; } + /* line 29, ../sass/_bars-btns.scss */ + .charts .sidebar a.active span { + display: inline-block; + height: 100%; + line-height: 100%; + padding: 8px; } + /* line 36, ../sass/_bars-btns.scss */ + .charts .sidebar a.active:hover, .charts .sidebar a.active:focus { + text-decoration: none; + opacity: 0.85; } + /* line 45, ../sass/_bars-btns.scss */ + .charts .sidebar a.active .snf-font-remove { + display: inline; } + /* line 48, ../sass/_bars-btns.scss */ + .charts .sidebar a.active.disabled { + background: transparent !important; + border-color: #818181 !important; + color: #818181 !important; } + /* line 15, ../sass/_bars-btns.scss */ + .charts .sidebar a.active.disabled:hover, .charts .sidebar a.active.disabled:focus { + cursor: default; + opacity: 1; } + /* line 18, ../sass/_bars-btns.scss */ + .charts .sidebar a.active.disabled:hover span, .charts .sidebar a.active.disabled:focus span { + color: #818181 !important; } + /* line 87, ../sass/_bars-btns.scss */ + .charts .sidebar a.active span { + border: 1px solid transparent; + background: transparent; } + /* line 93, ../sass/_bars-btns.scss */ + .charts .sidebar a.active:hover span, .charts .sidebar a.active:focus span { + color: white; } + /* line 97, ../sass/_bars-btns.scss */ + .charts .sidebar a.active.disabled { + background: transparent !important; + border-color: #818181 !important; + color: #818181 !important; } + /* line 15, ../sass/_bars-btns.scss */ + .charts .sidebar a.active.disabled:hover, .charts .sidebar a.active.disabled:focus { + cursor: default; + opacity: 1; } + /* line 18, ../sass/_bars-btns.scss */ + .charts .sidebar a.active.disabled:hover span, .charts .sidebar a.active.disabled:focus span { + color: #818181 !important; } +@media (max-width: 1200px) { + /* line 306, ../sass/_bars-btns.scss */ + .charts .sidebar a, .charts .sidebar a.active { + margin-right: 10px; + display: inline-block; } } + +/* line 314, ../sass/_bars-btns.scss */ +.notify .reload-btn { + padding: 0 4px; + font-size: 18px; + vertical-align: middle; + cursor: pointer; } + +/* Switch in filters */ +/* line 323, ../sass/_bars-btns.scss */ +.onoffswitch { + display: inline-block; + float: right; + position: relative; + width: 134px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; } + +/* line 332, ../sass/_bars-btns.scss */ +.onoffswitch-checkbox { + display: none; } + +/* line 335, ../sass/_bars-btns.scss */ +.onoffswitch-label { + display: block; + overflow: hidden; + cursor: pointer; + /*border: 2px solid #F7EFEF;*/ + border-radius: 20px; } + +/* line 342, ../sass/_bars-btns.scss */ +.onoffswitch-inner { + display: block; + width: 200%; + margin-left: -100%; + -moz-transition: margin 0.3s ease-in 0s; + -webkit-transition: margin 0.3s ease-in 0s; + -o-transition: margin 0.3s ease-in 0s; + transition: margin 0.3s ease-in 0s; } + +/* line 349, ../sass/_bars-btns.scss */ +.onoffswitch-inner:before, .onoffswitch-inner:after { + display: block; + float: left; + width: 50%; + height: 30px; + padding: 0; + line-height: 30px; + font-size: 12px; + color: white; + font-family: Trebuchet, Arial, sans-serif; + font-weight: normal; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; } + +/* line 362, ../sass/_bars-btns.scss */ +.onoffswitch-inner:before { + content: "Standard View"; + padding-left: 10px; + background-color: #222222; + color: white; } + +/* line 368, ../sass/_bars-btns.scss */ +.onoffswitch-inner:after { + content: "Compact View"; + padding-right: 10px; + background-color: #222222; + color: white; + text-align: right; } + +/* line 375, ../sass/_bars-btns.scss */ +.onoffswitch-switch { + display: block; + width: 19px; + margin: 6px; + background: white; + border: 2px solid #F7EFEF; + border-radius: 20px; + position: absolute; + top: 0; + bottom: 4px; + right: 103px; + -moz-transition: all 0.3s ease-in 0s; + -webkit-transition: all 0.3s ease-in 0s; + -o-transition: all 0.3s ease-in 0s; + transition: all 0.3s ease-in 0s; } + +/* line 391, ../sass/_bars-btns.scss */ +.onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-inner { + margin-left: 0; } + +/* line 394, ../sass/_bars-btns.scss */ +.onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-switch { + right: 0px; } + +/* Clickable elements that change state */ +/* These are icon-fonts. We insert in html two icons (one for each state) */ +/* The icon with the false state is hidden and only the correct one is displayed */ +/* Which state is the correct it comes from the class of a parent element */ +/* line 404, ../sass/_bars-btns.scss */ +li.active .snf-checkbox-unchecked, li.active .snf-radio-unchecked { + display: none; } + +/* line 407, ../sass/_bars-btns.scss */ +li:not(.active) .snf-checkbox-checked, li:not(.active) .snf-radio-checked { + display: none; } + +/* line 411, ../sass/_bars-btns.scss */ +table.dataTable tbody tr.selected .snf-checkbox-unchecked { + display: none; } + +/* line 415, ../sass/_bars-btns.scss */ +table.dataTable tbody tr:not(.selected) .snf-checkbox-checked { + display: none; } + +/* line 418, ../sass/_bars-btns.scss */ +.show-hide-all.open .snf-font-arrow-down { + display: none; } + +/* line 421, ../sass/_bars-btns.scss */ +.show-hide-all:not(.open) .snf-font-arrow-up { + display: none; } + +/* line 425, ../sass/_bars-btns.scss */ +.instructions .line-btn.open .snf-angle-down { + display: none; } + +/* line 429, ../sass/_bars-btns.scss */ +.instructions .line-btn:not(.open) .snf-angle-up { + display: none; } + +@font-face { + font-family: 'font-icons'; + src: url("../fonts/font-icons.eot?hm0cup"); + src: url("../fonts/font-icons.eot?#iefixhm0cup") format("embedded-opentype"), url("../fonts/font-icons.woff?hm0cup") format("woff"), url("../fonts/font-icons.ttf?hm0cup") format("truetype"), url("../fonts/font-icons.svg?hm0cup#font-icons") format("svg"); + font-weight: normal; + font-style: normal; } + +/* Font with kpal icons */ +@font-face { + font-family: "snf-font"; + src: url("../fonts/snf-font.eot"); + src: url("../fonts/snf-font.eot?#iefix") format("embedded-opentype"), url("../fonts/snf-font.woff") format("woff"), url("../fonts/snf-font.ttf") format("truetype"), url("../fonts/snf-font.svg#snf-font") format("svg"); + font-weight: normal; + font-style: normal; } + +/* line 47, ../sass/icon-fonts.scss */ +.snf-ok { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-ok:before { + content: "\61"; } + +/* line 50, ../sass/icon-fonts.scss */ +.snf-remove, body .custom-buttons .snf-font-remove { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-remove:before, body .custom-buttons .snf-font-remove:before { + content: "\62"; } + +/* line 53, ../sass/icon-fonts.scss */ +.snf-envelope { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-envelope:before { + content: "\63"; } + +/* line 56, ../sass/icon-fonts.scss */ +.snf-envelope-alt { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-envelope-alt:before { + content: "\64"; } + +/* line 59, ../sass/icon-fonts.scss */ +.snf-angle-up { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-angle-up:before { + content: "\65"; } + +/* line 62, ../sass/icon-fonts.scss */ +.snf-angle-down { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-angle-down:before { + content: "\66"; } + +/* line 65, ../sass/icon-fonts.scss */ +.snf-exclamation-sign { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-exclamation-sign:before { + content: "\67"; } + +/* line 68, ../sass/icon-fonts.scss */ +.snf-clipboard-h, .snf-details-project { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-clipboard-h:before, .snf-details-project:before { + content: "\68"; } + +/* line 71, ../sass/icon-fonts.scss */ +.snf-clipboard-i { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-clipboard-i:before { + content: "\69"; } + +/* line 74, ../sass/icon-fonts.scss */ +.snf-copy { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-copy:before { + content: "\6c"; } + +/* line 77, ../sass/icon-fonts.scss */ +.snf-search { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-search:before { + content: "\6d"; } + +/* line 80, ../sass/icon-fonts.scss */ +.snf-sign-out { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-sign-out:before { + content: "\6e"; } + +/* line 83, ../sass/icon-fonts.scss */ +.snf-archive { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-archive:before { + content: "\6b"; } + +/* line 86, ../sass/icon-fonts.scss */ +.snf-checkbox-checked { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-checkbox-checked:before { + content: "\6f"; } + +/* line 89, ../sass/icon-fonts.scss */ +.snf-checkbox-unchecked { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-checkbox-unchecked:before { + content: "\70"; } + +/* line 92, ../sass/icon-fonts.scss */ +.snf-radio-checked { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-radio-checked:before { + content: "\71"; } + +/* line 95, ../sass/icon-fonts.scss */ +.snf-radio-unchecked { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-radio-unchecked:before { + content: "\72"; } + +/* line 98, ../sass/icon-fonts.scss */ +.snf-info { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-info:before { + content: "\73"; } + +/* line 101, ../sass/icon-fonts.scss */ +.snf-user-outline { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-user-outline:before { + content: "\75"; } + +/* line 104, ../sass/icon-fonts.scss */ +.snf-user-full, .snf-details-user { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-user-full:before, .snf-details-user:before { + content: "\74"; } + +/* line 107, ../sass/icon-fonts.scss */ +.snf-wallet-full, .snf-details-quota { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-wallet-full:before, .snf-details-quota:before { + content: "\78"; } + +/* line 110, ../sass/icon-fonts.scss */ +.snf-wallet-outline { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-wallet-outline:before { + content: "\79"; } + +/* line 113, ../sass/icon-fonts.scss */ +.snf-keyboard { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-keyboard:before { + content: "\7a"; } + +/* line 116, ../sass/icon-fonts.scss */ +.snf-book-2 { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-book-2:before { + content: "\42"; } + +/* line 119, ../sass/icon-fonts.scss */ +.snf-bell-1 { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-bell-1:before { + content: "\43"; } + +/* line 122, ../sass/icon-fonts.scss */ +.snf-bulb { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-bulb:before { + content: "\46"; } + +/* line 125, ../sass/icon-fonts.scss */ +.snf-sun-1 { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-sun-1:before { + content: "\47"; } + +/* line 128, ../sass/icon-fonts.scss */ +.snf-moon-1 { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-moon-1:before { + content: "\76"; } + +/* line 131, ../sass/icon-fonts.scss */ +.snf-sun-2-full { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-sun-2-full:before { + content: "\77"; } + +/* line 134, ../sass/icon-fonts.scss */ +.snf-sun-2-outline { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-sun-2-outline:before { + content: "\6a"; } + +/* line 137, ../sass/icon-fonts.scss */ +.snf-moon-2-full:before { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-moon-2-full:before:before { + content: "\44"; } + +/* line 140, ../sass/icon-fonts.scss */ +.snf-moon-2-outline { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-moon-2-outline:before { + content: "\45"; } + +/* line 143, ../sass/icon-fonts.scss */ +.snf-sun-3 { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-sun-3:before { + content: "\41"; } + +/* line 146, ../sass/icon-fonts.scss */ +.snf-filter { + font-family: "font-icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-filter:before { + content: "\7b"; } + +/* line 149, ../sass/icon-fonts.scss */ +.snf-eye { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-eye:before { + content: "\41"; } + +/* line 152, ../sass/icon-fonts.scss */ +.snf-radio-checked { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-radio-checked:before { + content: "\42"; } + +/* line 155, ../sass/icon-fonts.scss */ +.snf-radio-unchecked { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-radio-unchecked:before { + content: "\43"; } + +/* line 158, ../sass/icon-fonts.scss */ +.snf-close { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-close:before { + content: "\44"; } + +/* line 161, ../sass/icon-fonts.scss */ +.snf-www { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-www:before { + content: "\49"; } + +/* line 164, ../sass/icon-fonts.scss */ +.snf-arrow-up, .show-hide-all span.snf-font-arrow-up { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-arrow-up:before, .show-hide-all span.snf-font-arrow-up:before { + content: "\4c"; } + +/* line 167, ../sass/icon-fonts.scss */ +.snf-arrow-down, .show-hide-all span.snf-font-arrow-down { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-arrow-down:before, .show-hide-all span.snf-font-arrow-down:before { + content: "\4d"; } + +/* line 170, ../sass/icon-fonts.scss */ +.snf-checkbox-unchecked { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-checkbox-unchecked:before { + content: "\61"; } + +/* line 173, ../sass/icon-fonts.scss */ +.snf-checkbox-checked { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-checkbox-checked:before { + content: "\62"; } + +/* line 176, ../sass/icon-fonts.scss */ +.snf-cancel-circled { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-cancel-circled:before { + content: "\63"; } + +/* line 179, ../sass/icon-fonts.scss */ +.snf-search { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-search:before { + content: "\64"; } + +/* line 182, ../sass/icon-fonts.scss */ +.snf-twitter-logo { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-twitter-logo:before { + content: "\67"; } + +/* line 185, ../sass/icon-fonts.scss */ +.snf-ok { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-ok:before { + content: "\68"; } + +/* line 188, ../sass/icon-fonts.scss */ +.snf-switch { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-switch:before { + content: "\69"; } + +/* line 191, ../sass/icon-fonts.scss */ +.snf-ban-circle { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-ban-circle:before { + content: "\6a"; } + +/* line 194, ../sass/icon-fonts.scss */ +.snf-ok-sign { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-ok-sign:before { + content: "\6c"; } + +/* line 197, ../sass/icon-fonts.scss */ +.snf-minus-sign { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-minus-sign:before { + content: "\6e"; } + +/* line 200, ../sass/icon-fonts.scss */ +.snf-edit { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-edit:before { + content: "\71"; } + +/* line 203, ../sass/icon-fonts.scss */ +.snf-listview { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-listview:before { + content: "\73"; } + +/* line 206, ../sass/icon-fonts.scss */ +.snf-gridview { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-gridview:before { + content: "\74"; } + +/* line 209, ../sass/icon-fonts.scss */ +.snf-dashboard-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-dashboard-outline:before { + content: "\7a"; } + +/* line 212, ../sass/icon-fonts.scss */ +.snf-pithos-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-pithos-outline:before { + content: "\79"; } + +/* line 215, ../sass/icon-fonts.scss */ +.snf-info-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-info-full:before { + content: "\70"; } + +/* line 218, ../sass/icon-fonts.scss */ +.snf-volume-create-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-volume-create-full:before { + content: "\36"; } + +/* line 221, ../sass/icon-fonts.scss */ +.snf-image-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-image-full:before { + content: "\51"; } + +/* line 224, ../sass/icon-fonts.scss */ +.snf-pc-create-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-pc-create-full:before { + content: "\53"; } + +/* line 227, ../sass/icon-fonts.scss */ +.snf-network-create-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-network-create-outline:before { + content: "\54"; } + +/* line 230, ../sass/icon-fonts.scss */ +.snf-network-create-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-network-create-full:before { + content: "\55"; } + +/* line 233, ../sass/icon-fonts.scss */ +.snf-ram-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-ram-outline:before { + content: "\4a"; } + +/* line 236, ../sass/icon-fonts.scss */ +.snf-nic-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-nic-outline:before { + content: "\50"; } + +/* line 239, ../sass/icon-fonts.scss */ +.snf-ram-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-ram-full:before { + content: "\52"; } + +/* line 242, ../sass/icon-fonts.scss */ +.snf-nic-full, .snf-details-nic { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-nic-full:before, .snf-details-nic:before { + content: "\72"; } + +/* line 245, ../sass/icon-fonts.scss */ +.snf-network-broken-1-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-network-broken-1-full:before { + content: "\56"; } + +/* line 248, ../sass/icon-fonts.scss */ +.snf-network-broken-2-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-network-broken-2-full:before { + content: "\57"; } + +/* line 251, ../sass/icon-fonts.scss */ +.snf-pc-broken-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-pc-broken-full:before { + content: "\58"; } + +/* line 254, ../sass/icon-fonts.scss */ +.snf-pc-reboot-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-pc-reboot-full:before { + content: "\59"; } + +/* line 257, ../sass/icon-fonts.scss */ +.snf-pc-switch-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-pc-switch-full:before { + content: "\5a"; } + +/* line 260, ../sass/icon-fonts.scss */ +.snf-key-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-key-full:before { + content: "\31"; } + +/* line 263, ../sass/icon-fonts.scss */ +.snf-router-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-router-full:before { + content: "\32"; } + +/* line 266, ../sass/icon-fonts.scss */ +.snf-chip-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-chip-full:before { + content: "\33"; } + +/* line 269, ../sass/icon-fonts.scss */ +.snf-plus-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-plus-full:before { + content: "\34"; } + +/* line 272, ../sass/icon-fonts.scss */ +.snf-snapshot-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-snapshot-full:before { + content: "\4e"; } + +/* line 275, ../sass/icon-fonts.scss */ +.snf-pithos-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-pithos-full:before { + content: "\35"; } + +/* line 278, ../sass/icon-fonts.scss */ +.snf-volume-full, .snf-details-volume { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-volume-full:before, .snf-details-volume:before { + content: "\4f"; } + +/* line 281, ../sass/icon-fonts.scss */ +.snf-network-full, .snf-details-network { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-network-full:before, .snf-details-network:before { + content: "\4b"; } + +/* line 284, ../sass/icon-fonts.scss */ +.snf-pc-full, .snf-details-vm { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-pc-full:before, .snf-details-vm:before { + content: "\78"; } + +/* line 287, ../sass/icon-fonts.scss */ +.snf-network-broken-1-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-network-broken-1-outline:before { + content: "\37"; } + +/* line 290, ../sass/icon-fonts.scss */ +.snf-network-broken-2-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-network-broken-2-outline:before { + content: "\38"; } + +/* line 293, ../sass/icon-fonts.scss */ +.snf-pc-broken-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-pc-broken-outline:before { + content: "\39"; } + +/* line 296, ../sass/icon-fonts.scss */ +.snf-volume-broken-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-volume-broken-outline:before { + content: "\30"; } + +/* line 299, ../sass/icon-fonts.scss */ +.snf-pc-reboot-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-pc-reboot-outline:before { + content: "\21"; } + +/* line 302, ../sass/icon-fonts.scss */ +.snf-pc-switch-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-pc-switch-outline:before { + content: "\40"; } + +/* line 305, ../sass/icon-fonts.scss */ +.snf-key-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-key-outline:before { + content: "\23"; } + +/* line 308, ../sass/icon-fonts.scss */ +.snf-router-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-router-outline:before { + content: "\48"; } + +/* line 311, ../sass/icon-fonts.scss */ +.snf-chip-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-chip-outline:before { + content: "\45"; } + +/* line 314, ../sass/icon-fonts.scss */ +.snf-image-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-image-outline:before { + content: "\66"; } + +/* line 317, ../sass/icon-fonts.scss */ +.snf-plus-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-plus-outline:before { + content: "\6d"; } + +/* line 320, ../sass/icon-fonts.scss */ +.snf-snapshot-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-snapshot-outline:before { + content: "\65"; } + +/* line 323, ../sass/icon-fonts.scss */ +.snf-volume-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-volume-outline:before { + content: "\75"; } + +/* line 326, ../sass/icon-fonts.scss */ +.snf-network-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-network-outline:before { + content: "\76"; } + +/* line 329, ../sass/icon-fonts.scss */ +.snf-pc-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-pc-outline:before { + content: "\77"; } + +/* line 332, ../sass/icon-fonts.scss */ +.snf-info-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-info-outline:before { + content: "\6f"; } + +/* line 335, ../sass/icon-fonts.scss */ +.snf-thunder-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-thunder-full:before { + content: "\6b"; } + +/* line 338, ../sass/icon-fonts.scss */ +.snf-lock-closed-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-lock-closed-full:before { + content: "\46"; } + +/* line 341, ../sass/icon-fonts.scss */ +.snf-lock-open-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-lock-open-full:before { + content: "\47"; } + +/* line 345, ../sass/icon-fonts.scss */ +.snf-link-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-link-outline:before { + content: "\26"; } + +/* line 348, ../sass/icon-fonts.scss */ +.snf-refresh-outline, body .custom-buttons .snf-font-reload { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-refresh-outline:before, body .custom-buttons .snf-font-reload:before { + content: "\29"; } + +/* line 351, ../sass/icon-fonts.scss */ +.snf-download-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-download-full:before { + content: "\25"; } + +/* line 354, ../sass/icon-fonts.scss */ +.snf-person-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-person-outline:before { + content: "\2a"; } + +/* line 357, ../sass/icon-fonts.scss */ +.snf-upload-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-upload-full:before { + content: "\28"; } + +/* line 360, ../sass/icon-fonts.scss */ +.snf-arrow-right-small-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-arrow-right-small-full:before { + content: "\2d"; } + +/* line 363, ../sass/icon-fonts.scss */ +.snf-copy-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-copy-outline:before { + content: "\3f"; } + +/* line 366, ../sass/icon-fonts.scss */ +.snf-copy-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-copy-full:before { + content: "\22"; } + +/* line 369, ../sass/icon-fonts.scss */ +.snf-arrow-left-small-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-arrow-left-small-full:before { + content: "\5f"; } + +/* line 372, ../sass/icon-fonts.scss */ +.snf-trash-full { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-trash-full:before { + content: "\3d"; } + +/* line 375, ../sass/icon-fonts.scss */ +.snf-trash-outline { + font-family: "snf-font"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + /* line 41, ../sass/icon-fonts.scss */ + .snf-trash-outline:before { + content: "\24"; } + +/* line 3, ../sass/_details.scss */ +.main { + margin: 2em 0 5em; } + /* line 5, ../sass/_details.scss */ + .main h4 .title { + font-size: 24px; } + /* line 8, ../sass/_details.scss */ + .main span[class^="snf-details"] { + float: left; + margin-right: 8px; + font-size: 35px; } + /* line 13, ../sass/_details.scss */ + .main .lt { + line-height: 35px; } + /* line 16, ../sass/_details.scss */ + .main .rt { + padding-top: 5px; } + /* line 19, ../sass/_details.scss */ + .main .actions-per-item { + padding: 0; } + +/* line 24, ../sass/_details.scss */ +.object-anchor { + height: 2px; } + +/* line 27, ../sass/_details.scss */ +.object-details h4 { + font-size: 14px; + letter-spacing: 1px; } + /* line 30, ../sass/_details.scss */ + .object-details h4 .lt { + display: block; + float: left; + max-width: 60%; + word-wrap: break-word; } + /* line 36, ../sass/_details.scss */ + .object-details h4 .rt { + padding-top: 5px; + display: block; + overflow: hidden; } + /* line 41, ../sass/_details.scss */ + .object-details h4 .arrow { + position: relative; + padding: 0 8px; } + /* line 45, ../sass/_details.scss */ + .object-details h4 .arrow:hover, .object-details h4 .arrow:focus { + top: 2px; + cursor: pointer; + outline: 0 none; } + /* line 51, ../sass/_details.scss */ + .object-details h4 .label { + float: right; + margin-left: 15px; + margin-bottom: 10px; } + /* line 55, ../sass/_details.scss */ + .object-details h4 .label.important { + font-weight: bold; } + /* line 59, ../sass/_details.scss */ + .object-details h4 em { + float: none; } + /* line 61, ../sass/_details.scss */ + .object-details h4 em.os-info { + float: right; + position: relative; + bottom: 3px; } + /* line 65, ../sass/_details.scss */ + .object-details h4 em.os-info img { + height: 26px; + margin-right: 5px; } +/* line 72, ../sass/_details.scss */ +.object-details h3 { + font-size: 18px; + margin: 0 0 1em; + font-weight: 400; + line-height: 35px; } + /* line 77, ../sass/_details.scss */ + .object-details h3 em { + margin-left: 10px; + font-size: 14px; + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 50%; + vertical-align: top; } + /* line 85, ../sass/_details.scss */ + .object-details h3 span[class^="snf-details"] { + float: left; + margin-right: 8px; + font-size: 25px; + height: 35px; + line-height: 35px; } + /* line 92, ../sass/_details.scss */ + .object-details h3 .popover-dismiss { + display: inline-block; + width: 18px; + height: 18px; + background: #4e4e4e; + -webkit-border-radius: 9px; + -moz-border-radius: 9px; + border-radius: 9px; + text-align: center; + font-weight: bold; + vertical-align: middle; + line-height: 18px; + font-size: 16px; + vertical-align: super; + cursor: pointer; + margin-left: 10px; + color: #818181; } + /* line 105, ../sass/_details.scss */ + .object-details h3 .popover-dismiss:hover, .object-details h3 .popover-dismiss:focus { + background: #686868; + color: #eeeeee; } + /* line 111, ../sass/_details.scss */ + .object-details h3 .popover .popover-content { + font-size: 12px; + line-height: 130%; } +/* line 117, ../sass/_details.scss */ +.object-details .icon-link { + margin-right: 10px; } +/* line 120, ../sass/_details.scss */ +.object-details p { + margin: 10px 20px; + font-style: italic; } +/* line 125, ../sass/_details.scss */ +.object-details .length { + margin-left: 6px; + border: 0 none; + font-style: italic; } + /* line 129, ../sass/_details.scss */ + .object-details .length::before { + content: '( '; } + /* line 132, ../sass/_details.scss */ + .object-details .length::after { + content: ' )'; } +/* line 136, ../sass/_details.scss */ +.object-details > .object-details { + margin-left: -20px; + margin-right: -20px; + padding: 12px 20px; } + +/* line 144, ../sass/_details.scss */ +.object-details-content .nav-tabs > li a { + opacity: 0.7; } +/* line 147, ../sass/_details.scss */ +.object-details-content .nav-tabs > li.active > a { + opacity: 1; } +/* line 152, ../sass/_details.scss */ +.object-details-content .nav-tabs > li:not(.active) > a:hover, .object-details-content .nav-tabs > li:not(.active) > a:focus { + opacity: 1; } + +/* line 157, ../sass/_details.scss */ +.tab-pane { + overflow: auto; } + +/* line 161, ../sass/_details.scss */ +.parts-separator { + border-top: 2px solid #4e4e4e; + padding-top: 1em; } + /* line 164, ../sass/_details.scss */ + .parts-separator h2 { + font-size: 24px; + margin-bottom: 2em; + padding-top: 1em; } + /* line 168, ../sass/_details.scss */ + .parts-separator h2 em { + max-width: 50%; + display: inline; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + vertical-align: top; } + +/* line 179, ../sass/_details.scss */ +.part-two > .object-details { + border-bottom: 2px solid #818181; + background: #383838; + padding: 14px 20px; + overflow-x: auto; } + /* line 184, ../sass/_details.scss */ + .part-two > .object-details .object-details { + padding: 5px 20px; } + /* line 187, ../sass/_details.scss */ + .part-two > .object-details .object-details:hover, .part-two > .object-details .object-details:focus { + background: #3d3d3d; } + /* line 192, ../sass/_details.scss */ + .part-two > .object-details .custom-btn span { + padding: 5px; } +/* line 197, ../sass/_details.scss */ +.part-two .object-details-content { + display: none; + padding: 0 35px; } + +/* line 230, ../sass/_details.scss */ +.show-hide-all span.snf-font-arrow-up { + padding: 0; } +/* line 234, ../sass/_details.scss */ +.show-hide-all span.snf-font-arrow-down { + padding: 0; } + +/* line 5, ../sass/_filters.scss */ +.filters-area { + margin-bottom: 40px; + margin-left: 140px; } + @media (max-width: 1200px) { + /* line 5, ../sass/_filters.scss */ + .filters-area { + margin: 0 10px 10px 0; } } + /* line 11, ../sass/_filters.scss */ + .filters-area.no-margin-left { + margin-left: 0; } + /* line 14, ../sass/_filters.scss */ + .filters-area a:focus, .filters-area input:focus { + outline: none; } + /* line 17, ../sass/_filters.scss */ + .filters-area .badge { + margin-left: 6px; + opacity: 0.9; + padding: 2px 9px; } + /* line 22, ../sass/_filters.scss */ + .filters-area ul.nav a { + padding-bottom: 10px; } + +/* line 27, ../sass/_filters.scss */ +.filter { + height: 30px; + margin: 0 10px 10px 0; + display: inline-block; + background: #eeeeee; + border: 1px solid transparent; } + /* line 33, ../sass/_filters.scss */ + .filter .form-group { + margin: 0; + height: 30px; } + /* line 38, ../sass/_filters.scss */ + .filter label, + .filter .dropdown { + height: 30px; + line-height: 30px; + border: 0 none; + padding: 0 10px; + color: #303030; + background: transparent; + font-weight: normal; + margin: 0; } + /* line 48, ../sass/_filters.scss */ + .filter label > a .selected-value, + .filter .dropdown > a .selected-value { + margin-left: 4px; } + /* line 51, ../sass/_filters.scss */ + .filter label > a .arrow, + .filter .dropdown > a .arrow { + font-weight: bold; } + /* line 56, ../sass/_filters.scss */ + .filter label.open a, + .filter .dropdown.open a { + text-decoration: none; + color: #303030; } + /* line 61, ../sass/_filters.scss */ + .filter label a, + .filter .dropdown a { + color: #303030; } + /* line 65, ../sass/_filters.scss */ + .filter .dropdown-menu, .filter .dropdown-list { + background: #eeeeee; + margin: 0; + width: auto; } + /* line 69, ../sass/_filters.scss */ + .filter .dropdown-menu > .active > a, .filter .dropdown-list > .active > a { + background: #eeeeee; } + /* line 72, ../sass/_filters.scss */ + .filter .dropdown-menu > li:hover > a, .filter .dropdown-list > li:hover > a { + background: #d9d9d9; + color: inherit; } + /* line 76, ../sass/_filters.scss */ + .filter .dropdown-menu a, .filter .dropdown-list a { + padding-left: 12px; + padding-right: 12px; } + /* line 77, ../sass/_filters.scss */ + .filter .dropdown-menu a span, .filter .dropdown-list a span { + margin-right: 6px; } + /* line 84, ../sass/_filters.scss */ + .filter input { + border: 0 none; + background: transparent; + height: 30px; + line-height: 30px; + padding: 0 5px; + font-weight: normal; + color: #303030; } + /* line 93, ../sass/_filters.scss */ + .filter .dropdown-list > li > a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 1.42857; + color: #303030; + white-space: nowrap; } + +/* line 104, ../sass/_filters.scss */ +.input-with-btn { + border-width: 0px; + background-color: transparent; + display: inline; } + @media screen and (min-width: 400px) { + /* line 108, ../sass/_filters.scss */ + .input-with-btn input { + width: 200px; } } + @media screen and (min-width: 600px) { + /* line 108, ../sass/_filters.scss */ + .input-with-btn input { + width: 300px; } } + @media screen and (min-width: 800px) { + /* line 108, ../sass/_filters.scss */ + .input-with-btn input { + width: 500px; } } + @media screen and (min-width: 1000px) { + /* line 108, ../sass/_filters.scss */ + .input-with-btn input { + width: 700px; } } + /* line 122, ../sass/_filters.scss */ + .input-with-btn .form-group { + display: inline-block; + background: #eeeeee; + border: 1px solid transparent; + margin-bottom: 0.6em; } + /* line 128, ../sass/_filters.scss */ + .input-with-btn .filter-error { + word-wrap: break-word; } + /* line 131, ../sass/_filters.scss */ + .input-with-btn .error-sign { + display: block; + opacity: 0; + position: static; + display: inline-block; + margin-right: 6px; + margin-left: 10px; + vertical-align: bottom; } + /* line 141, ../sass/_filters.scss */ + .input-with-btn .instructions { + margin-top: 0.6em; } + /* line 143, ../sass/_filters.scss */ + .input-with-btn .instructions * { + color: white; } + /* line 146, ../sass/_filters.scss */ + .input-with-btn .instructions .content-area { + display: none; + background: #222222; + padding: 12px 13px 18px; } + /* line 150, ../sass/_filters.scss */ + .input-with-btn .instructions .content-area dt { + width: 200px; } + /* line 153, ../sass/_filters.scss */ + .input-with-btn .instructions .content-area dd { + margin-left: 220px; } + /* line 157, ../sass/_filters.scss */ + .input-with-btn .instructions .clarifications { + font-style: italic; } + +/* line 164, ../sass/_filters.scss */ +.filter:not(.visible-filter):not(.visible-filter-fade) { + display: none; + opacity: 0; } + +/* line 169, ../sass/_filters.scss */ +.visible-filter-fade { + opacity: 1; + transition: opacity 0.5s; } + +/* line 174, ../sass/_filters.scss */ +.filters .filters-list { + border-radius: 15px; + background: #222222; + border: 1px solid white; + height: 28px; } + /* line 179, ../sass/_filters.scss */ + .filters .filters-list > a { + color: white; + line-height: 28px; + font-weight: bold; + padding: 8px 7px; + background: transparent; } + /* line 186, ../sass/_filters.scss */ + .filters .filters-list .popover { + padding: 0; } + /* line 189, ../sass/_filters.scss */ + .filters .filters-list .popover-content { + padding: 0; } + /* line 192, ../sass/_filters.scss */ + .filters .filters-list .popover ul { + list-style: none; + padding: 5px 0px; + min-width: 160px; } + /* line 196, ../sass/_filters.scss */ + .filters .filters-list .popover ul li { + white-space: nowrap; } + /* line 198, ../sass/_filters.scss */ + .filters .filters-list .popover ul li a { + color: #303030; } + /* line 201, ../sass/_filters.scss */ + .filters .filters-list .popover ul li span { + margin-right: 10px; } + /* line 205, ../sass/_filters.scss */ + .filters .filters-list .popover ul .divider { + height: 1px; + margin: 9px 0; + overflow: hidden; + background-color: #e5e5e5; } + /* line 212, ../sass/_filters.scss */ + .filters .filters-list .popover.bottom > .arrow:after { + border-bottom-color: #eeeeee; } + +/* line 1, ../sass/_modals.scss */ +p.progress-area { + visibility: hidden; } + +/* line 6, ../sass/_modals.scss */ +.in-progress .modal-body { + background-color: #818181; } + /* line 8, ../sass/_modals.scss */ + .in-progress .modal-body p.progress-area { + visibility: visible; } + +/* line 16, ../sass/_modals.scss */ +.modal[data-item="user"]:not([data-type="contact"]) .table-selected td:nth-child(3) { + display: none; } +/* line 22, ../sass/_modals.scss */ +.modal#user-contact p { + margin-top: 18px; + position: relative; } +/* line 27, ../sass/_modals.scss */ +.modal p { + position: relative; } +/* line 30, ../sass/_modals.scss */ +.modal p > .error-sign { + top: 0; } +/* line 34, ../sass/_modals.scss */ +.modal h3 { + margin-top: 0; + font-weight: bold; } +/* line 38, ../sass/_modals.scss */ +.modal textarea { + resize: vertical; } +/* line 42, ../sass/_modals.scss */ +.modal textarea, .modal input { + width: 87%; + vertical-align: text-top; + padding: 4px 8px; + border: 1px solid #d9d9d9; + color: #303030; } + /* line 48, ../sass/_modals.scss */ + .modal textarea.body, .modal input.body { + min-height: 160px; } +/* line 53, ../sass/_modals.scss */ +.modal label { + margin-right: 6px; + width: 70px; + vertical-align: sub; } +/* line 60, ../sass/_modals.scss */ +.modal .modal-body { + background-color: white; } +/* line 65, ../sass/_modals.scss */ +.modal .modal-footer { + margin-top: 0; } + /* line 67, ../sass/_modals.scss */ + .modal .modal-footer form { + display: inline; } + /* line 70, ../sass/_modals.scss */ + .modal .modal-footer .custom-btn:first-child { + float: left; + background-color: #303030; + border-color: #303030; } + +/* line 80, ../sass/_modals.scss */ +.modal .custom-btn { + color: white; + opacity: 0.9; } + /* line 84, ../sass/_modals.scss */ + .modal .custom-btn:hover, .modal .custom-btn:focus { + opacity: 1; } +/* line 89, ../sass/_modals.scss */ +.modal[data-karma="dark"] .elem { + color: #4e4e4e; } +/* line 94, ../sass/_modals.scss */ +.modal[data-karma="neutral"] .elem { + color: #207dc9; } +/* line 100, ../sass/_modals.scss */ +.modal[data-karma="good"] .elem { + color: #007238; } +/* line 108, ../sass/_modals.scss */ +.modal[data-karma="bad"] .elem { + color: #a66b18; } +/* line 115, ../sass/_modals.scss */ +.modal[data-caution="warning"][data-karma="good"] .elem, .modal[data-caution="warning"][data-karma="neutral"] .elem { + color: #a66b18; } +/* line 122, ../sass/_modals.scss */ +.modal[data-caution="dangerous"][data-karma="bad"] .elem, .modal[data-caution="dangerous"][data-karma="neutral"] .elem { + color: #c21934; } + +/* line 129, ../sass/_modals.scss */ +.custom-btn[data-karma="dark"] { + background-color: #222222; + border-color: transparent; } + +/* line 134, ../sass/_modals.scss */ +.modal em { + font-weight: bold; + font-style: normal; } +/* line 138, ../sass/_modals.scss */ +.modal .popover { + z-index: 2000; } + /* line 140, ../sass/_modals.scss */ + .modal .popover dl { + color: black; + font-weight: normal; } + /* line 143, ../sass/_modals.scss */ + .modal .popover dl dt { + width: 90px; } + /* line 146, ../sass/_modals.scss */ + .modal .popover dl dd { + margin-left: 110px; } + /* line 150, ../sass/_modals.scss */ + .modal .popover h2 { + font-size: 16px; + color: #303030; + font-weight: bold; + text-align: center; } +/* line 157, ../sass/_modals.scss */ +.modal .popover-content { + min-width: 150px; } + +/* line 163, ../sass/_modals.scss */ +.modal-content { + padding: 20px; + color: #303030; } + /* line 166, ../sass/_modals.scss */ + .modal-content .badge { + background-color: transparent; } + +/* line 173, ../sass/_modals.scss */ +.instructions-icon { + color: #3c96e0; + font-size: 22px; + margin-left: 78px; } + /* line 177, ../sass/_modals.scss */ + .instructions-icon:hover { + text-decoration: none; } + +/* line 182, ../sass/_modals.scss */ +.extra-info { + margin-top: 10px; } + +/* line 186, ../sass/_modals.scss */ +.error-sign { + color: red; + font-size: 20px; + margin-left: 10px; + position: absolute; + top: 6px; + display: none; } + /* line 195, ../sass/_modals.scss */ + .error-sign:hover, .error-sign:focus { + color: red; + text-decoration: none; } + +/* line 202, ../sass/_modals.scss */ +.form-area { + position: relative; } + +/* line 205, ../sass/_modals.scss */ +.form-subject { + margin-bottom: 15px; } + +/* line 209, ../sass/_modals.scss */ +.toggle-more { + margin-top: -16px; + display: none; } + +/* line 216, ../sass/_modals.scss */ +.modal .table-selected th, .modal .table-selected td { + word-break: break-word; } +/* line 220, ../sass/_modals.scss */ +.modal .table-selected td:last-child .wrap { + padding-right: 36px; } +/* line 224, ../sass/_modals.scss */ +.modal .table-selected tr:nth-child(2n) { + background: #f2f2f2; } +/* line 228, ../sass/_modals.scss */ +.modal .table-selected tr a { + font-weight: bold; } +/* line 233, ../sass/_modals.scss */ +.modal .table-selected tr:hover, +.modal .table-selected tr:focus { + background: #d9d9d9; } + /* line 235, ../sass/_modals.scss */ + .modal .table-selected tr:hover a, + .modal .table-selected tr:focus a { + color: red; } +/* line 240, ../sass/_modals.scss */ +.modal .table-selected .remove { + position: absolute; + right: 14px; + color: transparent; } + /* line 244, ../sass/_modals.scss */ + .modal .table-selected .remove:hover { + cursor: pointer; + text-decoration: none; } + +/* line 1, ../sass/_tables.scss */ +table thead th { + white-space: nowrap; } + +/* line 6, ../sass/_tables.scss */ +table td, +table th { + vertical-align: top; } + +/* line 10, ../sass/_tables.scss */ +table .wrap { + position: relative; } + +/* line 15, ../sass/_tables.scss */ +.table-items .snf-search { + opacity: 0.7; + font-size: 15px; } + /* line 19, ../sass/_tables.scss */ + .table-items .snf-search:hover, .table-items .snf-search:focus { + opacity: 1; } +/* line 23, ../sass/_tables.scss */ +.table-items .login-method { + padding: 2px 16px 2px 0px; + text-align: center; } +/* line 28, ../sass/_tables.scss */ +.table-items th .badge { + margin: 0 2px 0 4px; + display: inline; + padding-top: 2px; } +/* line 33, ../sass/_tables.scss */ +.table-items td { + padding: 8px 6px 0 6px; } + +/* line 41, ../sass/_tables.scss */ +.table-selected-main:not(.table-selected) td:last-child, +.table-items:not(.table-selected) td:last-child { + max-width: 60px; + min-width: 60px; + padding: 8px 5px; } + /* line 45, ../sass/_tables.scss */ + .table-selected-main:not(.table-selected) td:last-child .details-link:hover, + .table-items:not(.table-selected) td:last-child .details-link:hover { + text-decoration: none; } + /* line 48, ../sass/_tables.scss */ + .table-selected-main:not(.table-selected) td:last-child .summary-expand, + .table-items:not(.table-selected) td:last-child .summary-expand { + position: relative; + z-index: 10; + float: right; + padding-left: 8px; + padding-right: 8px; + background-color: #4d99d8; + color: #fff; } + /* line 57, ../sass/_tables.scss */ + .table-selected-main:not(.table-selected) td:last-child .summary-expand:hover, .table-selected-main:not(.table-selected) td:last-child .summary-expand:focus, + .table-items:not(.table-selected) td:last-child .summary-expand:hover, + .table-items:not(.table-selected) td:last-child .summary-expand:focus { + text-decoration: none; + background-color: #83b8e4; } + /* line 62, ../sass/_tables.scss */ + .table-selected-main:not(.table-selected) td:last-child dl, + .table-items:not(.table-selected) td:last-child dl { + z-index: 0; + position: relative; + padding: 8px; + display: none; + margin: 0; } + +/* line 72, ../sass/_tables.scss */ +.table-items .headerSortUp span.caret { + border-top: 0; + border-bottom: 4px solid; } + +/* line 79, ../sass/_tables.scss */ +#table-items-selected_filter label, +#table-items-total_filter label { + color: white; } +/* line 82, ../sass/_tables.scss */ +#table-items-selected_filter input, +#table-items-total_filter input { + color: #303030; + background: #eeeeee; + border: 1px solid transparent; + padding: 3px 5px; } + /* line 87, ../sass/_tables.scss */ + #table-items-selected_filter input:focus, + #table-items-total_filter input:focus { + outline: 0 none; } + +/* line 93, ../sass/_tables.scss */ +#table-items-selected_wrapper { + padding: 10px; + border: 1px solid gray; + margin-bottom: 20px; + display: none; } + +/* line 103, ../sass/_tables.scss */ +div.dataTables_length { + padding-left: 2em; + padding-top: 0.55em; } + /* line 106, ../sass/_tables.scss */ + div.dataTables_length select { + width: 55px; + display: inline-block; + margin-left: 4px; + vertical-align: baseline; + color: #222; } + +/* line 115, ../sass/_tables.scss */ +table.dataTable tbody tr { + background-color: inherit; } + /* line 117, ../sass/_tables.scss */ + table.dataTable tbody tr.even { + background-color: #3d3d3d; } + +/* line 123, ../sass/_tables.scss */ +table.dataTable thead th, +table.dataTable thead td { + border-bottom: 1px solid white; + border-top: 1px solid white; } + +/* line 127, ../sass/_tables.scss */ +table.dataTable tbody tr:hover { + background-color: #4f4f4f; } + +/* line 130, ../sass/_tables.scss */ +table.dataTable tbody tr.selected { + color: #303030; + background-color: #eeeeee; } + +/* line 136, ../sass/_tables.scss */ +html body .dataTables_wrapper label { + font-weight: normal; } +/* line 140, ../sass/_tables.scss */ +html body .dataTables_wrapper table th.sorting, html body .dataTables_wrapper table th.sorting_asc, html body .dataTables_wrapper table th.sorting_desc { + background-position: center left; + padding-left: 22px; } + +/* line 150, ../sass/_tables.scss */ +.dataTables_wrapper .dataTables_length, +.dataTables_wrapper .dataTables_info, +.dataTables_wrapper .dataTables_paginate { + padding-top: 0; + margin-bottom: 0.5em; + color: white; + line-height: 35px; } + +/* line 156, ../sass/_tables.scss */ +table.dataTable.no-footer { + border-bottom: 1px solid #eeeeee; + margin: 2em 0; } + +/* line 161, ../sass/_tables.scss */ +.dataTables_wrapper .dataTables_paginate .paginate_button { + color: white !important; + padding: 0 1em; } + +/* line 168, ../sass/_tables.scss */ +.container .dataTables_wrapper .dataTables_paginate .paginate_button:hover, +.container .dataTables_wrapper .dataTables_paginate .paginate_button:focus { + background: transparent; + border-color: white; + color: white !important; } + +/* line 174, ../sass/_tables.scss */ +.container .dataTables_wrapper .dataTables_paginate .paginate_button.disabled { + border-color: transparent; + color: #818181 !important; } + /* line 179, ../sass/_tables.scss */ + .container .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover, .container .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:focus, .container .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active { + color: #818181 !important; } + +/* line 186, ../sass/_tables.scss */ +.dataTables_wrapper .dataTables_paginate .paginate_button.current, +.dataTables_wrapper .dataTables_paginate .paginate_button.current:focus, +.dataTables_wrapper .dataTables_paginate .paginate_button.current:hover { + background: white; + color: #303030 !important; + border: transparent; } + +/* line 192, ../sass/_tables.scss */ +.dataTables_wrapper > .custom-buttons { + margin-bottom: 1em; + width: 100%; } + +/* line 197, ../sass/_tables.scss */ +.dataTables_wrapper .dataTables_processing { + background: #4e4e4e; + color: white; + padding: 5px 10px; + -webkit-box-shadow: inset 0 0 5px #888888; + box-shadow: inset 0 0 5px #888888; + z-index: 1; } + +/* line 205, ../sass/_tables.scss */ +.fixed { + position: fixed; } + +/* line 38, ../sass/_settings.scss */ +.ip_log tr td:nth-child(2), .ip_log tr th:nth-child(2) { + word-break: break-word; + max-width: 250px; } +/* line 38, ../sass/_settings.scss */ +.ip_log tr td:nth-child(3), .ip_log tr th:nth-child(3) { + word-break: break-word; + max-width: 150px; } +/* line 38, ../sass/_settings.scss */ +.ip_log tr td:nth-child(4), .ip_log tr th:nth-child(4) { + word-break: break-word; + max-width: 150px; } + +/* Layout & general stuff */ +/* line 4, ../sass/_extra.scss */ +html, body { + height: 100%; } + +/* line 8, ../sass/_extra.scss */ +body { + padding-top: 100px; } + +/* line 12, ../sass/_extra.scss */ +.wrapper { + padding-bottom: 50px; } + +/* +.container-solid{ + min-width: 1050px!important; +} +*/ +/* line 20, ../sass/_extra.scss */ +.container:not(.container-solid) { + max-width: 960px; } + +/* line 24, ../sass/_extra.scss */ +h1, h2, h3, h4 { + word-wrap: break-word; } + +/* line 28, ../sass/_extra.scss */ +.info { + overflow: auto; } + +/* line 33, ../sass/_extra.scss */ +.dl-horizontal dd, dt, +.tooltip-inner { + word-wrap: break-word; } + +/* line 36, ../sass/_extra.scss */ +.disabled { + cursor: default !important; } + +/* Home */ +/* line 42, ../sass/_extra.scss */ +.app-list { + position: relative; + text-align: center; + padding-top: 100px; } + /* line 46, ../sass/_extra.scss */ + .app-list a { + width: 210px; + font-size: 24px; + margin: 0 20px; + display: inline-block; + text-align: center; + opacity: 1; + border: 1px solid white; + color: white; + opacity: 1; } + /* line 29, ../sass/_bars-btns.scss */ + .app-list a span { + display: inline-block; + height: 100%; + line-height: 100%; + padding: 12px 10px; } + /* line 36, ../sass/_bars-btns.scss */ + .app-list a:hover, .app-list a:focus { + text-decoration: none; + opacity: 0.85; } + /* line 45, ../sass/_bars-btns.scss */ + .app-list a .snf-font-remove { + display: inline; } + /* line 48, ../sass/_bars-btns.scss */ + .app-list a.disabled { + background: transparent !important; + border-color: #818181 !important; + color: #818181 !important; } + /* line 15, ../sass/_bars-btns.scss */ + .app-list a.disabled:hover, .app-list a.disabled:focus { + cursor: default; + opacity: 1; } + /* line 18, ../sass/_bars-btns.scss */ + .app-list a.disabled:hover span, .app-list a.disabled:focus span { + color: #818181 !important; } + /* line 59, ../sass/_bars-btns.scss */ + .app-list a span { + border: 1px solid transparent; + width: 100%; } + /* line 65, ../sass/_bars-btns.scss */ + .app-list a:hover span, .app-list a:focus span { + border-color: white; } + /* line 69, ../sass/_bars-btns.scss */ + .app-list a.disabled { + @inlcude disabled; + color: #818181; } + /* line 74, ../sass/_bars-btns.scss */ + .app-list a.disabled:hover span, .app-list a.disabled:focus span { + border-color: transparent; } + /* line 52, ../sass/_extra.scss */ + .app-list a.disabled { + border-color: #a7a7a7; + color: gray; } + /* line 57, ../sass/_extra.scss */ + .app-list a.disabled:hover span, .app-list a.disabled:focus span { + border-color: transparent; } + +/* line 65, ../sass/_extra.scss */ +.nav-simple { + padding: 20px; + border-bottom: 1px solid white; } + /* line 68, ../sass/_extra.scss */ + .nav-simple .header { + float: left; + line-height: 40px; + font-size: 26px; } + /* line 72, ../sass/_extra.scss */ + .nav-simple .header img { + max-height: 50px; } + /* line 76, ../sass/_extra.scss */ + .nav-simple .login-info { + float: right; + position: relative; + line-height: 40px; + font-size: 16px; } + /* line 81, ../sass/_extra.scss */ + .nav-simple .login-info .has-dropdown { + display: inline; + position: relative; } + /* line 86, ../sass/_extra.scss */ + .nav-simple .login-info .has-dropdown:hover > a, .nav-simple .login-info .has-dropdown:focus > a { + background: #4d4d4d; } + /* line 90, ../sass/_extra.scss */ + .nav-simple .login-info .has-dropdown > a { + color: white; + display: inline-block; + padding: 0 10px; } + /* line 96, ../sass/_extra.scss */ + .nav-simple .login-info .dropdown-menu { + left: auto; + right: 0; + top: 27px; } + +/* Navigation */ +/* line 106, ../sass/_extra.scss */ +.navbar-default { + border: 0 none; + border-bottom: 1px solid transparent; + z-index: 1040; + margin: 0 auto; } + /* line 111, ../sass/_extra.scss */ + .navbar-default .container-fluid { + padding: 0; } + /* line 114, ../sass/_extra.scss */ + .navbar-default .home-icon { + padding: 0; + height: 50px; + width: 50px; + text-align: center; + line-height: 50px; + font-size: 2px; + background: #00a551; } + /* line 122, ../sass/_extra.scss */ + .navbar-default .home-icon img { + max-height: 50px; } + +/* line 129, ../sass/_extra.scss */ +.sub-nav { + top: 50px; + min-height: inherit; } + /* line 133, ../sass/_extra.scss */ + .sub-nav .nav > li > a { + padding-top: 8px; + padding-bottom: 8px; } + @media (max-width: 768px) { + /* line 129, ../sass/_extra.scss */ + .sub-nav { + display: none; } } + +/* line 142, ../sass/_extra.scss */ +.dropdown-menu { + overflow-y: auto; } + +/* line 147, ../sass/_extra.scss */ +.nav .has-dropdown:hover > ul.dropdown-menu, +.nav-simple .has-dropdown:hover > ul.dropdown-menu { + display: block; } + +/* More */ +/* line 157, ../sass/_extra.scss */ +svg > text:last-child { + display: none; } + +/* line 161, ../sass/_extra.scss */ +.has-dropdown .arrow { + margin-left: 6px; + vertical-align: middle; } + +/* line 166, ../sass/_extra.scss */ +.hidden-row { + display: none; } + +/* line 170, ../sass/_extra.scss */ +.with-shift *::selection { + background-color: transparent; } + +/* line 174, ../sass/_extra.scss */ +.with-shift *::-moz-selection { + background: transparent; } + +/* line 177, ../sass/_extra.scss */ +.tab-content { + background: #4e4e4e; + color: white; + padding: 20px; + border: 0 none; } + /* line 182, ../sass/_extra.scss */ + .tab-content .well { + margin-bottom: 0; } + +/* line 187, ../sass/_extra.scss */ +.selection-indicator { + cursor: pointer; + padding: 6px 12px 6px 6px; } + +/* Notification area */ +/* line 194, ../sass/_extra.scss */ +.notify { + padding: 30px 10px 15px; + width: 100%; + position: fixed; + bottom: 0; + background: white; + color: #303030; } + /* line 202, ../sass/_extra.scss */ + .notify .container > *:not(:last-child) { + margin-bottom: 16px; } + /* line 205, ../sass/_extra.scss */ + .notify .remove-icon { + color: transparent; + margin-left: 20px; + font-weight: bold; } + /* line 211, ../sass/_extra.scss */ + .notify .container > *:hover .remove-icon { + color: #d9534f; } + /* line 215, ../sass/_extra.scss */ + .notify .state-icon { + margin-right: 10px; } + /* line 218, ../sass/_extra.scss */ + .notify .success { + color: #449d44; } + /* line 221, ../sass/_extra.scss */ + .notify .error { + color: #d9534f; } + /* line 224, ../sass/_extra.scss */ + .notify .pending { + color: #f0ad4e; } + /* line 227, ../sass/_extra.scss */ + .notify .warning, .notify .no-notifications { + font-style: italic; + font-weight: bold; + display: inline-block; + text-align: right; } + /* line 232, ../sass/_extra.scss */ + .notify .warning > .wrap, .notify .no-notifications > .wrap { + display: block; + padding-right: 4px; } + /* line 236, ../sass/_extra.scss */ + .notify .warning a:hover, .notify .no-notifications a:hover { + cursor: pointer; } + /* line 240, ../sass/_extra.scss */ + .notify .close-notify { + position: absolute; + right: 20px; + top: 20px; + color: #303030; } + /* line 246, ../sass/_extra.scss */ + .notify .close-notify:hover, .notify .close-notify:focus { + color: inherit; } + /* line 250, ../sass/_extra.scss */ + .notify .dl-horizontal { + margin-left: 21px; } + /* line 252, ../sass/_extra.scss */ + .notify .dl-horizontal dt { + width: 80px; + vertical-align: top; + text-align: left; } + /* line 256, ../sass/_extra.scss */ + .notify .dl-horizontal dt span { + font-size: 20px; + vertical-align: text-bottom; + margin-right: 10px; } + /* line 262, ../sass/_extra.scss */ + .notify .dl-horizontal dd { + margin-left: 80px; } + +/* line 268, ../sass/_extra.scss */ +.lowercase { + text-transform: lowercase; } + +/* line 273, ../sass/_extra.scss */ +.shortcuts-btn .book-icon { + padding-right: 2px; + vertical-align: sub; + font-size: 17px; } + +/* line 281, ../sass/_extra.scss */ +body .shortcuts dt { + width: 119px; + margin-bottom: 12px; } +/* line 285, ../sass/_extra.scss */ +body .shortcuts dd { + margin-left: 139px; } +/* line 288, ../sass/_extra.scss */ +body .shortcuts .key { + padding: 2px 9px; + font-style: normal; + font-weight: bold; + border: 1px solid #dddddd; + background: whitesmoke; + border-radius: 6px; } + +/* line 298, ../sass/_extra.scss */ +.filters-examples dt { + font-weight: normal; + margin-bottom: 0; } +/* line 302, ../sass/_extra.scss */ +.filters-examples dd { + margin-bottom: 12px; } + /* line 304, ../sass/_extra.scss */ + .filters-examples dd .highlight { + background: whitesmoke; + padding: 2px 6px; + border-bottom: 1px solid #dddddd; } + /* line 309, ../sass/_extra.scss */ + .filters-examples dd.divider { + margin-bottom: 8px; + border-bottom: 1px solid #dddddd; } + +/* line 317, ../sass/_extra.scss */ +.notes dt { + width: 50px; } +/* line 320, ../sass/_extra.scss */ +.notes dd { + margin-left: 60px; } + +/* line 325, ../sass/_extra.scss */ +.popover { + z-index: 1999; + max-width: none; + color: #303030; + margin-bottom: 20px; } + /* line 330, ../sass/_extra.scss */ + .popover h2 { + text-align: center; + font-size: 1.3em; + font-weight: bold; + margin-top: 0; } + /* line 336, ../sass/_extra.scss */ + .popover h3 { + font-size: 1.2em; + font-weight: bold; } + /* line 340, ../sass/_extra.scss */ + .popover h4 { + font-size: 1.1em; + font-weight: bold; } + /* line 344, ../sass/_extra.scss */ + .popover dt { + margin-bottom: 8px; + overflow: visible; } + /* line 348, ../sass/_extra.scss */ + .popover .panel-default { + border-color: transparent; + box-shadow: none; } + +/* line 354, ../sass/_extra.scss */ +.sign-out { + text-align: right; } + /* line 356, ../sass/_extra.scss */ + .sign-out span { + margin-right: 10px; + vertical-align: middle; + font-size: 18px; } + +/* line 364, ../sass/_extra.scss */ +.stats section { + margin-bottom: 3em; } + /* line 366, ../sass/_extra.scss */ + .stats section h3 { + margin-bottom: 1em; } + /* line 368, ../sass/_extra.scss */ + .stats section h3 span { + margin-right: 0.5em; } + /* line 372, ../sass/_extra.scss */ + .stats section .custom-btn { + float: left; + margin-right: 32px; } + /* line 374, ../sass/_extra.scss */ + .stats section .custom-btn span { + padding-left: 0; } + /* line 377, ../sass/_extra.scss */ + .stats section .custom-btn .snf-download-full { + padding-right: 0; + padding-left: 8px; } + /* line 383, ../sass/_extra.scss */ + .stats section .spinner { + display: none; + float: left; + padding: 8px; } + +/* line 392, ../sass/_extra.scss */ +.navbar-right .dropdown-menu, .login-info .dropdown-menu { + min-width: 0; } + +@media (min-width: 1200px) { + /* line 397, ../sass/_extra.scss */ + .stick { + position: fixed; + top: 100px; + width: inherit; } } + +/* line 406, ../sass/_extra.scss */ +.themes { + position: fixed; + left: 10px; + bottom: 10px; } + +/* line 413, ../sass/_extra.scss */ +.charts .info { + overflow: hidden; } +/* line 416, ../sass/_extra.scss */ +.charts h3 { + text-align: center; + margin-bottom: 1em; } +/* line 420, ../sass/_extra.scss */ +.charts .c3-axis { + fill: white; } +/* line 423, ../sass/_extra.scss */ +.charts .c3 path, .charts .c3 line { + stroke: white; } +/* line 426, ../sass/_extra.scss */ +.charts .c3-legend-item text { + fill: white; } +/* line 429, ../sass/_extra.scss */ +.charts .c3-tooltip { + color: #222; } + +/* line 433, ../sass/_extra.scss */ +.popover-content { + max-width: 800px; } diff --git a/snf-admin-app/synnefo_admin/admin/static/css/screen.css b/snf-admin-app/synnefo_admin/admin/static/css/screen.css new file mode 100644 index 0000000000000000000000000000000000000000..ac065574b919226a0bcfb7225d34479d65430f37 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/css/screen.css @@ -0,0 +1,59 @@ +/* Welcome to Compass. + * In this file you should write your main styles. (or centralize your imports) + * Import this file using the following HTML or equivalent: + * <link href="/stylesheets/screen.css" media="screen, projection" rel="stylesheet" type="text/css" /> */ +/* line 17, ../../../../../../../var/lib/gems/1.9.1/gems/compass-0.12.7/frameworks/compass/stylesheets/compass/reset/_utilities.scss */ +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font: inherit; + font-size: 100%; + vertical-align: baseline; } + +/* line 22, ../../../../../../../var/lib/gems/1.9.1/gems/compass-0.12.7/frameworks/compass/stylesheets/compass/reset/_utilities.scss */ +html { + line-height: 1; } + +/* line 24, ../../../../../../../var/lib/gems/1.9.1/gems/compass-0.12.7/frameworks/compass/stylesheets/compass/reset/_utilities.scss */ +ol, ul { + list-style: none; } + +/* line 26, ../../../../../../../var/lib/gems/1.9.1/gems/compass-0.12.7/frameworks/compass/stylesheets/compass/reset/_utilities.scss */ +table { + border-collapse: collapse; + border-spacing: 0; } + +/* line 28, ../../../../../../../var/lib/gems/1.9.1/gems/compass-0.12.7/frameworks/compass/stylesheets/compass/reset/_utilities.scss */ +caption, th, td { + text-align: left; + font-weight: normal; + vertical-align: middle; } + +/* line 30, ../../../../../../../var/lib/gems/1.9.1/gems/compass-0.12.7/frameworks/compass/stylesheets/compass/reset/_utilities.scss */ +q, blockquote { + quotes: none; } + /* line 103, ../../../../../../../var/lib/gems/1.9.1/gems/compass-0.12.7/frameworks/compass/stylesheets/compass/reset/_utilities.scss */ + q:before, q:after, blockquote:before, blockquote:after { + content: ""; + content: none; } + +/* line 32, ../../../../../../../var/lib/gems/1.9.1/gems/compass-0.12.7/frameworks/compass/stylesheets/compass/reset/_utilities.scss */ +a img { + border: none; } + +/* line 116, ../../../../../../../var/lib/gems/1.9.1/gems/compass-0.12.7/frameworks/compass/stylesheets/compass/reset/_utilities.scss */ +article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section, summary { + display: block; } diff --git a/snf-admin-app/synnefo_admin/admin/static/fonts/font-icons.eot b/snf-admin-app/synnefo_admin/admin/static/fonts/font-icons.eot new file mode 100755 index 0000000000000000000000000000000000000000..11d7465850e5657119c32d5c784e3329b2726f46 Binary files /dev/null and b/snf-admin-app/synnefo_admin/admin/static/fonts/font-icons.eot differ diff --git a/snf-admin-app/synnefo_admin/admin/static/fonts/font-icons.svg b/snf-admin-app/synnefo_admin/admin/static/fonts/font-icons.svg new file mode 100755 index 0000000000000000000000000000000000000000..5937f955901c0052fc3ec52269bf3becb04a1915 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/fonts/font-icons.svg @@ -0,0 +1,58 @@ +<?xml version="1.0" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" > +<svg xmlns="http://www.w3.org/2000/svg"> +<metadata> +<json> +{ + "fontFamily": "font-icons", + "majorVersion": 1, + "minorVersion": 0, + "version": "Version 1.0", + "fontId": "font-icons", + "psName": "font-icons", + "subFamily": "Regular", + "fullName": "font-icons", + "description": "Font generated by IcoMoon." +} +</json> +</metadata> +<defs> +<font id="font-icons" horiz-adv-x="512"> +<font-face units-per-em="512" ascent="480" descent="-32" /> +<missing-glyph horiz-adv-x="512" /> +<glyph unicode=" " d="" horiz-adv-x="256" /> +<glyph unicode="A" d="M256 269c-24.813 0-45-20.187-45-45s20.187-45 45-45c24.813 0 45 20.187 45 45s-20.187 45-45 45zM256 320v0c53.020 0 96-42.98 96-96s-42.98-96-96-96c-53.020 0-96 42.98-96 96s42.98 96 96 96zM438.627 86.627c12.498-12.497 12.497-32.758 0-45.254-12.497-12.498-32.757-12.498-45.254 0l-32 32c-12.498 12.497-12.498 32.757 0 45.254 12.496 12.497 32.757 12.498 45.254 0l32-32zM150.627 374.627c12.498-12.497 12.497-32.758 0-45.254-12.497-12.498-32.757-12.498-45.254 0l-32 32c-12.498 12.497-12.498 32.757 0 45.254 12.496 12.497 32.757 12.498 45.254 0l32-32zM393.372 406.627c12.497 12.498 32.758 12.497 45.254 0 12.498-12.497 12.498-32.757 0.001-45.254l-32-32c-12.498-12.498-32.758-12.498-45.254 0-12.498 12.496-12.498 32.757 0 45.254l31.999 32zM105.373 118.627c12.497 12.498 32.757 12.497 45.254 0s12.498-32.757 0-45.254l-32-32c-12.497-12.498-32.757-12.498-45.254 0-12.497 12.496-12.498 32.757 0 45.254l32 32zM256 448c17.673 0 32-14.327 32-32v-32c0-17.673-14.327-32-32-32s-32 14.327-32 32v32c0 17.673 14.327 32 32 32zM256 96c17.673 0 32-14.326 32-32v-32c0-17.674-14.327-32-32-32s-32 14.326-32 32v32c0 17.674 14.327 32 32 32zM32 224c0 17.673 14.327 32 32 32h32c17.673 0 32-14.327 32-32s-14.327-32-32-32h-32c-17.673 0-32 14.326-32 32zM384 224c0 17.673 14.326 32 32 32h32c17.674 0 32-14.327 32-32s-14.326-32-32-32h-32c-17.674 0-32 14.327-32 32z" /> +<glyph unicode="B" d="M384 192h-64c-17 0-32-16-32-32h128c0 17-16 32-32 32zM356.5 400c-83.5 0-104.5-16-116.5-28-12 12-33 28-116.5 28s-123.5-23-123.5-39v-293c14.5 8 59.5 24 107 28 57.5 4.5 117-4.5 117-16 0-8 4-15.5 15.5-16 0 0 0 0 0.5 0 0 0 0 0 0.5 0 11.5 0.5 15.5 8 15.5 16 0 11.5 59.5 20.5 117 16 47-3.5 92.5-20 107-28v293c0 16-40 39-123.5 39zM224 114c-15 8-51.5 14-96 14s-85-6-96-13.5c0 0 0 205.5 0 221.5s32 29.5 96 29.5 96-13.5 96-29.5 0-222 0-222zM448 114.5c-11 7.5-51.5 13.5-96 13.5s-81-6-96-14c0 0 0 206 0 222s32 29.5 96 29.5 96-13.5 96-29.5 0-221.5 0-221.5zM384 256h-64c-17 0-32-16-32-32h128c0 17-16 32-32 32zM384 320h-64c-17 0-32-16-32-32h128c0 17-16 32-32 32zM160 256h-64c-16 0-32-15-32-32h128c0 16-15 32-32 32zM160 192h-64c-16 0-32-15-32-32h128c0 16-15 32-32 32zM160 320h-64c-16 0-32-15-32-32h128c0 16-15 32-32 32z" /> +<glyph unicode="C" d="M256 448q27.5 0 52-8.917t43.5-24.584 34.166-37.25 25.25-46.666 15.75-53.25q16.834-84.5 35.75-133.584t49.584-79.75h-195.667q3.666-10.5 3.666-21.334 0-26.5-18.75-45.25t-45.25-18.75-45.25 18.75-18.75 45.25q0 10.834 3.667 21.334h-195.667q30.666 30.666 49.584 79.75t35.75 133.584q5.666 28.167 15.75 53.25t25.25 46.666 34.166 37.25 43.5 24.584 52 8.916zM256 64q-8.834 0-15.084-6.25t-6.25-15.084 6.25-15.084 15.084-6.25 15.084 6.25 6.25 15.084-6.25 15.084-15.084 6.25zM256 405.333q-19.334 0-36.416-6t-29.25-15.334-22.584-22.416-16.916-25.417-11.75-26.333-7.75-23.084-4.166-17.75q-21.334-107.5-47-162.334h351.666q-25.666 54.834-47 162.334-1.666 8.667-4.166 17.75t-7.75 23.083-11.75 26.334-16.916 25.416-22.584 22.417-29.25 15.333-36.416 6z" /> +<glyph unicode="D" d="M248.082 216.068c-31.52 31.542-39.979 77.105-26.020 116.542-15.25-5.395-29.668-13.833-41.854-26.020-43.751-43.75-43.751-114.667 0-158.395 43.729-43.73 114.625-43.752 158.374 0 12.229 12.186 20.646 26.604 26.021 41.854-39.415-13.959-84.999-5.5-116.521 26.019z" /> +<glyph unicode="E" d="M349.852 136.85c-49.876-49.916-131.083-49.916-181 0-49.916 49.917-49.916 131.125 0 181.021 13.209 13.187 29.312 23.25 47.832 29.812 5.834 2.042 12.293 0.562 16.625-3.792 4.376-4.375 5.855-10.833 3.793-16.625-12.542-35.375-4-73.666 22.249-99.917 26.209-26.228 64.501-34.75 99.917-22.25 5.792 2.062 12.271 0.583 16.625-3.792 4.376-4.333 5.834-10.812 3.771-16.625-6.521-18.52-16.604-34.623-29.812-47.832zM191.477 295.246c-37.438-37.438-37.438-98.354 0-135.771 40-40.021 108.125-36.417 143 8.167-35.959-2.25-71.375 10.729-97.75 37.084-26.375 26.354-39.333 61.771-37.084 97.729-2.874-2.251-5.604-4.647-8.166-7.209z" /> +<glyph unicode="F" d="M256 480c-97.216 0-176-78.784-176-176 0-64.496 59.008-132.848 80.496-192.88 32.048-89.52 28.496-143.12 95.504-143.12 68 0 63.44 53.344 95.504 142.752 21.552 60.16 80.496 129.248 80.496 193.248 0 97.216-78.816 176-176 176zM297.472 45.184l-79.328-9.904c-2.832 8.192-5.872 17.776-9.568 30.288-0.048 0.16-0.112 0.336-0.144 0.496l99.008 12.368c-1.408-4.72-2.912-9.68-4.224-14.128-2.096-7.184-3.968-13.424-5.744-19.12zM203.776 81.472c-2.912 9.632-6.192 19.776-9.84 30.528h124.256c-1.968-5.744-3.936-11.504-5.632-16.944l-108.784-13.584zM256 0c-16.208 0-23.664 1.872-31.952 20l67.808 8.496c-9.824-26.464-16.976-28.496-35.856-28.496zM330.752 144h-149.328c-7.968 17.28-17.536 34.56-26.976 51.472-20.88 37.36-42.448 76-42.448 108.528 0 79.408 64.592 144 144 144s144-64.592 144-144c0-32.288-21.6-71.136-42.496-108.72-9.344-16.848-18.848-34.096-26.752-51.28zM256 400c4.4 0 8-3.584 8-8s-3.584-8-8-8c-44.112 0-80-35.888-80-80 0-4.416-3.584-8-8-8s-8 3.584-8 8c0 52.944 43.056 96 96 96z" /> +<glyph unicode="G" d="M12.572 140.286q-1.428 4.857 1.143 8.286l51.428 70.857-51.428 70.857q-2.572 3.714-1.143 8.286 1.143 4.286 5.715 5.714l83.428 27.429v87.428q0 4.572 3.715 7.428 4.285 2.857 8.285 1.143l83.428-26.857 51.429 70.857q2.571 3.428 7.428 3.428t7.428-3.428l51.428-70.857 83.428 26.857q4 1.714 8.286-1.143 3.714-2.857 3.714-7.428v-87.428l83.428-27.428q4.572-1.429 5.714-5.714 1.428-4.572-1.143-8.286l-51.428-70.857 51.428-70.857q2.572-3.428 1.143-8.286-1.143-4.286-5.714-5.714l-83.428-27.428v-87.428q0-4.572-3.714-7.428-4.286-2.857-8.286-1.143l-83.428 26.857-51.428-70.857q-2.857-3.715-7.428-3.715t-7.428 3.714l-51.428 70.857-83.428-26.857q-4-1.714-8.285 1.143-3.715 2.857-3.715 7.428v87.428l-83.428 27.428q-4.572 1.428-5.715 5.714zM91.428 219.429q0-33.428 13-63.857t35.143-52.572 52.572-35.143 63.857-13 63.857 13 52.572 35.143 35.143 52.572 13 63.857-13 63.857-35.143 52.572-52.572 35.143-63.857 13-63.857-13-52.572-35.143-35.143-52.572-13-63.857z" /> +<glyph unicode="a" d="M477.428 313.714q0-11.428-8-19.428l-245.714-245.714q-8-8-19.428-8t-19.428 8l-142.286 142.286q-8 8-8 19.428t8 19.428l38.857 38.857q8 8 19.428 8t19.428-8l84-84.286 187.429 187.714q8 8 19.428 8t19.428-8l38.857-38.857q8-8 8-19.429z" /> +<glyph unicode="b" d="M370.857 97.714q0-11.428-8-19.428l-38.857-38.857q-8-8-19.428-8t-19.428 8l-84 84-84-84q-8-8-19.428-8t-19.428 8l-38.857 38.857q-8 8-8 19.428t8 19.428l84 84-84 84q-8 8-8 19.428t8 19.428l38.857 38.857q8 8 19.428 8t19.428-8l84-84 84 84q8 8 19.428 8t19.428-8l38.857-38.857q8-8 8-19.428t-8-19.428l-84-84 84-84q8-8 8-19.428z" horiz-adv-x="403" /> +<glyph unicode="c" d="M475.428 45.714v219.429q-9.143-10.286-19.714-18.857-76.572-58.857-121.714-96.572-14.572-12.286-23.714-19.143t-24.714-13.857-29.286-7h-0.571q-13.714 0-29.285 7t-24.714 13.857-23.714 19.143q-45.143 37.714-121.715 96.572-10.572 8.572-19.715 18.857v-219.428q0-3.714 2.715-6.428t6.428-2.714h420.572q3.714 0 6.428 2.714t2.714 6.428zM475.428 346v7t-0.143 3.714-0.857 3.572-1.572 2.572-2.572 2.143-4 0.715h-420.572q-3.715 0-6.428-2.715t-2.715-6.428q0-48 42-81.143 55.143-43.428 114.571-90.571 1.714-1.428 10-8.428t13.143-10.714 12.714-9 14.429-7.857 12.286-2.572h0.571q5.714 0 12.286 2.572t14.428 7.857 12.714 9 13.143 10.714 10 8.428q59.428 47.143 114.572 90.572 15.428 12.286 28.714 33t13.286 37.572zM512 356.572v-310.857q0-18.857-13.428-32.286t-32.286-13.428h-420.572q-18.857 0-32.285 13.428t-13.428 32.286v310.857q0 18.857 13.428 32.286t32.285 13.428h420.572q18.857 0 32.286-13.428t13.428-32.286z" /> +<glyph unicode="d" d="M512 272.572v-226.857q0-18.857-13.428-32.286t-32.286-13.428h-420.572q-18.857 0-32.285 13.428t-13.428 32.286v226.857q12.572-14 28.857-24.857 103.429-70.286 142-98.572 16.285-12 26.429-18.714t27-13.714 31.428-7h0.572q14.572 0 31.428 7t27 13.714 26.428 18.714q48.572 35.143 142.286 98.572 16.286 11.143 28.572 24.857zM512 356.572q0-22.572-14-43.143t-34.857-35.143q-107.428-74.571-133.714-92.857-2.857-2-12.143-8.714t-15.428-10.857-14.857-9.286-16.428-7.714-14.286-2.572h-0.571q-6.571 0-14.286 2.572t-16.429 7.714-14.857 9.286-15.429 10.857-12.143 8.714q-26 18.286-74.857 52.143t-58.572 40.715q-17.715 12-33.428 33t-15.714 39q0 22.286 11.857 37.143t33.857 14.857h420.572q18.572 0 32.143-13.428t13.572-32.286z" /> +<glyph unicode="e" d="M307.143 137.143q0-3.714-2.857-6.572l-14.286-14.286q-2.857-2.857-6.572-2.857t-6.572 2.857l-112.286 112.286-112.285-112.286q-2.857-2.857-6.572-2.857t-6.572 2.857l-14.285 14.286q-2.857 2.857-2.857 6.572t2.857 6.572l133.143 133.143q2.857 2.857 6.571 2.857t6.572-2.857l133.143-133.143q2.857-2.857 2.857-6.572z" horiz-adv-x="329" /> +<glyph unicode="f" d="M307.143 265.143q0-3.715-2.857-6.572l-133.143-133.143q-2.857-2.857-6.571-2.857t-6.572 2.857l-133.143 133.143q-2.857 2.857-2.857 6.571t2.857 6.572l14.285 14.286q2.857 2.857 6.572 2.857t6.572-2.857l112.285-112.286 112.285 112.286q2.857 2.857 6.572 2.857t6.572-2.857l14.286-14.286q2.857-2.857 2.857-6.572z" horiz-adv-x="329" /> +<glyph unicode="g" d="M219.429 438.857q59.715 0 110.143-29.428t79.857-79.857 29.428-110.143-29.428-110.143-79.857-79.857-110.143-29.428-110.143 29.428-79.857 79.857-29.428 110.143 29.428 110.143 79.857 79.857 110.143 29.428zM256 82.572v54.286q0 4-2.571 6.714t-6.286 2.714h-54.857q-3.714 0-6.572-2.857t-2.857-6.572v-54.286q0-3.714 2.857-6.572t6.572-2.857h54.857q3.714 0 6.286 2.714t2.571 6.714zM255.429 180.857l5.143 177.429q0 3.428-2.857 5.143-2.857 2.286-6.857 2.286h-62.857q-4 0-6.857-2.286-2.857-1.714-2.857-5.143l4.857-177.429q0-2.857 2.857-5t6.857-2.143h52.857q4 0 6.714 2.143t3 5z" horiz-adv-x="439" /> +<glyph unicode="h" d="M24-32h400c13.904 0 24 10.096 24 24v448c0 13.904-10.096 24-24 24h-64c-4.416 0-8-3.584-8-8s3.584-8 8-8h64c5.008 0 8-2.992 8-8v-448c0-5.008-2.992-8-8-8h-400c-5.008 0-8 2.992-8 8v448c0 5.008 2.992 8 8 8h64c4.416 0 8 3.584 8 8s-3.584 8-8 8h-64c-13.904 0-24-10.096-24-24v-448c0-13.904 10.096-24 24-24zM88 400c4.416 0 8 3.584 8 8s-3.584 8-8 8h-32c-4.416 0-8-3.584-8-8v-384c0-4.416 3.584-8 8-8h336c4.416 0 8 3.584 8 8v384c0 4.416-3.584 8-8 8h-32c-4.416 0-8-3.584-8-8s3.584-8 8-8h24v-368h-320v368h24zM144 368h160c19.44 0 32 12.56 32 32v72c0 4.416-3.584 8-8 8h-208c-4.416 0-8-3.584-8-8v-72c0-19.44 12.56-32 32-32zM128 464h192v-64c0-10.624-5.392-16-16-16h-160c-10.608 0-16 5.376-16 16v64zM136 224h176c4.416 0 8 3.584 8 8s-3.584 8-8 8h-176c-4.416 0-8-3.584-8-8s3.584-8 8-8zM136 288h176c4.416 0 8 3.584 8 8s-3.584 8-8 8h-176c-4.416 0-8-3.584-8-8s3.584-8 8-8zM136 160h176c4.416 0 8 3.584 8 8s-3.584 8-8 8h-176c-4.416 0-8-3.584-8-8s3.584-8 8-8zM136 96h176c4.416 0 8 3.584 8 8s-3.584 8-8 8h-176c-4.416 0-8-3.584-8-8s3.584-8 8-8z" horiz-adv-x="448" /> +<glyph unicode="i" d="M384-32.656h-288c-17.68 0-32 14.288-32 31.936v385.552c0 17.632 14.32 31.92 32 31.92h80v-33.264h-64c-8.832 0-16-7.136-16-15.968v-352.272c0-8.848 7.168-15.968 16-15.968h256c8.848 0 16 7.12 16 15.968v352.272c0 8.832-7.152 15.968-16 15.968h-64v33.264h80c17.664 0 32-14.304 32-31.92v-385.552c0-17.648-14.336-31.936-32-31.936zM176 287.696h176v-15.968h-176v15.968zM176 239.792h176v-15.968h-176v15.968zM176 191.872h176v-15.968h-176v15.968zM176 143.968h176v-15.968h-176v15.968zM352 32.224h-176v15.936h176v-15.936zM176 96.064h176v-15.968h-176v15.968zM128 288.016h16v-15.968h-16v15.968zM128 240.112h16v-15.968h-16v15.968zM128 192.224h16v-15.968h-16v15.968zM128 144.32h16v-15.968h-16v15.968zM144 32.528h-16v15.968h16v-15.968zM128 96.4h16v-15.968h-16v15.968zM336 366.544c8.848 0 16-7.152 16-15.984 0-8.816 0-30.928 0-30.928h-224c0 0 0 22.112 0 30.928 0 8.832 7.152 15.984 16 15.984h48c0 0 0.192 22.704 0.192 48.656 0 26.944 20.8 49.472 47.808 49.472s48.528-23.68 48.528-49.632c0-27.952-0.528-48.496-0.528-48.496h48zM240 416.752c-8.848 0-16-7.136-16-15.952 0-8.832 7.152-15.968 16-15.968s16 7.136 16 15.968c0 8.816-7.152 15.952-16 15.952z" /> +<glyph unicode="j" d="M256 336c-61.75 0-112-50.25-112-112s50.25-112 112-112 112 50.25 112 112c0 61.75-50.25 112-112 112zM256 144c-44.188 0-80 35.812-80 80s35.812 80 80 80 80-35.812 80-80-35.812-80-80-80zM256 368c8.833 0 16 7.167 16 16v32c0 8.833-7.167 16-16 16s-16-7.167-16-16v-32c0-8.833 7.167-16 16-16zM256 80c-8.833 0-16-7.167-16-16v-32c0-8.833 7.167-16 16-16s16 7.167 16 16v32c0 8.833-7.167 16-16 16zM380.438 325.833l22.625 22.625c6.25 6.25 6.25 16.375 0 22.625s-16.375 6.25-22.625 0l-22.625-22.625c-6.25-6.25-6.25-16.375 0-22.625s16.375-6.25 22.625 0zM131.562 122.166l-22.625-22.625c-6.25-6.249-6.25-16.374 0-22.624s16.375-6.25 22.625 0l22.625 22.624c6.25 6.271 6.25 16.376 0 22.625-6.249 6.251-16.375 6.272-22.625 0zM112 224c0 8.833-7.167 16-16 16h-32c-8.833 0-16-7.167-16-16s7.167-16 16-16h32c8.833 0 16 7.167 16 16zM448 240h-32c-8.833 0-16-7.167-16-16s7.167-16 16-16h32c8.833 0 16 7.167 16 16s-7.167 16-16 16zM131.541 325.833c6.251-6.25 16.376-6.25 22.625 0 6.251 6.25 6.251 16.375 0 22.625l-22.625 22.625c-6.25 6.25-16.374 6.25-22.625 0-6.25-6.25-6.25-16.375 0-22.625l22.625-22.625zM380.459 122.188c-6.271 6.25-16.376 6.25-22.625 0-6.251-6.25-6.271-16.375 0-22.625l22.625-22.625c6.249-6.25 16.374-6.25 22.624 0s6.25 16.374 0 22.625l-22.624 22.625z" /> +<glyph unicode="k" d="M434.381 358.4c0 25.6-25.472 25.6-25.472 25.6h-305.817c0 0-25.472 0-25.472-25.6v-25.6h356.761v25.6zM357.913 435.2h-203.853c0 0-25.472 0-25.472-25.6h254.822c0 25.6-25.498 25.6-25.498 25.6zM485.325 332.8c-15.079 15.155-15.079 15.155-15.079 15.155v-40.755h-428.519v40.755c0 0 0 0-15.078-15.155s-25.881-19.226-19.789-51.225c6.016-31.872 35.277-206.72 39.603-230.375 4.787-25.959 31.155-25.6 31.155-25.6h356.762c0 0 26.368-0.358 31.155 25.6 4.352 23.654 33.587 198.503 39.629 230.375 6.067 32-4.736 36.070-19.84 51.226zM357.913 194.56c0 0 0-25.6-25.498-25.6h-152.857c-25.472 0-25.472 25.6-25.472 25.6v51.2h35.686v-40.96h132.429v40.96h35.738v-51.2z" /> +<glyph unicode="l" d="M432 416h-32v-32h16v-256h-128v-128h-192v384h16v32h-32c-8.801 0-16-7.2-16-16v-416c0-8.8 7.199-16 16-16h252l116 116v316c0 8.8-7.2 16-16 16zM320 0v96h96l-96-96zM384 416h-64v32c0 17.6-14.4 32-32 32h-64c-17.602 0-32-14.4-32-32v-32h-64v-64h256v64zM288 416h-64v31.943c0.017 0.019 0.036 0.039 0.057 0.057h63.884c0.021-0.018 0.041-0.038 0.059-0.057v-31.943z" /> +<glyph unicode="m" d="M496.131 44.302l-121.276 103.147c-12.537 11.283-25.945 16.463-36.776 15.963 28.628 33.534 45.921 77.039 45.921 124.588 0 106.039-85.961 192-192 192s-192-85.961-192-192c0-106.039 85.961-192 192-192 47.549 0 91.054 17.293 124.588 45.922-0.5-10.831 4.68-24.239 15.963-36.776l103.147-121.276c17.661-19.623 46.511-21.277 64.11-3.678s15.946 46.449-3.677 64.11zM192 160c-70.692 0-128 57.308-128 128s57.308 128 128 128 128-57.308 128-128-57.307-128-128-128z" /> +<glyph unicode="n" d="M192 224h-160v64h160v64l96-96-96-96zM512 480v-416l-192-96v96h-192v128h32v-96h160v288l128 64h-288v-128h-32v160z" /> +<glyph unicode="o" d="M0 480v-512h512v512h-512zM480 0h-448v448h448v-448zM384 368l-160-160-96 96-64-64 160-160 224 224-64 64z" /> +<glyph unicode="p" d="M0 480v-512h512v512h-512zM480 0h-448v448h448v-448z" /> +<glyph unicode="q" d="M256 480c-141.385 0-256-114.615-256-256s114.615-256 256-256 256 114.615 256 256-114.615 256-256 256zM256 32c-106.039 0-192 85.961-192 192s85.961 192 192 192c106.039 0 192-85.961 192-192s-85.961-192-192-192zM160 224c0 53.019 42.981 96 96 96s96-42.981 96-96c0-53.019-42.981-96-96-96s-96 42.981-96 96z" /> +<glyph unicode="r" d="M256 480c-141.385 0-256-114.615-256-256s114.615-256 256-256 256 114.615 256 256-114.615 256-256 256zM256 32c-106.039 0-192 85.961-192 192s85.961 192 192 192c106.039 0 192-85.961 192-192s-85.961-192-192-192z" /> +<glyph unicode="s" d="M256 480c-141.385 0-256-114.615-256-256s114.615-256 256-256 256 114.615 256 256-114.615 256-256 256zM256 16c-114.875 0-208 93.125-208 208s93.125 208 208 208 208-93.125 208-208-93.125-208-208-208zM224 352h64v-64h-64zM320 96h-128v32h32v96h-32v32h96v-128h32z" /> +<glyph unicode="t" d="M128 320c0 70.692 57.308 128 128 128s128-57.308 128-128c0-70.692-57.308-128-128-128s-128 57.308-128 128zM384 160h-256c-70.692 0-128-57.309-128-128v-32h512v32c0 70.691-57.308 128-128 128z" /> +<glyph unicode="u" d="M424-32h-336c-13.255 0-24 10.745-24 24 0 34.431 14.534 76.102 38.879 111.471 20.346 29.559 45.342 51.81 72.339 64.805-11.899 10.822-22.147 24.013-30.254 39.061-13 24.131-19.873 52.024-19.873 80.664 0 39.803 13.102 77.411 36.893 105.895 24.832 29.73 58.221 46.104 94.016 46.104s69.184-16.374 94.017-46.105c23.79-28.484 36.893-66.091 36.893-105.895 0-28.64-6.872-56.533-19.872-80.664-8.107-15.048-18.355-28.239-30.255-39.061 26.997-12.995 51.993-35.246 72.339-64.805 24.344-35.368 38.878-77.039 38.878-111.47 0-13.255-10.745-24-24-24zM115.037 16h281.926c-4.613 19.67-14.26 41.192-27.381 60.255-20.727 30.113-46.588 49.811-72.819 55.463-11.053 2.382-18.944 12.155-18.944 23.462v17.146c0 8.63 4.633 16.595 12.135 20.861 29.739 16.916 48.956 54.133 48.956 94.813 0 57.346-37.192 104-82.909 104s-82.909-46.654-82.909-104c0-40.68 19.217-77.896 48.957-94.813 7.501-4.267 12.134-12.231 12.134-20.861v-17.146c0-11.307-7.892-21.080-18.944-23.462-26.231-5.652-52.092-25.35-72.82-55.463-13.123-19.063-22.768-40.585-27.382-60.255z" /> +<glyph unicode="v" d="M0 219.429q0 43.714 16.428 83.572t44.572 69 67.286 47 82.857 19.572q12.571 0.571 17.429-11.143 5.143-11.714-4.286-20.572-24.572-22.285-37.572-51.857t-13-62.428q0-42.286 20.857-78t56.571-56.572 78-20.857q33.714 0 65.143 14.572 11.714 5.143 20.572-3.714 4-4 5-9.714t-1.286-10.857q-26.857-58-81-92.714t-118.143-34.714q-44.571 0-85.143 17.428t-70 46.857-46.857 70-17.428 85.143zM36.572 219.429q0-37.143 14.572-71t39-58.286 58.285-39 71-14.572q41.143 0 78.143 17.572t63 49q-15.428-2.572-31.428-2.572-52 0-96.286 25.714t-70 70-25.714 96.286q0 54.857 29.714 102-57.428-17.143-93.857-65.428t-36.428-109.714z" /> +<glyph unicode="w" d="M256 336c-61.75 0-112-50.25-112-112s50.25-112 112-112 112 50.251 112 112c0 61.75-50.25 112-112 112zM256 368c8.833 0 16 7.146 16 16v32c0 8.833-7.167 16-16 16-8.854 0-16-7.167-16-16v-32c0-8.854 7.146-16 16-16zM256 80c-8.854 0-16-7.167-16-16v-32c0-8.854 7.146-16 16-16 8.833 0 16 7.146 16 16v32c0 8.833-7.167 16-16 16zM380.417 325.833l22.625 22.625c6.25 6.25 6.25 16.375 0 22.625s-16.375 6.25-22.625 0l-22.625-22.625c-6.251-6.25-6.251-16.375 0-22.625 6.25-6.249 16.374-6.249 22.625 0zM131.541 122.146l-22.623-22.625c-6.252-6.25-6.252-16.376 0-22.625 6.249-6.25 16.373-6.25 22.623 0l22.625 22.625c6.251 6.291 6.251 16.375 0 22.625-6.249 6.25-16.374 6.292-22.625 0zM112 224c0 8.833-7.167 16-16 16h-32c-8.854 0-16-7.167-16-16 0-8.854 7.146-16 16-16h32c8.833 0 16 7.146 16 16zM448 240h-32c-8.854 0-16-7.167-16-16 0-8.854 7.146-16 16-16h32c8.833 0 16 7.146 16 16 0 8.833-7.167 16-16 16zM131.521 325.833c6.249-6.25 16.375-6.25 22.625 0 6.249 6.25 6.249 16.375 0 22.625l-22.625 22.625c-6.25 6.25-16.376 6.25-22.625 0-6.25-6.25-6.25-16.375 0-22.625l22.625-22.625zM380.459 122.188c-6.293 6.25-16.376 6.25-22.625 0s-6.293-16.375 0-22.625l22.625-22.625c6.249-6.249 16.374-6.249 22.625 0 6.249 6.25 6.249 16.376 0 22.625l-22.625 22.625z" /> +<glyph unicode="x" d="M288 128c-17.664 0-32 14.336-32 32v32c0 17.68 14.336 32 32 32h192v-96h-192zM312 198.672c-13.248 0-24-10.752-24-24 0-13.264 10.752-24 24-24 13.264 0 24 10.736 24 24 0 13.248-10.736 24-24 24zM240 192v-32c0-26.496 21.504-48 48-48h160v-64c0-26.496-21.488-48-48-48h-336c-26.496 0-48 21.504-48 48v304c0 26.512 21.504 48 48 48h154.112l-37.584-15.664h-124.512c-13.248 0-24-10.752-24-24 0-13.264 10.752-24 24-24h391.984v-96.336h-160c-26.496 0-48-21.488-48-48zM80.016 304h-32v-32h16v16h16v16zM80.016 256h-32v-32h16.656l-0.656 16 16-0.336v16.336zM80.016 208h-32v-32h16.336l-0.336 16h16v16zM80.016 160h-32v-32h16.336l-0.336 16 16-0.336v16.336zM80.016 112h-32v-32h16v16h16v16zM80.016 64h-32v-32h16.336l-0.336 15.664h16v16.336zM144.016 352.992l188 79.008 36-79.008h-224zM362.336 400h37.664c14.016 0 26.496-6.096 35.28-15.664h-65.296l-7.648 15.664z" /> +<glyph unicode="y" d="M256 160c0 17.673 14.327 32 32 32s32-14.327 32-32c0-17.673-14.327-32-32-32s-32 14.327-32 32zM464.016 272c-0.016 0-0.016 0 0 0l-0.016 96v48c0 26.512-21.504 48-48 48h-328c-48.528 0-88-39.488-88-88v-320c0-48.512 39.472-88 88-88h288c48.512 0 88 39.488 88 88v24c0 0 0 0 0.016 0 63.968 48.016 63.968 143.984 0 192zM88 432h328c8.816 0 16-7.168 16-16v-98.944c-5.024 1.792-10.368 2.944-16 2.944h-0.016v80c0 8.848-7.168 16-16 16h-336c-8.832 0-16-7.152-16-16v-63.088c-9.872 10.096-15.984 23.856-15.984 39.088 0 30.928 25.056 56 56 56zM399.984 384h-336v16h336v-16zM399.984 368v-16h-336v16h336zM399.984 336v-16h-311.984c-8.64 0-16.704 2.112-24 5.6v10.4h335.984zM432 56c0-30.928-25.072-56-56-56h-288c-30.944 0-56 25.072-56 56v252.176c15.216-12.592 34.736-20.176 56-20.176h328c8.816 0 16-7.168 16-16v-32h-144c-44.192 0-80-35.808-80-80s35.824-80 80-80h144v-24zM452.432 112h-164.432c-26.464 0-48 21.536-48 48s21.536 48 48 48h144c9.872 0.128 19.664 4.912 25.632 12.864 1.664 2.24 2.96 4.752 4 7.376 0.144 0.352 0.4 0.624 0.528 0.992 11.504-15.088 17.84-33.584 17.84-53.232 0-24.608-9.936-47.44-27.568-64z" /> +<glyph unicode="z" d="M544 416h-512c-17.6 0-32-14.4-32-32v-320c0-17.6 14.4-32 32-32h512c17.6 0 32 14.4 32 32v320c0 17.6-14.4 32-32 32zM320 352h64v-64h-64v64zM416 256v-64h-64v64h64zM224 352h64v-64h-64v64zM320 256v-64h-64v64h64zM128 352h64v-64h-64v64zM224 256v-64h-64v64h64zM64 352h32v-64h-32v64zM64 256h64v-64h-64v64zM96 96h-32v64h32v-64zM384 96h-256v64h256v-64zM512 96h-96v64h96v-64zM512 192h-64v64h64v-64zM512 288h-96v64h96v-64z" horiz-adv-x="576" /> +<glyph unicode="{" d="M256 480c-141.385 0-256-35.817-256-80v-48l192-192v-160c0-17.673 28.653-32 64-32s64 14.327 64 32v160l192 192v48c0 44.183-114.615 80-256 80zM47.192 410.588c11.972 6.829 28.791 13.31 48.639 18.744 43.972 12.038 100.854 18.668 160.169 18.668s116.197-6.63 160.169-18.668c19.848-5.434 36.667-11.915 48.64-18.744 7.896-4.503 12.162-8.312 14.148-10.588-1.986-2.276-6.253-6.084-14.148-10.588-11.973-6.829-28.792-13.31-48.64-18.744-43.971-12.038-100.854-18.668-160.169-18.668s-116.197 6.63-160.169 18.668c-19.848 5.434-36.667 11.915-48.639 18.744-7.896 4.504-12.162 8.312-14.149 10.588 1.987 2.276 6.253 6.084 14.149 10.588z" /> +</font></defs></svg> \ No newline at end of file diff --git a/snf-admin-app/synnefo_admin/admin/static/fonts/font-icons.ttf b/snf-admin-app/synnefo_admin/admin/static/fonts/font-icons.ttf new file mode 100755 index 0000000000000000000000000000000000000000..056baee76b5974bf610f8616c9a8fc905637f636 Binary files /dev/null and b/snf-admin-app/synnefo_admin/admin/static/fonts/font-icons.ttf differ diff --git a/snf-admin-app/synnefo_admin/admin/static/fonts/font-icons.woff b/snf-admin-app/synnefo_admin/admin/static/fonts/font-icons.woff new file mode 100755 index 0000000000000000000000000000000000000000..d12e4a45ce4266086c381a89b3a299e4164cb428 Binary files /dev/null and b/snf-admin-app/synnefo_admin/admin/static/fonts/font-icons.woff differ diff --git a/snf-admin-app/synnefo_admin/admin/static/fonts/snf-font.eot b/snf-admin-app/synnefo_admin/admin/static/fonts/snf-font.eot new file mode 100644 index 0000000000000000000000000000000000000000..bcbdc442d1de9eac65d6eb1d9031ecd861be681a Binary files /dev/null and b/snf-admin-app/synnefo_admin/admin/static/fonts/snf-font.eot differ diff --git a/snf-admin-app/synnefo_admin/admin/static/fonts/snf-font.svg b/snf-admin-app/synnefo_admin/admin/static/fonts/snf-font.svg new file mode 100644 index 0000000000000000000000000000000000000000..a65d6bd6b13e1acbc15d6b1104958d82dc7d2cc7 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/fonts/snf-font.svg @@ -0,0 +1,643 @@ +<?xml version="1.0" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" > +<svg xmlns="http://www.w3.org/2000/svg"> +<metadata> +This is a custom SVG font generated by IcoMoon. +<iconset grid="14"></iconset> +</metadata> +<defs> +<font id="snf-font" horiz-adv-x="448" > +<font-face units-per-em="448" ascent="384" descent="-64" /> +<missing-glyph horiz-adv-x="448" /> +<glyph class="hidden" unicode="" d="M0,384L 448 -64L0 -64 z" horiz-adv-x="0" /> +<glyph unicode="(" d="M 224.001,384L0,160L 140,160L 140-63.999L 308-64L 308,160L 448,160 z" /> +<glyph unicode="*" d="M 371-64L 77-64 c-11.598,0-21,9.402-21,21c0,30.127, 12.717,66.589, 34.019,97.537c 17.803,25.864, 39.674,45.334, 63.297,56.704 + c-10.412,9.469-19.379,21.011-26.472,34.178c-11.375,21.115-17.389,45.521-17.389,70.581c0,34.828, 11.464,67.735, 32.281,92.658 + C 163.464,334.673, 192.679,349, 224,349s 60.536-14.327, 82.265-40.342c 20.816-24.924, 32.281-57.83, 32.281-92.658 + c0-25.060-6.013-49.466-17.388-70.581c-7.094-13.167-16.061-24.709-26.473-34.178c 23.622-11.371, 45.494-30.84, 63.297-56.704 + C 379.283,23.589, 392-12.873, 392-43C 392-54.598, 382.598-64, 371-64z M 100.657-22l 246.685,0 + c-4.036,17.211-12.477,36.043-23.958,52.723c-18.136,26.349-40.764,43.585-63.717,48.53c-9.671,2.084-16.576,10.636-16.576,20.529 + l0,15.003 c0,7.551, 4.054,14.521, 10.618,18.253c 26.022,14.802, 42.837,47.366, 42.837,82.961c0,50.178-32.543,91-72.545,91 + c-40.002,0-72.545-40.822-72.545-91c0-35.595, 16.815-68.159, 42.837-82.961c 6.563-3.734, 10.617-10.702, 10.617-18.253l0-15.003 + c0-9.894-6.905-18.445-16.576-20.529c-22.952-4.946-45.581-22.181-63.717-48.53C 113.134,14.043, 104.695-4.789, 100.657-22z" /> +<glyph unicode="q" d="M 422.422,278.412l-4.396-4.396l-80.164,80.206l 4.382,4.382c0,0, 19.194,25.382, 46.494,25.382 + c 12.026,0, 25.648-4.942, 39.886-19.18C 475.58,317.85, 422.422,278.412, 422.422,278.412z M 391.706,247.682L 118.776-25.374l0-0.014 l-0.028-0.014L 118.72-25.402 L0-64 + l 38.584,118.776l0,0 l0,0.014l0,0.042l 0.028,0 L 311.556,327.902l 3.528-3.542 M 38.64-25.346l 59.318,17.066l-40.516,43.148L 38.64-25.346z" /> +<glyph unicode="e" d="M 284-62.739l-58.31,82.381l-58.278-82.381l-9.28,100.48L 66.502-4.48l 42.208,91.686L 8.192,96.506l 82.406,58.259L 8.186,213.005 + l 100.518,9.299L 66.496,313.978l 91.635-42.221l 9.28,100.512l 58.278-82.406l 58.31,82.406l 9.248-100.512l 91.699,42.221l-42.24-91.667 + l 100.48-9.299l-82.381-58.246l 82.381-58.259l-100.48-9.299l 42.24-91.686l-91.699,42.227L 284-62.739z M 169.216,56.941l 7.757-84.006 + l 48.717,68.87l 48.742-68.87l 7.731,84.006l 76.659-35.302L 323.52,98.278l 84,7.776l-68.87,48.704l 68.87,48.691l-84,7.776 + l 35.309,76.621l-76.659-35.296L 274.432,336.582l-48.742-68.883L 176.973,336.582L 169.216,252.557l-76.595,35.296l 35.277-76.621L 43.878,203.456 + l 68.896-48.691l-68.896-48.704l 84.019-7.776l-35.283-76.64L 169.216,56.941z" /> +<glyph unicode="G" d="M 155.437,26.113L 155.437,270.046 c0,6.597, 2.565,64.622, 76.024,64.622c 73.985,0, 78.218-58.532, 78.218-63.889l0-158.93 l 53.65,0 + l-0.006,161.579c-1.889,36.957-36.593,106.098-130.751,106.098c-94.534,0-128.557-71.050-128.557-108.741l0-244.614 l-34.256,0 l0-85.685 l 308.438,0 l0,85.685 L 155.437,26.171 z" /> +<glyph unicode="F" d="M 356.192,22.126l-0.013,252.055c-1.844,36.262-33.606,107.832-126.044,107.832c-92.807,0-123.895-66.090-123.895-103.098 + l0-256.795 l-33.665,0 l0-84.14 l 302.893,0 l0,84.14 L 356.192,22.12 z M 156.677,278.175c0,6.512, 2.519,63.486, 74.66,63.486c 72.667,0, 76.809-57.441, 76.809-62.752l0-256.788 l-151.424,0 L 156.677,278.175 z" /> +<glyph unicode="k" d="M 310.208,376.173L 143.635,142.925L 253.83,142.925L 204.909-17.786L 376.48,215.398L 274.24,215.398 z" /> +<glyph unicode="o" d="M 223.744-57.6C 103.853-57.6, 6.317,39.923, 6.317,159.802c0,119.891, 97.536,217.434, 217.427,217.434c 119.917,0, 217.478-97.536, 217.478-217.434 + C 441.222,39.923, 343.661-57.6, 223.744-57.6z M 223.744,364.429c-112.832,0-204.627-91.802-204.627-204.634C 19.117,46.982, 110.912-44.8, 223.744-44.8 + c 112.858,0, 204.678,91.782, 204.678,204.602C 428.422,272.634, 336.602,364.429, 223.744,364.429zM 199.872,254.285c0,14.931, 8.518,22.387, 25.549,22.387c 17.062,0, 25.581-7.456, 25.581-22.387c0-7.098-2.131-12.634-6.381-16.602 + c-4.269-3.942-10.682-5.914-19.2-5.914C 208.397,231.776, 199.872,239.283, 199.872,254.285zM 201.875,185.914L 248.838,185.914L 248.838,42.957L 201.875,42.957z" /> +<glyph unicode="w" d="M 412.8-60.8l-390.4,0 l0,102.4 l 166.4,0 l0,38.4 l-147.2,0 l0,288 l 352,0 l0-288 l-128,0 l0-38.4 l 147.2,0 L 412.8-60.8 z M 35.2-48l 364.8,0 l0,76.8 l-147.2,0 l0,64 l 128,0 l0,262.4 l-326.4,0 l0-262.4 l 147.2,0 l0-64 l-166.4,0 L 35.2-48 z" /> +<glyph unicode="v" d="M 252.173,371.213c-5.459,5.459-12.704,8.454-20.416,8.454c-7.712,0-14.957-3.008-20.397-8.454 + c-5.446-5.446-8.442-12.678-8.435-20.39c 0.006-7.699, 3.014-14.931, 8.461-20.358c 5.427-5.453, 12.666-8.448, 20.378-8.448 + c 7.706,0, 14.95,3.002, 20.422,8.448C 263.379,341.702, 263.379,359.974, 252.173,371.213z M 243.136,339.52c-6.074-6.054-16.646-6.080-22.701,0 + c-3.034,3.027-4.704,7.046-4.71,11.322c0,4.282, 1.664,8.301, 4.691,11.334c 3.027,3.034, 7.053,4.698, 11.341,4.698 + c 4.294,0, 8.333-1.677, 11.36-4.698C 249.35,355.917, 249.35,345.754, 243.136,339.52zM 56.314,136.986c 7.731,0, 14.989,3.034, 20.397,8.493c 5.44,5.389, 8.442,12.595, 8.461,20.288c 0.019,7.744-2.976,15.014-8.435,20.474 + c-5.446,5.459-12.678,8.461-20.384,8.461c-7.699,0-14.944-3.002-20.403-8.454c-11.245-11.264-11.245-29.574, 0.006-40.832 + C 41.402,139.981, 48.634,136.986, 56.314,136.986z M 44.992,177.203c 3.040,3.034, 7.072,4.704, 11.354,4.704c 4.275,0, 8.294-1.67, 11.328-4.704 + c 3.040-3.040, 4.704-7.085, 4.691-11.398c-0.006-4.269-1.67-8.256-4.71-11.27c-3.027-3.066-7.059-4.742-11.341-4.742 + c-4.269,0-8.288,1.67-11.315,4.685C 38.739,160.736, 38.739,170.931, 44.992,177.203zM 151.578,37.286c-7.693,0-14.938-2.989-20.403-8.435c-5.459-5.472-8.448-12.736-8.429-20.461c 0.019-7.693, 3.034-14.912, 8.474-20.32 + c 5.44-5.427, 12.678-8.41, 20.39-8.41c 7.706,0, 14.95,2.982, 20.429,8.422c 11.162,11.206, 11.168,29.485-0.032,40.8 + C 166.515,34.304, 159.264,37.286, 151.578,37.286z M 162.989-2.854c-6.074-6.042-16.672-6.054-22.746,0 + c-3.021,3.002-4.691,7.008-4.698,11.277c-0.013,4.301, 1.651,8.352, 4.672,11.373c 3.040,3.034, 7.072,4.691, 11.36,4.691 + c 4.294,0, 8.352-1.67, 11.373-4.659C 169.178,13.542, 169.184,3.354, 162.989-2.854zM 404.435,81.139C 398.982,86.592, 391.731,89.6, 384.032,89.6c-7.706,0-14.944-3.008-20.384-8.461c-11.194-11.194-11.2-29.472,0-40.781 + c 5.446-5.44, 12.685-8.429, 20.397-8.429s 14.944,2.995, 20.416,8.461C 415.61,51.661, 415.603,69.946, 404.435,81.139z M 395.386,49.414 + c-6.048-6.054-16.653-6.010-22.669-0.019c-6.234,6.285-6.246,16.467-0.013,22.701C 375.731,75.13, 379.757,76.8, 384.032,76.8 + c 4.282,0, 8.314-1.67, 11.347-4.704C 401.594,65.862, 401.581,55.68, 395.386,49.414zM 377.485,258.214c 16.704,0, 28.826,10.95, 28.826,26.035c0,15.93-12.934,28.89-28.826,28.89c-15.878,0-28.794-12.954-28.794-28.89 + C 348.691,268.922, 360.531,258.214, 377.485,258.214z M 377.485,300.339c 8.838,0, 16.026-7.219, 16.026-16.090c0-9.766-8.634-13.235-16.026-13.235 + c-7.379,0-15.994,3.469-15.994,13.235C 361.491,293.12, 368.666,300.339, 377.485,300.339zM 59.706,129.299c-15.91,0-28.858-12.909-28.858-28.774c0-15.93, 12.941-28.89, 28.858-28.89c 15.878,0, 28.794,12.954, 28.794,28.89 + C 88.499,116.397, 75.584,129.299, 59.706,129.299z M 59.706,84.435c-8.851,0-16.058,7.219-16.058,16.090c0,8.806, 7.2,15.974, 16.058,15.974 + c 8.819,0, 15.994-7.168, 15.994-15.974C 75.699,91.654, 68.525,84.435, 59.706,84.435zM 260.973,1.997c-15.878,0-28.8-12.934-28.8-28.838c0-15.936, 12.922-28.902, 28.8-28.902c 15.923,0, 28.883,12.96, 28.883,28.902 + C 289.856-10.938, 276.902,1.997, 260.973,1.997z M 260.973-42.938c-8.819,0-16,7.226-16,16.102c0,8.845, 7.174,16.038, 16,16.038 + c 8.87,0, 16.083-7.194, 16.083-16.038C 277.056-35.866, 269.997-42.938, 260.973-42.938zM 333.843,194.349c-15.91,0-28.851-12.922-28.851-28.8c0-15.898, 12.941-28.832, 28.851-28.832c 15.891,0, 28.819,12.934, 28.819,28.832 + C 362.662,181.434, 349.734,194.349, 333.843,194.349z M 333.843,149.517c-8.851,0-16.051,7.194-16.051,16.032c0,8.826, 7.2,16, 16.051,16 + c 8.832,0, 16.019-7.174, 16.019-16C 349.862,156.71, 342.675,149.517, 333.843,149.517zM 56.307,256.902c 7.731,0, 14.989,3.034, 20.397,8.493c 5.44,5.389, 8.442,12.595, 8.461,20.288c 0.019,7.744-2.976,15.014-8.435,20.474 + c-5.446,5.459-12.678,8.461-20.384,8.461c-7.699,0-14.944-3.002-20.403-8.454c-11.245-11.264-11.245-29.574, 0.006-40.838 + C 41.402,259.891, 48.634,256.902, 56.307,256.902z M 44.992,297.114c 3.040,3.034, 7.072,4.704, 11.354,4.704c 4.275,0, 8.294-1.67, 11.328-4.704 + c 3.040-3.040, 4.704-7.085, 4.691-11.398c-0.006-4.269-1.67-8.256-4.71-11.27c-6.042-6.112-16.589-6.112-22.656-0.064 + C 38.739,280.646, 38.739,290.842, 44.992,297.114zM 260.87,1.99l-34.394,149.619l 130.394-81.606c 1.338,3.994, 3.488,7.763, 6.624,10.95L 239.11,158.797l 66.726,0.122 + c-0.512,2.138-0.845,4.346-0.845,6.637c0,2.118, 0.262,4.173, 0.698,6.163l-69.44-0.128l 123.699,91.539 + c-3.725,2.432-6.643,5.683-8.57,9.581L 224.301,178.669l 11.59,143.757c-1.363-0.198-2.726-0.403-4.128-0.403 + c-2.989,0-5.862,0.582-8.627,1.453l-11.789-146.163L 82.899,274.534c-1.434-3.373-3.507-6.483-6.189-9.139 + c-0.429-0.435-0.947-0.755-1.402-1.165l 122.272-92.557l-113.133,0.397c 0.448-2.054, 0.73-4.147, 0.723-6.304 + c-0.006-2.227-0.326-4.384-0.819-6.496l 100.563-0.352L 83.264,116.979c 2.438-3.482, 4.064-7.52, 4.774-11.878l 116.896,48.237l-48.416-116.531 + c 4.269-0.73, 8.269-2.413, 11.808-4.934l 46.701,112.403l 33.395-145.286C 252.205,0.845, 256.397,1.978, 260.87,1.99z" /> +<glyph unicode="u" d="M 239.872,223.802c-22.918,0-41.562-18.662-41.562-41.6c0-22.906, 18.643-41.536, 41.562-41.536c 22.912,0, 41.549,18.63, 41.549,41.536 + C 281.421,205.139, 262.784,223.802, 239.872,223.802z M 239.872,153.466c-15.859,0-28.762,12.896-28.762,28.736c0,15.878, 12.902,28.8, 28.762,28.8 + c 15.853,0, 28.749-12.922, 28.749-28.8C 268.621,166.355, 255.725,153.466, 239.872,153.466zM 435.053,182.202c0,107.603-87.558,195.149-195.181,195.149c-107.61,0-195.155-87.546-195.155-195.149 + c0-13.075, 1.318-25.837, 3.776-38.202l-3.776,0 c0-22.547, 4.326-44.589, 12.096-65.286l-43.539-22.758l 48.717-57.971l 25.267,26.848 + c 35.75-45.299, 90.771-75.802, 152.448-75.802c 107.514,0, 194.976,92.563, 194.976,194.963l-3.411,0 C 433.734,156.365, 435.053,169.126, 435.053,182.202 + z M 62.528,17.235l-29.203,34.758l 28.352,14.81l 11.328,5.92l 14.445,7.552l 0.96,0.499l 66.707,34.854L 62.528,17.235z M 63.859,97.971 + c 1.997-4.154, 4.154-8.224, 6.438-12.211l-2.118-1.114C 66.56,89.024, 65.101,93.466, 63.859,97.971z M 239.706-38.163 + c-58.355,0-110.336,29.114-143.693,72.307l 7.872,8.365c 35.174-34.266, 83.123-55.456, 135.994-55.456c 76.787,0, 143.328,44.576, 175.168,109.197 + C 393.472,21.19, 323.066-38.163, 239.706-38.163z M 239.872-0.147c-49.472,0-94.355,19.866-127.232,51.962l 77.792,82.675l-7.629,10.061 + l-101.171-52.87c-15.334,26.688-24.122,57.594-24.122,90.522c0,100.55, 81.805,182.349, 182.355,182.349c 100.563,0, 182.381-81.798, 182.381-182.349 + S 340.435-0.147, 239.872-0.147z" /> +<glyph unicode="m" d="M 266.81-54.848L 177.645-54.848 L 177.645,114.938 L 0.666,114.938 L 0.666,201.299 l 176.979,0 L 177.645,373.197 l 89.171,0 l0-171.891 l 176.979,0 l0-86.362 L 266.81,114.944 L 266.81-54.848 z M 190.445-42.048l 63.571,0 + L 254.016,127.738 l 176.979,0 l0,60.762 L 254.010,188.499 L 254.010,360.397 l-63.571,0 l0-171.891 L 13.466,188.506 l0-60.762 l 176.979,0 L 190.445-42.048 z" /> +<glyph unicode="f" d="M 438.4-67.2l-435.2,0 l0,441.6 l 435.2,0 L 438.4-67.2 z M 16-54.4l 409.6,0 l0,416 l-409.6,0 L 16-54.4 zM 342.4,16l-236.8,0 l0,57.6 l 96,0 l0,44.8 l-96,0 l0,185.6 l 236.8,0 l0-185.6 l-96,0 l0-44.8 l 96,0 L 342.4,16 z M 118.4,28.8l 211.2,0 l0,32 l-96,0 l0,70.4 l 96,0 l0,160 l-211.2,0 l0-160 l 96,0 l0-70.4 l-96,0 L 118.4,28.8 z" /> +<glyph unicode="E" d="M 22.586,362.874c-6.95,0-12.602-5.651-12.602-12.64c0-6.963, 5.651-12.589, 12.602-12.589c 6.912,0, 12.608,5.626, 12.608,12.589 + C 35.194,357.222, 29.498,362.874, 22.586,362.874zM 22.586,320.045c-6.95,0-12.602-5.664-12.602-12.646c0-6.918, 5.651-12.576, 12.602-12.576c 6.912,0, 12.608,5.658, 12.608,12.576 + C 35.194,314.381, 29.498,320.045, 22.586,320.045zM 22.586,277.178c-6.95,0-12.602-5.683-12.602-12.602c0-6.989, 5.651-12.646, 12.602-12.646c 6.912,0, 12.608,5.658, 12.608,12.646 + C 35.194,271.494, 29.498,277.178, 22.586,277.178zM 22.586,234.31c-6.95,0-12.602-5.683-12.602-12.634c0-6.957, 5.651-12.621, 12.602-12.621c 6.912,0, 12.608,5.664, 12.608,12.621 + C 35.194,228.634, 29.498,234.31, 22.586,234.31zM 22.586,191.424c-6.95,0-12.602-5.651-12.602-12.614c0-6.925, 5.651-12.582, 12.602-12.582c 6.912,0, 12.608,5.658, 12.608,12.582 + C 35.194,185.773, 29.498,191.424, 22.586,191.424zM 22.586,148.595c-6.95,0-12.602-5.651-12.602-12.608c0-6.95, 5.651-12.646, 12.602-12.646c 6.912,0, 12.608,5.696, 12.608,12.646 + C 35.194,142.944, 29.498,148.595, 22.586,148.595zM 22.586,105.722c-6.95,0-12.602-5.645-12.602-12.634c0-6.938, 5.651-12.595, 12.602-12.595c 6.912,0, 12.608,5.658, 12.608,12.595 + C 35.194,100.077, 29.498,105.722, 22.586,105.722zM 22.586,62.835c-6.95,0-12.602-5.613-12.602-12.602c0-6.963, 5.651-12.621, 12.602-12.621c 6.912,0, 12.608,5.658, 12.608,12.621 + C 35.194,57.222, 29.498,62.835, 22.586,62.835zM 22.586,19.981c-6.95,0-12.602-5.645-12.602-12.608c0-6.989, 5.651-12.589, 12.602-12.589c 6.912,0, 12.608,5.6, 12.608,12.589 + C 35.194,14.336, 29.498,19.981, 22.586,19.981zM 22.586-22.886c-6.95,0-12.602-5.651-12.602-12.602c0-6.963, 5.651-12.614, 12.602-12.614c 6.912,0, 12.608,5.651, 12.608,12.614 + C 35.194-28.538, 29.498-22.886, 22.586-22.886zM 65.69,362.874c-6.97,0-12.634-5.651-12.634-12.64c0-6.963, 5.664-12.589, 12.634-12.589c 6.912,0, 12.576,5.626, 12.576,12.589 + C 78.266,357.222, 72.602,362.874, 65.69,362.874zM 65.69,320.045c-6.97,0-12.634-5.664-12.634-12.646c0-6.918, 5.664-12.576, 12.634-12.576c 6.912,0, 12.576,5.658, 12.576,12.576 + C 78.266,314.381, 72.602,320.045, 65.69,320.045zM 65.69,277.178c-6.97,0-12.634-5.683-12.634-12.602c0-6.989, 5.664-12.646, 12.634-12.646c 6.912,0, 12.576,5.658, 12.576,12.646 + C 78.266,271.494, 72.602,277.178, 65.69,277.178zM 65.69,234.31c-6.97,0-12.634-5.683-12.634-12.634c0-6.957, 5.664-12.621, 12.634-12.621c 6.912,0, 12.576,5.664, 12.576,12.621 + C 78.266,228.634, 72.602,234.31, 65.69,234.31zM 65.69,191.424c-6.97,0-12.634-5.651-12.634-12.614c0-6.925, 5.664-12.582, 12.634-12.582c 6.912,0, 12.576,5.658, 12.576,12.582 + C 78.266,185.773, 72.602,191.424, 65.69,191.424zM 65.69,148.595c-6.97,0-12.634-5.651-12.634-12.608c0-6.95, 5.664-12.646, 12.634-12.646c 6.912,0, 12.576,5.696, 12.576,12.646 + C 78.266,142.944, 72.602,148.595, 65.69,148.595zM 65.69,105.722c-6.97,0-12.634-5.645-12.634-12.634c0-6.938, 5.664-12.595, 12.634-12.595c 6.912,0, 12.576,5.658, 12.576,12.595 + C 78.266,100.077, 72.602,105.722, 65.69,105.722zM 65.69,62.835c-6.97,0-12.634-5.613-12.634-12.602c0-6.963, 5.664-12.621, 12.634-12.621c 6.912,0, 12.576,5.658, 12.576,12.621 + C 78.266,57.222, 72.602,62.835, 65.69,62.835zM 65.69,19.981c-6.97,0-12.634-5.645-12.634-12.608c0-6.989, 5.664-12.589, 12.634-12.589c 6.912,0, 12.576,5.6, 12.576,12.589 + C 78.266,14.336, 72.602,19.981, 65.69,19.981zM 65.69-22.886c-6.97,0-12.634-5.651-12.634-12.602c0-6.963, 5.664-12.614, 12.634-12.614c 6.912,0, 12.576,5.651, 12.576,12.614 + C 78.266-28.538, 72.602-22.886, 65.69-22.886zM 108.768,362.874c-6.95,0-12.589-5.651-12.589-12.64c0-6.963, 5.638-12.589, 12.589-12.589c 6.938,0, 12.589,5.626, 12.589,12.589 + C 121.35,357.222, 115.706,362.874, 108.768,362.874zM 108.768,320.045c-6.95,0-12.589-5.664-12.589-12.646c0-6.918, 5.638-12.576, 12.589-12.576c 6.938,0, 12.589,5.658, 12.589,12.576 + C 121.35,314.381, 115.706,320.045, 108.768,320.045zM 108.768,19.981c-6.95,0-12.589-5.645-12.589-12.608c0-6.989, 5.638-12.589, 12.589-12.589c 6.938,0, 12.589,5.6, 12.589,12.589 + C 121.35,14.336, 115.706,19.981, 108.768,19.981zM 96.16-35.494A12.608,12.608 1620 1 1 121.376-35.49400000000003A12.608,12.608 1620 1 1 96.16-35.49400000000003zM 151.859,362.874c-6.925,0-12.589-5.651-12.589-12.64c0-6.963, 5.664-12.589, 12.589-12.589c 6.97,0, 12.614,5.626, 12.614,12.589 + C 164.474,357.222, 158.822,362.874, 151.859,362.874zM 151.859,320.045c-6.925,0-12.589-5.664-12.589-12.646c0-6.918, 5.664-12.576, 12.589-12.576c 6.97,0, 12.614,5.658, 12.614,12.576 + C 164.474,314.381, 158.822,320.045, 151.859,320.045zM 151.859,19.981c-6.925,0-12.589-5.645-12.589-12.608c0-6.989, 5.664-12.589, 12.589-12.589c 6.97,0, 12.614,5.6, 12.614,12.589 + C 164.474,14.336, 158.822,19.981, 151.859,19.981zM 151.859-22.886c-6.925,0-12.589-5.651-12.589-12.602c0-6.963, 5.664-12.614, 12.589-12.614c 6.97,0, 12.614,5.651, 12.614,12.614 + C 164.474-28.538, 158.822-22.886, 151.859-22.886zM 194.976,362.874c-6.957,0-12.602-5.651-12.602-12.64c0-6.963, 5.645-12.589, 12.602-12.589c 6.912,0, 12.57,5.626, 12.57,12.589 + C 207.546,357.222, 201.888,362.874, 194.976,362.874zM 194.976,320.045c-6.957,0-12.602-5.664-12.602-12.646c0-6.918, 5.645-12.576, 12.602-12.576c 6.912,0, 12.57,5.658, 12.57,12.576 + C 207.546,314.381, 201.888,320.045, 194.976,320.045zM 194.976,19.981c-6.957,0-12.602-5.645-12.602-12.608c0-6.989, 5.645-12.589, 12.602-12.589c 6.912,0, 12.57,5.6, 12.57,12.589 + C 207.546,14.336, 201.888,19.981, 194.976,19.981zM 194.976-22.886c-6.957,0-12.602-5.651-12.602-12.602c0-6.963, 5.645-12.614, 12.602-12.614c 6.912,0, 12.57,5.651, 12.57,12.614 + C 207.546-28.538, 201.888-22.886, 194.976-22.886zM 238.067,362.874c-6.97,0-12.589-5.651-12.589-12.64c0-6.963, 5.619-12.589, 12.589-12.589c 6.938,0, 12.57,5.626, 12.57,12.589 + C 250.637,357.222, 245.005,362.874, 238.067,362.874zM 238.067,320.045c-6.97,0-12.589-5.664-12.589-12.646c0-6.918, 5.619-12.576, 12.589-12.576c 6.938,0, 12.57,5.658, 12.57,12.576 + C 250.637,314.381, 245.005,320.045, 238.067,320.045zM 238.067,19.981c-6.97,0-12.589-5.645-12.589-12.608c0-6.989, 5.619-12.589, 12.589-12.589c 6.938,0, 12.57,5.6, 12.57,12.589 + C 250.637,14.336, 245.005,19.981, 238.067,19.981zM 225.459-35.494A12.608,12.608 1620 1 1 250.675-35.49400000000003A12.608,12.608 1620 1 1 225.459-35.49400000000003zM 281.133,362.874c-6.931,0-12.576-5.651-12.576-12.64c0-6.963, 5.651-12.589, 12.576-12.589c 6.944,0, 12.595,5.626, 12.595,12.589 + C 293.728,357.222, 288.077,362.874, 281.133,362.874zM 281.133,320.045c-6.931,0-12.576-5.664-12.576-12.646c0-6.918, 5.651-12.576, 12.576-12.576c 6.944,0, 12.595,5.658, 12.595,12.576 + C 293.728,314.381, 288.077,320.045, 281.133,320.045zM 281.133,19.981c-6.931,0-12.576-5.645-12.576-12.608c0-6.989, 5.651-12.589, 12.576-12.589c 6.944,0, 12.595,5.6, 12.595,12.589 + C 293.728,14.336, 288.077,19.981, 281.133,19.981zM 281.133-22.886c-6.931,0-12.576-5.651-12.576-12.602c0-6.963, 5.651-12.614, 12.576-12.614c 6.944,0, 12.595,5.651, 12.595,12.614 + C 293.728-28.538, 288.077-22.886, 281.133-22.886zM 324.243,362.874c-6.97,0-12.595-5.651-12.595-12.64c0-6.963, 5.626-12.589, 12.595-12.589c 6.925,0, 12.608,5.626, 12.608,12.589 + C 336.851,357.222, 331.168,362.874, 324.243,362.874zM 324.243,320.045c-6.97,0-12.595-5.664-12.595-12.646c0-6.918, 5.626-12.576, 12.595-12.576c 6.925,0, 12.608,5.658, 12.608,12.576 + C 336.851,314.381, 331.168,320.045, 324.243,320.045zM 324.243,19.981c-6.97,0-12.595-5.645-12.595-12.608c0-6.989, 5.626-12.589, 12.595-12.589c 6.925,0, 12.608,5.6, 12.608,12.589 + C 336.851,14.336, 331.168,19.981, 324.243,19.981zM 324.243-22.886c-6.97,0-12.595-5.651-12.595-12.602c0-6.963, 5.626-12.614, 12.595-12.614c 6.925,0, 12.608,5.651, 12.608,12.614 + C 336.851-28.538, 331.168-22.886, 324.243-22.886zM 367.366,362.874c-6.989,0-12.64-5.651-12.64-12.64c0-6.963, 5.651-12.589, 12.64-12.589c 6.931,0, 12.57,5.626, 12.57,12.589 + C 379.942,357.222, 374.304,362.874, 367.366,362.874zM 367.366,320.045c-6.989,0-12.64-5.664-12.64-12.646c0-6.918, 5.651-12.576, 12.64-12.576c 6.931,0, 12.57,5.658, 12.57,12.576 + C 379.942,314.381, 374.304,320.045, 367.366,320.045zM 367.366,277.178c-6.989,0-12.64-5.683-12.64-12.602c0-6.989, 5.651-12.646, 12.64-12.646c 6.931,0, 12.57,5.658, 12.57,12.646 + C 379.942,271.494, 374.304,277.178, 367.366,277.178zM 367.366,234.31c-6.989,0-12.64-5.683-12.64-12.634c0-6.957, 5.651-12.621, 12.64-12.621c 6.931,0, 12.57,5.664, 12.57,12.621 + C 379.942,228.634, 374.304,234.31, 367.366,234.31zM 367.366,191.424c-6.989,0-12.64-5.651-12.64-12.614c0-6.925, 5.651-12.582, 12.64-12.582c 6.931,0, 12.57,5.658, 12.57,12.582 + C 379.942,185.773, 374.304,191.424, 367.366,191.424zM 367.366,148.595c-6.989,0-12.64-5.651-12.64-12.608c0-6.95, 5.651-12.646, 12.64-12.646c 6.931,0, 12.57,5.696, 12.57,12.646 + C 379.942,142.944, 374.304,148.595, 367.366,148.595zM 367.366,105.722c-6.989,0-12.64-5.645-12.64-12.634c0-6.938, 5.651-12.595, 12.64-12.595c 6.931,0, 12.57,5.658, 12.57,12.595 + C 379.942,100.077, 374.304,105.722, 367.366,105.722zM 367.366,62.835c-6.989,0-12.64-5.613-12.64-12.602c0-6.963, 5.651-12.621, 12.64-12.621c 6.931,0, 12.57,5.658, 12.57,12.621 + C 379.942,57.222, 374.304,62.835, 367.366,62.835zM 367.366,19.981c-6.989,0-12.64-5.645-12.64-12.608c0-6.989, 5.651-12.589, 12.64-12.589c 6.931,0, 12.57,5.6, 12.57,12.589 + C 379.942,14.336, 374.304,19.981, 367.366,19.981zM 367.366-22.886c-6.989,0-12.64-5.651-12.64-12.602c0-6.963, 5.651-12.614, 12.64-12.614c 6.931,0, 12.57,5.651, 12.57,12.614 + C 379.942-28.538, 374.304-22.886, 367.366-22.886zM 410.438,362.874c-6.97,0-12.595-5.651-12.595-12.64c0-6.963, 5.626-12.589, 12.595-12.589c 6.938,0, 12.544,5.626, 12.544,12.589 + C 422.982,357.222, 417.376,362.874, 410.438,362.874zM 410.438,320.045c-6.97,0-12.595-5.664-12.595-12.646c0-6.918, 5.626-12.576, 12.595-12.576c 6.938,0, 12.544,5.658, 12.544,12.576 + C 422.982,314.381, 417.376,320.045, 410.438,320.045zM 410.438,277.178c-6.97,0-12.595-5.683-12.595-12.602c0-6.989, 5.626-12.646, 12.595-12.646c 6.938,0, 12.544,5.658, 12.544,12.646 + C 422.982,271.494, 417.376,277.178, 410.438,277.178zM 410.438,234.31c-6.97,0-12.595-5.683-12.595-12.634c0-6.957, 5.626-12.621, 12.595-12.621c 6.938,0, 12.544,5.664, 12.544,12.621 + C 422.982,228.634, 417.376,234.31, 410.438,234.31zM 410.438,191.424c-6.97,0-12.595-5.651-12.595-12.614c0-6.925, 5.626-12.582, 12.595-12.582c 6.938,0, 12.544,5.658, 12.544,12.582 + C 422.982,185.773, 417.376,191.424, 410.438,191.424zM 410.438,148.595c-6.97,0-12.595-5.651-12.595-12.608c0-6.95, 5.626-12.646, 12.595-12.646c 6.938,0, 12.544,5.696, 12.544,12.646 + C 422.982,142.944, 417.376,148.595, 410.438,148.595zM 410.438,105.722c-6.97,0-12.595-5.645-12.595-12.634c0-6.938, 5.626-12.595, 12.595-12.595c 6.938,0, 12.544,5.658, 12.544,12.595 + C 422.982,100.077, 417.376,105.722, 410.438,105.722zM 410.438,62.835c-6.97,0-12.595-5.613-12.595-12.602c0-6.963, 5.626-12.621, 12.595-12.621c 6.938,0, 12.544,5.658, 12.544,12.621 + C 422.982,57.222, 417.376,62.835, 410.438,62.835zM 410.438,19.981c-6.97,0-12.595-5.645-12.595-12.608c0-6.989, 5.626-12.589, 12.595-12.589c 6.938,0, 12.544,5.6, 12.544,12.589 + C 422.982,14.336, 417.376,19.981, 410.438,19.981zM 410.438-22.886c-6.97,0-12.595-5.651-12.595-12.602c0-6.963, 5.626-12.614, 12.595-12.614c 6.938,0, 12.544,5.651, 12.544,12.614 + C 422.982-28.538, 417.376-22.886, 410.438-22.886zM 434.829-58.726L 2.522-58.726 L 2.522,373.536 l 432.307,0 L 434.829-58.726 z M 15.322-45.926l 406.707,0 L 422.029,360.736 L 15.322,360.736 L 15.322-45.926 z M 343.904,32.198l-250.432,0 L 93.472,282.611 l 250.432,0 + L 343.904,32.198 z M 106.266,44.998l 224.832,0 L 331.098,269.811 l-224.832,0 L 106.266,44.998 zM 317.274,58.803l-197.184,0 L 120.090,255.962 l 197.184,0 L 317.274,58.803 z M 132.89,71.603l 171.584,0 L 304.474,243.162 l-171.584,0 L 132.89,71.603 z" /> +<glyph unicode="H" d="M 393.6,163.2L 393.6,258.746 c0,3.61, 6.4,9.107, 6.4,15.104l0,17.722 c0,10.688-10.605,18.829-21.286,18.829l-3.277,0 + c-10.694,0-20.237-8.141-20.237-18.829l0-17.722 c0-5.984, 6.4-11.469, 6.4-15.098L 361.6,163.2 l-352,0 l0-166.4 l 422.4,0 l0,166.4 L 393.6,163.2 z M 371.277,267.981 + c-2.266,1.114-3.277,3.418-3.277,5.869l0,17.722 c0,3.629, 3.802,6.029, 7.437,6.029l 3.277,0 c 3.622,0, 8.486-2.4, 8.486-6.029l0-17.722 + c0-2.502-0.966-4.749-3.251-5.862L 380.8,266.24l0-103.040 l-6.4,0 L 374.4,266.221 L 371.277,267.981z M 419.2,9.6l-396.8,0 l0,140.8 l 340.947,0 l 27.443,0 L 419.2,150.4 L 419.2,9.6 zM 361.523,81.926c0-10.106, 8.198-18.323, 18.278-18.323c 10.080,0, 18.278,8.218, 18.278,18.323c0,10.093-8.198,18.298-18.278,18.298 + C 369.722,100.224, 361.523,92.019, 361.523,81.926z M 385.28,81.926c0-3.046-2.458-5.523-5.478-5.523s-5.478,2.483-5.478,5.523 + c0,3.034, 2.458,5.498, 5.478,5.498S 385.28,84.954, 385.28,81.926zM 321.28,100.224c-10.080,0-18.285-8.205-18.285-18.298c0-10.106, 8.205-18.323, 18.285-18.323c 10.099,0, 18.31,8.218, 18.31,18.323 + C 339.59,92.019, 331.379,100.224, 321.28,100.224z M 321.28,76.403c-3.027,0-5.485,2.483-5.485,5.523c0,3.034, 2.458,5.498, 5.485,5.498 + c 2.989,0, 5.51-2.522, 5.51-5.498C 326.79,78.88, 324.32,76.403, 321.28,76.403zM 262.739,100.224c-10.080,0-18.278-8.205-18.278-18.298c0-10.106, 8.198-18.323, 18.278-18.323c 10.080,0, 18.278,8.218, 18.278,18.323 + C 281.018,92.019, 272.819,100.224, 262.739,100.224z M 262.739,76.403c-3.021,0-5.478,2.483-5.478,5.523c0,3.034, 2.458,5.498, 5.478,5.498 + s 5.478-2.47, 5.478-5.498C 268.218,78.88, 265.76,76.403, 262.739,76.403z" /> +<glyph unicode="#" d="M 246.4-60.8l-89.6,0 l0,54.125 l 30.765,18.202L 156.8,29.734l0,29.037 l 30.765,18.202L 156.8,95.187l0,71.040 + c-49.843,18.33-49.805,56.134-49.766,96.102l0,3.347 c0,59.488, 55.258,111.674, 118.253,111.674c 59.002,0, 100.205-45.926, 100.205-111.674 + c0-45.12-39.136-84.922-79.085-102.272L 246.406-60.8 z M 169.6-48l 64,0 L 233.6,172.026 l 4.045,1.6c 37.293,14.765, 75.046,51.117, 75.046,92.051 + c0,58.221-35.942,98.874-87.405,98.874c-56.173,0-105.453-46.208-105.453-98.874l0-3.36 c-0.038-41.005-0.070-70.637, 45.35-85.446 + l 4.416-1.446l0-72.934 l 43.085-25.51L 169.6,51.475l0-14.438 l 43.085-25.51L 169.6-13.978L 169.6-48 z M 228.653,272.966 + c-19.539,0-35.437,15.917-35.437,35.482c0,19.558, 15.898,35.462, 35.437,35.462c 19.546,0, 35.443-15.904, 35.443-35.462 + C 264.096,288.89, 248.192,272.966, 228.653,272.966z M 228.653,331.11c-12.48,0-22.637-10.17-22.637-22.662c0-12.506, 10.157-22.682, 22.637-22.682 + c 12.486,0, 22.643,10.176, 22.643,22.682C 251.296,320.941, 241.139,331.11, 228.653,331.11z" /> +<glyph unicode="@" d="M 297.331,228.531c0,25.664-15.328,48.378-39.066,57.869l-5.67-14.17c 17.626-7.059, 29.466-24.621, 29.466-43.706 + c0-25.901-21.203-46.963-47.264-46.963c-25.741,0-46.682,21.069-46.682,46.963c0,18.426, 9.709,34.067, 25.978,41.862l-6.598,13.766 + c-21.363-10.234-34.643-31.552-34.643-55.629c0-34.317, 27.789-62.234, 61.939-62.234C 269.293,166.291, 297.331,194.208, 297.331,228.531zM 227.2,299.635L 240.192,299.635L 240.192,215.174L 227.2,215.174zM 272.678,46.259l0,45.472 l 129.939,0 L 402.618,377.6 L 51.782,377.6 l0-285.869 l 149.434,0 l0-45.472 L 32.288,46.259 L 32.288-51.2 l 389.818,0 L 422.106,46.259 L 272.678,46.259 z M 415.61-44.698L 38.79-44.698 L 38.79,39.757 + l 168.922,0 l0,58.477 L 58.278,98.234 L 58.278,371.104 l 337.843,0 l0-272.87 L 266.182,98.234 l0-58.477 l 149.434,0 L 415.616-44.698 z" /> +<glyph unicode="!" d="M 296.934,231.437c0,25.274-15.699,48.531-39.072,57.882l-5.67-14.176c 17.638-7.053, 29.472-24.621, 29.472-43.712 + c0-25.894-21.203-46.963-47.258-46.963c-25.741,0-46.688,21.069-46.688,46.963c0,14.33, 5.933,26.931, 16.198,35.507l 2.336-19.718l 15.162,1.798 + L 216.32,291.904l-42.771,1.907l-0.685-15.251l 20.102-0.902c-12.915-11.456-20.506-28.045-20.506-46.234c0-34.31, 27.782-62.234, 61.946-62.234 + C 268.89,169.203, 296.934,197.12, 296.934,231.437zM 272.678,39.859l0,45.472 l 129.939,0 L 402.618,371.2 L 51.782,371.2 l0-285.869 l 149.434,0 l0-45.472 L 32.288,39.859 L 32.288-57.6 l 389.818,0 L 422.106,39.859 L 272.678,39.859 z M 415.61-51.098L 38.79-51.098 L 38.79,33.357 + l 168.922,0 l0,58.477 L 58.278,91.834 L 58.278,364.704 l 337.843,0 l0-272.87 L 266.182,91.834 l0-58.477 l 149.434,0 L 415.616-51.098 z" /> +<glyph unicode="0" d="M 391.686,184.198c0,93.242-75.885,169.107-169.165,169.107c-93.254,0-169.133-75.866-169.133-169.107 + c0-5.325, 0.282-10.586, 0.762-15.789c-0.493-5.203-0.762-10.47-0.762-15.802c0-21.338, 4.019-41.734, 11.27-60.55l-37.555-19.616l 39.174-46.541 + l 36.634,38.925c 30.63-30.714, 72.915-49.792, 119.61-49.792c 75.366,0, 139.36,49.562, 161.165,117.798c-9.786-80.333-78.362-142.771-161.299-142.771 + c-52.73,0-99.648,25.242-129.363,64.25l-4.442-4.717c 30.931-40.058, 79.392-65.933, 133.805-65.933c 93.171,0, 168.96,75.795, 168.96,168.947 + c0,4.64-0.23,9.222-0.608,13.76C 391.354,172.237, 391.686,178.182, 391.686,184.198z M 60.813,134.637c 3.923-12.781, 9.344-24.922, 16.019-36.218 + l-6.438-3.36C 65.632,107.597, 62.336,120.851, 60.813,134.637z M 66.541,35.526l-29.402,34.944l 122.515,63.994L 66.541,35.526z M 222.528,21.434 + c-44.998,0-85.786,18.368-115.277,48l 70.061,74.451l-3.814,5.030l-90.989-47.526c-14.413,24.275-22.72,52.589-22.72,82.81 + C 59.795,273.92, 132.794,346.906, 222.528,346.906c 78.406,0, 144.026-55.699, 159.366-129.6l-45.882-38.797l-64.307-28.73l-47.411,36.243l-10.566,43.341 + l 28.48,22.586l-3.974,5.011l-26.176-20.755l-4.64,19.008l-6.214-1.51L 217.030,188.8L 176,188.8 l0-6.4 l 42.579,0 l 0.026-0.096l 49.37-37.741l-6.579-113.664 + l 6.387-0.378l 2.56,44.282l 34.605-26.861l 3.923,5.050l-38.080,29.555l 3.558,61.402l 62.97,28.134l 42.010-31.405 + C 360.243,72, 297.203,21.434, 222.528,21.434z M 381.037,147.392l-38.266,28.602l 40.435,34.054c 1.35-8.422, 2.074-17.050, 2.074-25.85 + C 385.286,171.539, 383.789,159.232, 381.037,147.392z" /> +<glyph unicode="9" d="M 335.11,159.942L 338.464,165.395L 282.131,200.013L 222.778,231.859L 236.134,284.979L 312.16,294.592L 311.36,300.941L 231.002,290.784L 214.266,224.218L 183.68,284.8L 131.2,284.8L 131.2,278.4L 179.744,278.4L 211.693,215.098L 132.378,165.382L 135.776,159.955L 176,185.171L 176,150.4L 182.4,150.4L 182.4,189.184L 217.594,211.245L 221.171,225.459L 275.648,196.237L 245.011,159.578L 249.926,155.475L 281.299,193.018 zM 265.6,35.2l0,44.8 l 128,0 l0,281.6 l-345.6,0 l0-281.6 l 147.2,0 l0-44.8 l-166.4,0 l0-96 l 384,0 l0,96 L 265.6,35.2 z M 406.4-54.4l-371.2,0 l0,83.2 l 166.4,0 l0,57.6 l-147.2,0 l0,268.8 l 332.8,0 l0-268.8 l-128,0 l0-57.6 l 147.2,0 L 406.4-54.4 z" /> +<glyph unicode="8" d="M 403.661,253.613c-11.226,0-20.358-9.152-20.358-20.403c0-0.813, 0.134-1.574, 0.23-2.349l-66.266-19.104 + c-3.27,6.867-10.234,11.661-18.342,11.661c-11.232,0-20.378-9.114-20.378-20.314c0-11.232, 9.146-20.378, 20.378-20.378 + c 11.226,0, 20.358,9.146, 20.358,20.378c0,1.434-0.154,2.816-0.435,4.166l 65.805,18.97c 1.235-3.091, 3.341-5.658, 6.080-7.597l-45.952-70.611 + c-7.731,4.986-19.443,4.224-25.894-2.298c-7.258-7.251-7.757-18.701-1.67-26.714l-54.112-32.922c-3.661,3.475-8.595,5.645-14.035,5.645 + c-7.091,0-13.338-3.661-16.979-9.184l-33.274,24.090l0,0l-13.101,9.491l 13.274,30.688l 30.016-0.013c 0.544-4.499, 2.47-8.672, 5.709-11.91 + c 3.827-3.872, 8.934-6.010, 14.387-6.010c 5.44,0, 10.566,2.131, 14.464,6.003c 7.834,7.904, 7.827,20.813-0.038,28.794 + c-5.344,5.286-14.131,6.701-21.376,4.352l-10.912,21.376c 3.827,3.712, 6.221,8.883, 6.221,14.611c0,11.206-9.158,20.326-20.403,20.326 + c-11.213,0-20.333-9.12-20.333-20.326c0-8.864, 5.696-16.346, 13.574-19.155l-14.394-33.299l-30.010,0.013l-18.925,98.874 + c 7.078,2.758, 11.584,8.902, 11.584,16.832c0,11.238-9.133,20.39-20.352,20.39c-11.226,0-20.358-9.152-20.358-20.39 + c0-9.299, 6.176-16.16, 15.475-17.946l-18.118-62.298c-1.28,0.237-2.586,0.378-3.91,0.378c-5.478,0-10.726-2.125-14.355-5.805 + c-3.866-3.808-6.003-8.909-6.003-14.342c-0.013-5.453, 2.106-10.566, 5.965-14.419c 0.576-0.582, 1.229-1.037, 1.862-1.536L 53.018,151.488 + c-1.165,10.125-9.69,18.048-20.128,18.048c-11.226,0-20.358-9.133-20.358-20.352c0-11.226, 9.133-20.365, 20.358-20.365 + c 10.426,0, 18.944,7.91, 20.122,18.029L 134.4,146.842l 31.814-22.458l 1.882-9.805c-3.13-0.87-6.054-2.349-8.333-4.608 + c-3.853-3.859-5.971-8.96-5.971-14.394c 0.019-5.459, 2.15-10.566, 6.003-14.394c 3.834-3.853, 8.947-5.971, 14.4-5.971 + s 10.56,2.118, 14.381,5.965c 3.859,3.853, 5.984,8.96, 5.984,14.406c0,3.744-1.075,7.309-2.963,10.438l 38.394-27.795 + c-0.768-2.138-1.261-4.403-1.261-6.803c0-11.251, 9.126-20.403, 20.346-20.403c 11.251,0, 20.403,9.152, 20.403,20.403 + c0,4.058-1.222,7.814-3.29,10.989l 54.432,33.133c 3.597-2.874, 7.981-4.518, 12.672-4.518c 5.44,0, 10.541,2.112, 14.387,5.952 + c 7.667,7.744, 7.821,20.115, 0.576,28.051l 46.624,71.373c 2.611-1.018, 5.549-1.613, 8.787-1.613c 12,0, 20.39,7.578, 20.39,18.426 + C 424.051,244.461, 414.899,253.613, 403.661,253.613z M 298.925,187.482c-8.621,0-15.622,7.008-15.622,15.622c0,8.582, 7.008,15.565, 15.622,15.565 + c 8.608,0, 15.603-6.982, 15.603-15.565C 314.534,194.49, 307.533,187.482, 298.925,187.482z M 249.107,164.717c 4.224,0, 8.262-1.6, 11.066-4.371 + c 6.016-6.118, 6.029-16.032, 0.032-22.093c-5.965-5.939-16.237-5.958-22.125,0.006c-2.938,2.931-4.563,6.842-4.563,11.014 + c-0.006,4.173, 1.613,8.102, 4.55,11.046C 240.851,163.117, 244.877,164.717, 249.107,164.717z M 201.466,204.038 + c0,8.589, 6.989,15.578, 15.584,15.578c 8.634,0, 15.654-6.989, 15.654-15.578c0-8.634-7.027-15.648-15.654-15.648 + C 208.454,188.39, 201.466,195.405, 201.466,204.038z M 214.982,183.853c 0.691-0.077, 1.363-0.211, 2.067-0.211c 3.782,0, 7.283,1.101, 10.317,2.899 + l 10.426-20.422c-1.12-0.698-2.163-1.51-3.091-2.445c-3.283-3.296-5.203-7.539-5.715-12.102l-27.955,0.013L 214.982,183.853z + M 32.896,133.574c-8.608,0-15.603,7.002-15.603,15.61c0,8.602, 7.002,15.603, 15.603,15.603s 15.603-7.002, 15.603-15.603 + C 48.499,140.576, 41.498,133.574, 32.896,133.574z M 179.482,114.797c-2.189,0.518-4.442,0.71-6.701,0.576l-0.954,4.966L 179.482,114.797z + M 193.811,146.835l-12.038-27.834l-11.264,8.154l-3.782,19.686L 193.811,146.835z M 163.757,132.064l-20.518,14.867l 17.862-0.134L 163.757,132.064z + M 122.586,267.302c0,8.627, 7.002,15.635, 15.603,15.635c 8.602,0, 15.603-7.014, 15.603-15.635c0-8.186-6.266-13.69-15.603-13.69 + C 128.858,253.613, 122.586,259.11, 122.586,267.302z M 100.243,156.218c-2.957,2.95-4.582,6.874-4.57,11.059c0,4.16, 1.626,8.064, 4.602,10.995 + c 2.778,2.803, 6.79,4.416, 11.008,4.416c 4.224,0, 8.25-1.619, 11.066-4.454c 2.938-2.906, 4.55-6.803, 4.55-10.97 + c0-4.179-1.619-8.096-4.576-11.034C 116.435,150.317, 106.125,150.317, 100.243,156.218z M 122.675,150.4c 1.062,0.723, 2.074,1.542, 3.002,2.477 + c 0.16,0.16, 0.282,0.365, 0.435,0.531l 4.589-3.008L 122.675,150.4 z M 128.922,157.248c 1.728,3.027, 2.726,6.426, 2.726,10.010 + c0,5.446-2.118,10.534-5.946,14.323c-1.811,1.83-4.013,3.264-6.432,4.262l 18.515,63.046c 0.141,0, 0.269-0.032, 0.41-0.032 + c 1.408,0, 2.752,0.128, 4.051,0.326l 18.746-97.594l-24.269,0.013L 128.922,157.248z M 140.493,146.95l 0.064-0.051L 140.493,146.95L 140.493,146.95z + M 185.216,84.538c-5.862-5.901-16.173-5.907-22.074,0.006c-2.957,2.938-4.595,6.861-4.602,11.046c0,4.154, 1.626,8.077, 4.57,11.027 + c 2.816,2.797, 6.848,4.397, 11.066,4.397c 4.218,0, 8.243-1.6, 11.053-4.39c 2.957-2.95, 4.582-6.867, 4.582-11.034 + C 189.805,91.418, 188.179,87.494, 185.216,84.538z M 249.069,55.776c-8.602,0-15.597,7.021-15.597,15.648c0,8.589, 6.995,15.578, 15.597,15.578 + c 8.634,0, 15.648-6.989, 15.648-15.578C 264.717,62.797, 257.702,55.776, 249.069,55.776z M 344.314,120.326 + c-5.901-5.882-16.218-5.837-22.054-0.026c-6.061,6.125-6.067,16.032-0.013,22.093c 2.797,2.803, 6.81,4.416, 11.034,4.416 + c 4.211,0, 8.237-1.613, 11.040-4.422C 350.362,136.333, 350.355,126.432, 344.314,120.326z M 403.661,219.539c-9.338,0-15.603,5.491-15.603,13.677 + c0,8.634, 7.002,15.648, 15.603,15.648c 8.627,0, 15.635-7.021, 15.635-15.648C 419.302,225.030, 413.018,219.539, 403.661,219.539z" /> +<glyph unicode="7" d="M 405.024,100.55c-3.373,0-6.56-0.678-9.594-1.702c-24.224,32.8-79.098,68.166-132.582,79.808 + c 0.045,0.659, 0.192,1.267, 0.192,1.946c0,2.054-0.211,4.058-0.608,6.003c 57.286,12.883, 98.413-10.464, 120.71-29.21 + c-8.557-11.77-7.635-28.39, 2.963-39.072c 5.69-5.677, 13.267-8.8, 21.35-8.8c 8.045,0, 15.622,3.123, 21.357,8.826 + c 11.693,11.814, 11.686,30.957-0.013,42.662c-5.459,5.485-13.242,8.634-21.363,8.634c-7.629,0-14.886-2.861-20.262-7.744 + c-23.642,19.565-67.162,44.115-126.669,30.752c-3.014,6.918-8.544,12.467-15.424,15.546c 19.635,36.243, 59.142,83.616, 100.365,101.92 + c 5.28-6.714, 13.946-10.81, 24.614-10.81c 17.798,0, 30.234,11.232, 30.234,27.315c0,16.672-13.562,30.24-30.234,30.24 + c-16.646,0-30.189-13.562-30.189-30.24c0-3.987, 0.774-7.674, 2.195-10.97c-43.155-19.366-83.354-67.904-103.238-105.395 + c-1.062,0.218-2.189,0.237-3.29,0.339c 4.179,32.774, 15.648,83.405, 46.566,112.179c 5.35-5.984, 13.555-9.632, 23.565-9.632 + c 17.773,0, 30.189,11.232, 30.189,27.309c0,16.678-13.542,30.253-30.189,30.253c-16.653,0-30.195-13.568-30.195-30.253 + c0-4.506, 1.062-8.57, 2.854-12.173c-33.626-30.381-45.382-83.859-49.459-117.805c-2.746-0.365-5.414-0.96-7.878-2.016 + c-16.378,26.886-45.926,48.518-87.462,63.283c-18.547,6.592-36.538,10.784-50.854,13.44c 0.294,8.115-2.528,16.333-8.646,22.534 + c-5.478,5.44-13.28,8.557-21.395,8.557c-8.128,0-15.91-3.13-21.357-8.595c-11.757-11.782-11.738-30.925, 0.019-42.662 + c 5.67-5.734, 13.242-8.902, 21.338-8.902c 8.090,0, 15.706,3.155, 21.459,8.902c 3.878,3.923, 6.342,8.704, 7.622,13.734 + c 38.618-7.29, 104.883-26.394, 133.683-73.485c-4.883-3.482-8.646-8.371-10.784-14.074c-58.733,15.104-120.544,2.208-152.301-23.757 + c-3.629,1.453-7.584,2.227-11.616,2.227c-8.102,0-15.891-3.117-21.382-8.57c-5.709-5.709-8.851-13.286-8.851-21.35 + c 0.013-8.090, 3.174-15.674, 8.896-21.35c 4.134-4.134, 9.267-6.88, 14.854-8.102c-0.986-0.762-2.016-1.485-2.893-2.374 + c-5.728-5.658-8.883-13.21-8.883-21.267c-0.026-8.090, 3.11-15.686, 8.845-21.395c 5.683-5.702, 13.254-8.838, 21.325-8.838 + c 8.077,0, 15.667,3.149, 21.37,8.87c 5.715,5.683, 8.858,13.254, 8.858,21.318c0,7.059-2.522,13.638-6.918,18.963 + c 27.405,25.914, 77.843,66.81, 127.021,71.802c 0.563-5.056, 2.355-9.549, 5.254-13.229c-31.2-33.402-58.822-86.496-73.997-118.765 + c-1.965,0.634-3.994,1.088-6.093,1.312c-16.736,1.811-31.45-10.368-33.21-26.816c-1.766-16.55, 10.259-31.456, 26.81-33.229 + c 1.082-0.115, 2.144-0.166, 3.206-0.166c 15.475,0, 28.378,11.603, 30.003,26.995c 0.858,8.006-1.466,15.872-6.534,22.157 + c-2.342,2.899-5.178,5.235-8.301,7.066c 15.085,31.91, 42.496,84.486, 72.704,116.986c 5.19-3.968, 12.006-6.336, 20.006-6.336 + c 5.664,0, 10.694,1.242, 15.046,3.309c 39.322-52.954, 27.795-138.534, 27.648-139.514l 1.414-0.205c-15.827-0.902-28.454-13.926-28.454-29.965 + c0-16.678, 13.53-30.259, 30.163-30.259c 16.678,0, 30.259,13.574, 30.259,30.259c0,15.494-11.846,28.141-26.963,29.805 + c 0.563,4.23, 3.77,30.957-0.032,63.379c-3.814,32.55-13.741,59.398-28.774,79.712c 4.122,3.226, 7.072,7.514, 8.621,12.608 + c 52.666-11.379, 104.41-46.362, 127.552-76.422c-8.64-5.306-14.464-14.746-14.464-25.581c0-16.666, 13.555-30.227, 30.214-30.227 + c 16.646,0, 30.189,13.555, 30.189,30.227C 435.213,87.040, 421.67,100.55, 405.024,100.55z M 407.443,163.232c 6.426,0, 12.563-2.458, 16.832-6.752 + c 9.222-9.229, 9.216-24.32, 0.013-33.626c-9.037-8.966-24.736-8.915-33.638-0.026c-9.242,9.318-9.254,24.41-0.019,33.658 + C 394.893,160.774, 401.018,163.232, 407.443,163.232z M 370.061,350.438c 13.139,0, 23.834-10.694, 23.834-23.84c0-12.506-9.574-20.915-23.834-20.915 + c-14.003,0-23.789,8.602-23.789,20.915C 346.272,339.744, 356.941,350.438, 370.061,350.438z M 305.677,364.282c 13.12,0, 23.789-10.701, 23.789-23.853 + c0-12.506-9.562-20.909-23.789-20.909c-14.234,0-23.795,8.397-23.795,20.909C 281.882,353.581, 292.557,364.282, 305.677,364.282z M 69.562,269.498 + c-9.043-9.030-24.762-9.056-33.722,0c-9.274,9.261-9.286,24.346-0.026,33.632c 4.256,4.269, 10.387,6.714, 16.826,6.714 + s 12.595-2.445, 16.864-6.675C 78.688,293.856, 78.707,278.752, 69.562,269.498z M 143.411,31.571c 3.994-4.947, 5.824-11.149, 5.152-17.466 + c-1.382-12.954-12.986-22.483-26.17-21.139c-13.043,1.402-22.528,13.152-21.133,26.195c 1.293,12.128, 11.45,21.267, 23.622,21.267 + c 0.845,0, 1.696-0.045, 2.547-0.134C 133.741,39.616, 139.418,36.518, 143.411,31.571z M 302.477-13.075c0-13.158-10.701-23.859-23.859-23.859 + c-13.101,0-23.763,10.701-23.763,23.859c0,13.088, 10.662,23.738, 23.763,23.738C 291.776,10.662, 302.477,0.013, 302.477-13.075z M 16.877,139.718 + c0,6.349, 2.477,12.32, 6.97,16.813c 4.288,4.256, 10.438,6.701, 16.864,6.701c 6.419,0, 12.557-2.438, 16.826-6.682 + c 4.512-4.506, 7.002-10.483, 7.002-16.826c0-6.355-2.483-12.339-6.989-16.838c-8.992-9.011-24.672-8.992-33.658,0.006 + C 19.373,127.373, 16.89,133.35, 16.877,139.718z M 69.466,69.786c-9.005-9.024-24.672-9.037-33.658-0.032 + c-4.512,4.493-6.989,10.477-6.963,16.851c0,6.342, 2.483,12.282, 7.008,16.755c 4.224,4.288, 10.342,6.746, 16.774,6.746 + c 6.438,0, 12.582-2.47, 16.864-6.79c 4.48-4.448, 6.95-10.394, 6.95-16.742S 73.971,74.266, 69.466,69.786z M 71.334,110.035 + c-3.584,2.803-7.776,4.838-12.358,5.786c 1.056,0.806, 2.131,1.581, 3.085,2.534c 5.715,5.709, 8.864,13.293, 8.864,21.363 + c0,8.058-3.149,15.642-8.883,21.357c-1.203,1.19-2.547,2.221-3.942,3.181c 33.478,26.554, 95.533,32.416, 144.934,20.512 + c-0.051-0.358-0.070-0.717-0.109-1.082c-28.307-2.752-60.282-16.845-94.15-41.984C 94.022,130.765, 81.261,119.456, 71.334,110.035z + M 232.806,159.699c-14.003,0-23.782,8.595-23.782,20.902c0,13.152, 10.669,23.853, 23.782,23.853c 13.146,0, 23.84-10.701, 23.84-23.853 + C 256.646,168.102, 247.066,159.699, 232.806,159.699z M 405.024,46.598c-13.133,0-23.814,10.688-23.814,23.827c0,13.082, 10.682,23.725, 23.814,23.725 + c 13.12,0, 23.789-10.643, 23.789-23.725C 428.813,57.286, 418.144,46.598, 405.024,46.598z" /> +<glyph unicode="x" d="M 412.8-60.8l-390.4,0 l0,102.4 l 166.4,0 l0,38.4 l-147.2,0 l0,288 l 352,0 l0-288 l-128,0 l0-38.4 l 147.2,0 L 412.8-60.8 z" /> +<glyph unicode="K" d="M 252.173,371.213c-5.459,5.459-12.704,8.454-20.416,8.454c-7.712,0-14.957-3.008-20.397-8.454 + c-5.446-5.446-8.442-12.678-8.435-20.39c 0.006-7.699, 3.014-14.931, 8.461-20.358c 5.427-5.453, 12.666-8.448, 20.378-8.448 + c 7.706,0, 14.95,3.002, 20.422,8.448C 263.379,341.702, 263.379,359.974, 252.173,371.213zM 56.314,136.986c 7.731,0, 14.989,3.034, 20.397,8.493c 5.44,5.389, 8.442,12.595, 8.461,20.288c 0.019,7.744-2.976,15.014-8.435,20.474 + c-5.446,5.459-12.678,8.461-20.384,8.461c-7.699,0-14.944-3.002-20.403-8.454c-11.245-11.264-11.245-29.574, 0.006-40.832 + C 41.402,139.981, 48.634,136.986, 56.314,136.986zM 151.578,37.286c-7.693,0-14.938-2.989-20.403-8.435c-5.459-5.472-8.448-12.736-8.429-20.461c 0.019-7.693, 3.034-14.912, 8.474-20.32 + c 5.44-5.427, 12.678-8.41, 20.39-8.41c 7.706,0, 14.95,2.982, 20.429,8.422c 11.162,11.206, 11.168,29.485-0.032,40.8 + C 166.515,34.304, 159.264,37.286, 151.578,37.286zM 404.435,81.139C 398.982,86.592, 391.731,89.6, 384.032,89.6c-7.706,0-14.944-3.008-20.384-8.461c-11.194-11.194-11.2-29.472,0-40.781 + c 5.446-5.44, 12.685-8.429, 20.397-8.429s 14.944,2.995, 20.416,8.461C 415.61,51.661, 415.603,69.946, 404.435,81.139zM 377.485,258.214c 16.704,0, 28.826,10.95, 28.826,26.035c0,15.93-12.934,28.89-28.826,28.89c-15.878,0-28.794-12.954-28.794-28.89 + C 348.691,268.922, 360.531,258.214, 377.485,258.214zM 59.706,129.299c-15.91,0-28.858-12.909-28.858-28.774c0-15.93, 12.941-28.89, 28.858-28.89c 15.878,0, 28.794,12.954, 28.794,28.89 + C 88.499,116.397, 75.584,129.299, 59.706,129.299zM 260.973,1.997c-15.878,0-28.8-12.934-28.8-28.838c0-15.936, 12.922-28.902, 28.8-28.902c 15.923,0, 28.883,12.96, 28.883,28.902 + C 289.856-10.938, 276.902,1.997, 260.973,1.997zM 333.843,194.349c-15.91,0-28.851-12.922-28.851-28.8c0-15.898, 12.941-28.832, 28.851-28.832c 15.891,0, 28.819,12.934, 28.819,28.832 + C 362.662,181.434, 349.734,194.349, 333.843,194.349zM 56.307,256.902c 7.731,0, 14.989,3.034, 20.397,8.493c 5.44,5.389, 8.442,12.595, 8.461,20.288c 0.019,7.744-2.976,15.014-8.435,20.474 + c-5.446,5.459-12.678,8.461-20.384,8.461c-7.699,0-14.944-3.002-20.403-8.454c-11.245-11.264-11.245-29.574, 0.006-40.838 + C 41.402,259.891, 48.634,256.902, 56.307,256.902zM 260.87,1.99l-34.394,149.619l 130.394-81.606c 1.338,3.994, 3.488,7.763, 6.624,10.95L 239.11,158.797l 66.726,0.122 + c-0.512,2.138-0.845,4.346-0.845,6.637c0,2.118, 0.262,4.173, 0.698,6.163l-69.44-0.128l 123.699,91.539 + c-3.725,2.432-6.643,5.683-8.57,9.581L 224.301,178.669l 11.59,143.757c-1.363-0.198-2.726-0.403-4.128-0.403 + c-2.989,0-5.862,0.582-8.627,1.453l-11.789-146.163L 82.899,274.534c-1.434-3.373-3.507-6.483-6.189-9.139 + c-0.429-0.435-0.947-0.755-1.402-1.165l 122.272-92.557l-113.133,0.397c 0.448-2.054, 0.73-4.147, 0.723-6.304 + c-0.006-2.227-0.326-4.384-0.819-6.496l 100.563-0.352L 83.264,116.979c 2.438-3.482, 4.064-7.52, 4.774-11.878l 116.896,48.237l-48.416-116.531 + c 4.269-0.73, 8.269-2.413, 11.808-4.934l 46.701,112.403l 33.395-145.286C 252.205,0.845, 256.397,1.978, 260.87,1.99z" /> +<glyph unicode="O" d="M 239.872-12.947c-52.864,0-100.851,21.158-136.032,55.411l-7.846-8.346c 33.363-43.181, 85.357-72.282, 143.712-72.282 + c 83.36,0, 153.773,59.347, 175.334,134.406C 383.2,31.622, 316.659-12.947, 239.872-12.947zM 70.317,85.76c-2.278,3.994-4.461,8.045-6.458,12.205c 1.242-4.512, 2.662-8.966, 4.25-13.357L 70.317,85.76zM 182.803,144.55l 7.629-10.061L 112.602,51.776c 32.89-32.096, 77.792-51.923, 127.27-51.923c 100.563,0, 182.381,81.798, 182.381,182.349 + S 340.435,364.55, 239.872,364.55c-100.55,0-182.355-81.798-182.355-182.349c0-32.928, 8.813-63.814, 24.147-90.509L 182.803,144.55z M 239.872,223.802 + c 22.912,0, 41.549-18.662, 41.549-41.6c0-22.906-18.643-41.536-41.549-41.536c-22.918,0-41.562,18.63-41.562,41.536 + C 198.31,205.139, 216.954,223.802, 239.872,223.802zM 33.325,51.994L 62.528,17.235L 155.11,115.629 z" /> +<glyph unicode="5" d="M 247.059,291.2c-0.627,0-1.248,0-1.875,0c-36.845,0-74.56,9.798-75.328,29.946l 12.794-0.173 + c 0.243-6.445, 21.978-18.97, 62.643-19.29c 39.597-0.512, 65.6,10.771, 66.157,18.707l 12.768,0.352C 322.778,300.403, 284.634,291.2, 247.059,291.2zM 246.88,246.4c-0.646,0-1.28,0-1.907,0c-30.643,0-82.803,7.053-83.936,27.616l 12.781,1.197 + c 1.626-4.826, 27.552-13.99, 71.238-14.291c 45.638-0.282, 73.498,9.67, 75.136,14.822l 12.73-2.17C 331.027,252.8, 278.848,246.4, 246.88,246.4zM 247.494,187.072c-1.146,0-2.272,0.006-3.411,0.019c-57.92,0.474-117.222,15.11-118.477,42.17l 12.787,0.589 + c 0.563-12.147, 41.082-29.427, 105.805-29.958c 67.584-0.704, 110.451,16.858, 111.424,29.715l 12.762-0.96C 366.323,201.421, 305.203,187.072, 247.494,187.072 + zM 246.976,121.126c-1.171,0-2.33,0.006-3.494,0.013c-70.662,0.486-142.931,17.082-144.326,47.853l 12.787,0.576 + c 0.646-14.195, 47.891-35.046, 131.635-35.622c 84.115-0.845, 137.325,20.154, 138.566,35.354l 12.755-1.037 + C 392.378,137.427, 317.466,121.126, 246.976,121.126zM 247.36,54.381c-1.453,0-2.893,0.006-4.346,0.019c-79.046,0.461-159.936,18.95-161.574,53.382l 12.787,0.614 + c 0.909-19.078, 62.272-40.685, 148.877-41.197c 90.291-0.678, 155.053,20.915, 156.736,40.851l 12.755-1.075 + C 409.696,72.55, 326.125,54.381, 247.36,54.381zM 247.053-3.718c-0.992,0-1.978,0.006-2.97,0.013c-57.926,0.429-117.229,16.077-118.477,45.126l 12.787,0.55 + c 0.653-15.181, 44.262-32.41, 105.798-32.877c 1.005-0.006, 2.010-0.013, 3.002-0.013c 60.96,0, 107.328,17.037, 108.422,32.672l 12.768-0.902 + C 366.342,11.693, 304.915-3.718, 247.053-3.718zM 246.726-54.4c-0.518,0-1.030,0-1.542,0c-36.794,0-74.381,12.762-75.066,35.45L 182.912-18.56 + c 0.282-9.306, 24.16-22.573, 62.413-23.040c 0.531-0.006, 1.069-0.006, 1.6-0.006c 38.938,0, 63.622,13.293, 64.237,23.027l 12.774-0.806 + C 322.49-42.266, 283.366-54.4, 246.726-54.4zM 247.181,336l-2.842,0 c-11.091,0-108.461,2.374-109.843,28.787l-0.352,7.29l 112.851,0.442l 113.427-0.397l-0.883-8.205 + C 356.301,337.798, 258.336,336, 247.181,336z" /> +<glyph unicode="N" d="M 284-62.739l-58.31,82.381l-58.278-82.381l-9.28,100.48L 66.502-4.48l 42.208,91.686L 8.192,96.506l 82.406,58.259L 8.186,213.005 + l 100.518,9.299L 66.496,313.978l 91.635-42.221l 9.28,100.512l 58.278-82.406l 58.31,82.406l 9.248-100.512l 91.699,42.221l-42.24-91.667 + l 100.48-9.299l-82.381-58.246l 82.381-58.259l-100.48-9.299l 42.24-91.686l-91.699,42.227L 284-62.739z" /> +<glyph unicode="4" d="M 266.81-54.848L 177.645-54.848 L 177.645,114.938 L 0.666,114.938 L 0.666,201.299 l 176.979,0 L 177.645,373.197 l 89.171,0 l0-171.891 l 176.979,0 l0-86.362 L 266.81,114.944 L 266.81-54.848 z" /> +<glyph unicode="3" d="M 132.89,243.162L 304.474,243.162L 304.474,71.597L 132.89,71.597zM 331.098,269.811l-224.832,0 l0-224.813 l 224.832,0 L 331.098,269.811 z M 317.274,58.803l-197.184,0 L 120.090,255.962 l 197.184,0 L 317.274,58.803 zM 2.522,373.536l0-432.262 l 432.301,0 L 434.822,373.536 L 2.522,373.536 z M 324.243,362.874c 6.925,0, 12.608-5.651, 12.608-12.64c0-6.963-5.683-12.589-12.608-12.589 + c-6.97,0-12.595,5.626-12.595,12.589C 311.648,357.222, 317.274,362.874, 324.243,362.874z M 324.243,320.045c 6.925,0, 12.608-5.664, 12.608-12.646 + c0-6.918-5.683-12.576-12.608-12.576c-6.97,0-12.595,5.658-12.595,12.576C 311.648,314.381, 317.274,320.045, 324.243,320.045z M 281.133,362.874 + c 6.944,0, 12.595-5.651, 12.595-12.64c0-6.963-5.651-12.589-12.595-12.589c-6.918,0-12.576,5.626-12.576,12.589 + C 268.557,357.222, 274.202,362.874, 281.133,362.874z M 281.133,320.045c 6.944,0, 12.595-5.664, 12.595-12.646c0-6.918-5.651-12.576-12.595-12.576 + c-6.918,0-12.576,5.658-12.576,12.576C 268.557,314.381, 274.202,320.045, 281.133,320.045z M 238.067,362.874c 6.938,0, 12.57-5.651, 12.57-12.64 + c0-6.963-5.632-12.589-12.57-12.589c-6.97,0-12.589,5.626-12.589,12.589C 225.478,357.222, 231.098,362.874, 238.067,362.874z M 238.067,320.045 + c 6.938,0, 12.57-5.664, 12.57-12.646c0-6.918-5.632-12.576-12.57-12.576c-6.97,0-12.589,5.658-12.589,12.576 + C 225.478,314.381, 231.098,320.045, 238.067,320.045z M 194.976,362.874c 6.912,0, 12.57-5.651, 12.57-12.64c0-6.963-5.658-12.589-12.57-12.589 + c-6.957,0-12.602,5.626-12.602,12.589C 182.374,357.222, 188.019,362.874, 194.976,362.874z M 194.976,320.045c 6.912,0, 12.57-5.664, 12.57-12.646 + c0-6.918-5.658-12.576-12.57-12.576c-6.957,0-12.602,5.658-12.602,12.576C 182.374,314.381, 188.019,320.045, 194.976,320.045z M 151.859,362.874 + c 6.963,0, 12.614-5.651, 12.614-12.64c0-6.963-5.645-12.589-12.614-12.589c-6.925,0-12.589,5.626-12.589,12.589 + C 139.27,357.222, 144.934,362.874, 151.859,362.874z M 151.859,320.045c 6.963,0, 12.614-5.664, 12.614-12.646c0-6.918-5.645-12.576-12.614-12.576 + c-6.925,0-12.589,5.658-12.589,12.576C 139.27,314.381, 144.934,320.045, 151.859,320.045z M 108.768,362.874c 6.938,0, 12.582-5.651, 12.589-12.64 + c0-6.963-5.651-12.589-12.589-12.589c-6.95,0-12.589,5.626-12.589,12.589C 96.179,357.222, 101.818,362.874, 108.768,362.874z M 108.768,320.045 + c 6.938,0, 12.582-5.664, 12.589-12.646c0-6.918-5.651-12.576-12.589-12.576c-6.95,0-12.589,5.658-12.589,12.576 + C 96.179,314.381, 101.818,320.045, 108.768,320.045z M 22.586-48.102c-6.95,0-12.602,5.651-12.602,12.614c0,6.95, 5.651,12.602, 12.602,12.602 + c 6.912,0, 12.608-5.651, 12.608-12.602C 35.194-42.451, 29.498-48.102, 22.586-48.102z M 22.586-5.21c-6.95,0-12.602,5.6-12.602,12.589 + c0,6.963, 5.651,12.608, 12.602,12.608c 6.912,0, 12.608-5.645, 12.608-12.608C 35.194,0.39, 29.498-5.21, 22.586-5.21z M 22.586,37.613 + c-6.95,0-12.602,5.658-12.602,12.627c0,6.989, 5.651,12.602, 12.602,12.602c 6.912,0, 12.608-5.613, 12.608-12.602 + C 35.194,43.27, 29.498,37.613, 22.586,37.613z M 22.586,80.493c-6.95,0-12.602,5.658-12.602,12.595c0,6.989, 5.651,12.64, 12.602,12.64 + c 6.912,0, 12.608-5.651, 12.608-12.64C 35.194,86.15, 29.498,80.493, 22.586,80.493z M 22.586,123.334c-6.95,0-12.602,5.702-12.602,12.653 + c0,6.957, 5.651,12.608, 12.602,12.608c 6.912,0, 12.608-5.651, 12.608-12.608C 35.194,129.037, 29.498,123.334, 22.586,123.334z M 22.586,166.227 + c-6.95,0-12.602,5.658-12.602,12.582c0,6.963, 5.651,12.614, 12.602,12.614c 6.912,0, 12.608-5.651, 12.608-12.614 + C 35.194,171.885, 29.498,166.227, 22.586,166.227z M 22.586,209.056c-6.95,0-12.602,5.664-12.602,12.621c0,6.95, 5.651,12.634, 12.602,12.634 + c 6.912,0, 12.608-5.677, 12.608-12.634C 35.194,214.72, 29.498,209.056, 22.586,209.056z M 22.586,251.93c-6.95,0-12.602,5.658-12.602,12.646 + c0,6.918, 5.651,12.602, 12.602,12.602c 6.912,0, 12.608-5.683, 12.608-12.602C 35.194,257.587, 29.498,251.93, 22.586,251.93z M 22.586,294.822 + c-6.95,0-12.602,5.658-12.602,12.576c0,6.982, 5.651,12.646, 12.602,12.646c 6.912,0, 12.608-5.664, 12.608-12.646 + C 35.194,300.48, 29.498,294.822, 22.586,294.822z M 22.586,337.645c-6.95,0-12.602,5.626-12.602,12.589c0,6.989, 5.651,12.64, 12.602,12.64 + c 6.912,0, 12.608-5.651, 12.608-12.64C 35.194,343.27, 29.498,337.645, 22.586,337.645z M 65.69-48.102c-6.97,0-12.634,5.651-12.634,12.614 + c0,6.95, 5.664,12.602, 12.634,12.602c 6.912,0, 12.576-5.651, 12.576-12.602C 78.266-42.451, 72.602-48.102, 65.69-48.102z M 65.69-5.21 + c-6.97,0-12.634,5.6-12.634,12.589c0,6.963, 5.664,12.608, 12.634,12.608c 6.912,0, 12.576-5.645, 12.576-12.608 + C 78.266,0.39, 72.602-5.21, 65.69-5.21z M 65.69,37.613c-6.97,0-12.634,5.658-12.634,12.627c0,6.989, 5.664,12.602, 12.634,12.602 + c 6.912,0, 12.576-5.613, 12.576-12.602C 78.266,43.27, 72.602,37.613, 65.69,37.613z M 65.69,80.493c-6.97,0-12.634,5.658-12.634,12.595 + c0,6.989, 5.664,12.64, 12.634,12.64c 6.912,0, 12.576-5.651, 12.576-12.64C 78.266,86.15, 72.602,80.493, 65.69,80.493z M 65.69,123.334 + c-6.97,0-12.634,5.702-12.634,12.653c0,6.957, 5.664,12.608, 12.634,12.608c 6.912,0, 12.576-5.651, 12.576-12.608 + C 78.266,129.037, 72.602,123.334, 65.69,123.334z M 65.69,166.227c-6.97,0-12.634,5.658-12.634,12.582c0,6.963, 5.664,12.614, 12.634,12.614 + c 6.912,0, 12.576-5.651, 12.576-12.614C 78.266,171.885, 72.602,166.227, 65.69,166.227z M 65.69,209.056c-6.97,0-12.634,5.664-12.634,12.621 + c0,6.95, 5.664,12.634, 12.634,12.634c 6.912,0, 12.576-5.677, 12.576-12.634C 78.266,214.72, 72.602,209.056, 65.69,209.056z M 65.69,251.93 + c-6.97,0-12.634,5.658-12.634,12.646c0,6.918, 5.664,12.602, 12.634,12.602c 6.912,0, 12.576-5.683, 12.576-12.602 + C 78.266,257.587, 72.602,251.93, 65.69,251.93z M 65.69,294.822c-6.97,0-12.634,5.658-12.634,12.576c0,6.982, 5.664,12.646, 12.634,12.646 + c 6.912,0, 12.576-5.664, 12.576-12.646C 78.266,300.48, 72.602,294.822, 65.69,294.822z M 65.69,337.645c-6.97,0-12.634,5.626-12.634,12.589 + c0,6.989, 5.664,12.64, 12.634,12.64c 6.912,0, 12.576-5.651, 12.576-12.64C 78.266,343.27, 72.602,337.645, 65.69,337.645z M 108.768-48.102 + c-6.963,0-12.608,5.645-12.608,12.608s 5.645,12.608, 12.608,12.608s 12.608-5.645, 12.608-12.608S 115.731-48.102, 108.768-48.102z M 96.179,7.373 + c0,6.963, 5.638,12.608, 12.589,12.608c 6.938,0, 12.582-5.645, 12.589-12.608c0-6.989-5.651-12.589-12.589-12.589 + C 101.818-5.21, 96.179,0.39, 96.179,7.373z M 151.859-48.102c-6.925,0-12.589,5.651-12.589,12.614c0,6.95, 5.664,12.602, 12.589,12.602 + c 6.963,0, 12.614-5.651, 12.614-12.602C 164.474-42.451, 158.829-48.102, 151.859-48.102z M 151.859-5.21c-6.925,0-12.589,5.6-12.589,12.589 + c0,6.963, 5.664,12.608, 12.589,12.608c 6.963,0, 12.614-5.645, 12.614-12.608C 164.474,0.39, 158.829-5.21, 151.859-5.21z M 194.976-48.102 + c-6.957,0-12.602,5.651-12.602,12.614c0,6.95, 5.645,12.602, 12.602,12.602c 6.912,0, 12.57-5.651, 12.57-12.602 + C 207.546-42.451, 201.888-48.102, 194.976-48.102z M 194.976-5.21c-6.957,0-12.602,5.6-12.602,12.589c0,6.963, 5.645,12.608, 12.602,12.608 + c 6.912,0, 12.57-5.645, 12.57-12.608C 207.546,0.39, 201.888-5.21, 194.976-5.21z M 238.067-48.102c-6.97,0-12.608,5.645-12.608,12.608 + s 5.638,12.608, 12.608,12.608c 6.963,0, 12.608-5.645, 12.608-12.608S 245.030-48.102, 238.067-48.102z M 225.478,7.373c0,6.963, 5.619,12.608, 12.589,12.608 + c 6.938,0, 12.57-5.645, 12.57-12.608c0-6.989-5.632-12.589-12.57-12.589C 231.098-5.21, 225.478,0.39, 225.478,7.373z M 281.133-48.102 + c-6.918,0-12.576,5.651-12.576,12.614c0,6.95, 5.645,12.602, 12.576,12.602c 6.944,0, 12.595-5.651, 12.595-12.602 + C 293.728-42.451, 288.077-48.102, 281.133-48.102z M 281.133-5.21c-6.918,0-12.576,5.6-12.576,12.589c0,6.963, 5.645,12.608, 12.576,12.608 + c 6.944,0, 12.595-5.645, 12.595-12.608C 293.728,0.39, 288.077-5.21, 281.133-5.21z M 324.243-48.102c-6.97,0-12.595,5.651-12.595,12.614 + c0,6.95, 5.626,12.602, 12.595,12.602c 6.925,0, 12.608-5.651, 12.608-12.602C 336.851-42.451, 331.168-48.102, 324.243-48.102z M 324.243-5.21 + c-6.97,0-12.595,5.6-12.595,12.589c0,6.963, 5.626,12.608, 12.595,12.608c 6.925,0, 12.608-5.645, 12.608-12.608 + C 336.851,0.39, 331.168-5.21, 324.243-5.21z M 343.898,32.198L 93.472,32.198 L 93.472,282.611 l 250.426,0 L 343.898,32.198 z M 367.36-48.102 + c-6.989,0-12.64,5.651-12.64,12.614c0,6.95, 5.651,12.602, 12.64,12.602c 6.938,0, 12.582-5.651, 12.576-12.602 + C 379.936-42.451, 374.298-48.102, 367.36-48.102z M 367.36-5.21c-6.989,0-12.64,5.6-12.64,12.589c0,6.963, 5.651,12.608, 12.64,12.608 + c 6.938,0, 12.582-5.645, 12.576-12.608C 379.936,0.39, 374.298-5.21, 367.36-5.21z M 367.36,37.613c-6.989,0-12.64,5.658-12.64,12.627 + c0,6.989, 5.651,12.602, 12.64,12.602c 6.938,0, 12.582-5.613, 12.576-12.602C 379.936,43.27, 374.298,37.613, 367.36,37.613z M 367.36,80.493 + c-6.989,0-12.64,5.658-12.64,12.595c0,6.989, 5.651,12.64, 12.64,12.64c 6.938,0, 12.582-5.651, 12.576-12.64 + C 379.936,86.15, 374.298,80.493, 367.36,80.493z M 367.36,123.334c-6.989,0-12.64,5.702-12.64,12.653c0,6.957, 5.651,12.608, 12.64,12.608 + c 6.938,0, 12.582-5.651, 12.576-12.608C 379.936,129.037, 374.298,123.334, 367.36,123.334z M 367.36,166.227c-6.989,0-12.64,5.658-12.64,12.582 + c0,6.963, 5.651,12.614, 12.64,12.614c 6.938,0, 12.582-5.651, 12.576-12.614C 379.936,171.885, 374.298,166.227, 367.36,166.227z M 367.36,209.056 + c-6.989,0-12.64,5.664-12.64,12.621c0,6.95, 5.651,12.634, 12.64,12.634c 6.938,0, 12.582-5.677, 12.576-12.634 + C 379.936,214.72, 374.298,209.056, 367.36,209.056z M 367.36,251.93c-6.989,0-12.64,5.658-12.64,12.646c0,6.918, 5.651,12.602, 12.64,12.602 + c 6.938,0, 12.582-5.683, 12.576-12.602C 379.936,257.587, 374.298,251.93, 367.36,251.93z M 367.36,294.822c-6.989,0-12.64,5.658-12.64,12.576 + c0,6.982, 5.651,12.646, 12.64,12.646c 6.938,0, 12.582-5.664, 12.576-12.646C 379.936,300.48, 374.298,294.822, 367.36,294.822z M 367.36,337.645 + c-6.989,0-12.64,5.626-12.64,12.589c0,6.989, 5.651,12.64, 12.64,12.64c 6.938,0, 12.582-5.651, 12.576-12.64 + C 379.936,343.27, 374.298,337.645, 367.36,337.645z M 410.438-48.102c-6.97,0-12.595,5.651-12.595,12.614c0,6.95, 5.626,12.602, 12.595,12.602 + c 6.938,0, 12.544-5.651, 12.544-12.602C 422.982-42.451, 417.376-48.102, 410.438-48.102z M 410.438-5.21c-6.97,0-12.595,5.6-12.595,12.589 + c0,6.963, 5.626,12.608, 12.595,12.608c 6.938,0, 12.544-5.645, 12.544-12.608C 422.982,0.39, 417.376-5.21, 410.438-5.21z M 410.438,37.613 + c-6.97,0-12.595,5.658-12.595,12.627c0,6.989, 5.626,12.602, 12.595,12.602c 6.938,0, 12.544-5.613, 12.544-12.602 + C 422.982,43.27, 417.376,37.613, 410.438,37.613z M 410.438,80.493c-6.97,0-12.595,5.658-12.595,12.595c0,6.989, 5.626,12.64, 12.595,12.64 + c 6.938,0, 12.544-5.651, 12.544-12.64C 422.982,86.15, 417.376,80.493, 410.438,80.493z M 410.438,123.334c-6.97,0-12.595,5.702-12.595,12.653 + c0,6.957, 5.626,12.608, 12.595,12.608c 6.938,0, 12.544-5.651, 12.544-12.608C 422.982,129.037, 417.376,123.334, 410.438,123.334z M 410.438,166.227 + c-6.97,0-12.595,5.658-12.595,12.582c0,6.963, 5.626,12.614, 12.595,12.614c 6.938,0, 12.544-5.651, 12.544-12.614 + C 422.982,171.885, 417.376,166.227, 410.438,166.227z M 410.438,209.056c-6.97,0-12.595,5.664-12.595,12.621c0,6.95, 5.626,12.634, 12.595,12.634 + c 6.938,0, 12.544-5.677, 12.544-12.634C 422.982,214.72, 417.376,209.056, 410.438,209.056z M 410.438,251.93c-6.97,0-12.595,5.658-12.595,12.646 + c0,6.918, 5.626,12.602, 12.595,12.602c 6.938,0, 12.544-5.683, 12.544-12.602C 422.982,257.587, 417.376,251.93, 410.438,251.93z M 410.438,294.822 + c-6.97,0-12.595,5.658-12.595,12.576c0,6.982, 5.626,12.646, 12.595,12.646c 6.938,0, 12.544-5.664, 12.544-12.646 + C 422.982,300.48, 417.376,294.822, 410.438,294.822z M 410.438,337.645c-6.97,0-12.595,5.626-12.595,12.589c0,6.989, 5.626,12.64, 12.595,12.64 + c 6.938,0, 12.544-5.651, 12.544-12.64C 422.982,343.27, 417.376,337.645, 410.438,337.645z" /> +<glyph unicode="2" d="M 393.6,163.2L 393.6,258.746 c0,3.61, 6.4,9.107, 6.4,15.104l0,17.722 c0,10.688-10.605,18.829-21.286,18.829l-3.277,0 c-10.694,0-20.237-8.141-20.237-18.829 + l0-17.722 c0-5.984, 6.4-11.469, 6.4-15.098L 361.6,163.2 l-352,0 l0-166.4 l 422.4,0 l0,166.4 L 393.6,163.2 z M 262.739,63.603c-10.074,0-18.272,8.224-18.272,18.323 + c0,10.093, 8.198,18.298, 18.272,18.298c 10.080,0, 18.278-8.205, 18.278-18.298C 281.018,71.827, 272.819,63.603, 262.739,63.603z M 321.28,63.603 + c-10.080,0-18.285,8.224-18.285,18.323c0,10.093, 8.205,18.298, 18.285,18.298c 10.093,0, 18.304-8.205, 18.304-18.298 + C 339.59,71.827, 331.373,63.603, 321.28,63.603z M 379.802,63.603c-10.074,0-18.272,8.224-18.272,18.323c0,10.093, 8.198,18.298, 18.272,18.298 + c 10.080,0, 18.278-8.205, 18.278-18.298C 398.080,71.827, 389.882,63.603, 379.802,63.603z" /> +<glyph unicode="1" d="M 224.563,377.35c-62.989,0-118.963-52.186-118.963-111.674l0-3.347 c0-39.962, 1.357-77.766, 51.2-96.102l0-71.040 l 30.765-18.214L 156.8,58.778 + l0-29.037 l 30.765-18.214L 156.8-6.675L 156.8-60.8 l 89.6,0 L 246.4,163.398 c 39.949,17.357, 78.733,57.158, 78.733,102.272C 325.133,331.424, 283.565,377.35, 224.563,377.35z + M 228.653,272.966c-19.539,0-35.437,15.917-35.437,35.482c0,19.558, 15.898,35.462, 35.437,35.462c 19.546,0, 35.443-15.904, 35.443-35.462 + C 264.096,288.89, 248.192,272.966, 228.653,272.966z" /> +<glyph unicode="Z" d="M 266.278,39.859l0,45.472 l 129.939,0 L 396.218,371.2 L 45.382,371.2 l0-285.869 l 149.434,0 l0-45.472 L 25.888,39.859 L 25.888-57.6 l 389.818,0 L 415.706,39.859 L 266.278,39.859 z M 220.8,293.235l 12.992,0 l0-84.461 L 220.8,208.774 + L 220.8,293.235 z M 166.47,222.131c0,24.077, 13.274,45.395, 34.643,55.629l 6.598-13.766c-16.269-7.789-25.978-23.437-25.978-41.862 + c0-25.901, 20.941-46.963, 46.682-46.963c 26.061,0, 47.258,21.069, 47.258,46.963c0,19.085-11.84,36.653-29.466,43.706l 5.67,14.17 + c 23.731-9.498, 39.066-32.211, 39.066-57.869c0-34.317-28.045-62.234-62.528-62.234C 194.253,159.891, 166.47,187.808, 166.47,222.131z" /> +<glyph unicode="Y" d="M 272.678,39.859l0,45.472 l 129.939,0 L 402.618,371.2 L 51.782,371.2 l0-285.869 l 149.434,0 l0-45.472 L 32.288,39.859 L 32.288-57.6 l 389.818,0 L 422.106,39.859 L 272.678,39.859 z M 172.461,231.437 + c0,18.189, 7.597,34.79, 20.506,46.234l-20.102,0.902l 0.685,15.251l 42.771-1.907l 5.094-42.886l-15.162-1.798l-2.336,19.718 + c-10.266-8.576-16.198-21.184-16.198-35.507c0-25.894, 20.947-46.963, 46.688-46.963c 26.054,0, 47.258,21.069, 47.258,46.963 + c0,19.091-11.84,36.659-29.472,43.712l 5.67,14.176c 23.373-9.35, 39.072-32.614, 39.072-57.875c0-34.317-28.045-62.24-62.528-62.24 + C 200.25,169.203, 172.461,197.12, 172.461,231.437z" /> +<glyph unicode="X" d="M 272.678,39.859l0,45.472 l 129.939,0 L 402.618,371.2 L 51.782,371.2 l0-285.869 l 149.434,0 l0-45.472 L 32.288,39.859 L 32.288-57.6 l 389.818,0 L 422.106,39.859 L 272.678,39.859 z M 251.782,166.118l 31.098,37.216 + l-55.309,29.658l-3.635-14.432l-35.718-22.394L 188.218,156.8 l-6.496,0 l0,35.302 l-40.832-25.594l-3.45,5.51l 80.525,50.477l-32.442,64.25l-49.28,0 l0,6.496 + l 53.274,0 l 31.040-61.498l 16.992,67.571l 81.587,10.317l 0.806-6.445l-77.178-9.76l-13.568-53.926l 60.256-32.326l 57.184-35.142l-3.392-5.536 + l-54.624,33.581l-31.853-38.118L 251.782,166.118z" /> +<glyph unicode="W" d="M 403.661,253.613c-11.226,0-20.358-9.152-20.358-20.403c0-0.813, 0.134-1.574, 0.23-2.349l-66.266-19.104 + c-3.27,6.867-10.234,11.661-18.342,11.661c-11.232,0-20.378-9.114-20.378-20.314c0-11.232, 9.146-20.378, 20.378-20.378 + c 11.226,0, 20.358,9.146, 20.358,20.378c0,1.434-0.154,2.816-0.435,4.166l 65.805,18.97c 1.235-3.091, 3.341-5.658, 6.080-7.597l-45.952-70.611 + c-7.731,4.986-19.443,4.224-25.894-2.298c-7.258-7.251-7.757-18.701-1.67-26.714l-54.112-32.922c-3.661,3.475-8.595,5.645-14.035,5.645 + c-7.091,0-13.338-3.661-16.979-9.184l-33.274,24.090l0,0l-13.101,9.491l 13.274,30.688l 30.016-0.013c 0.544-4.499, 2.47-8.672, 5.709-11.91 + c 3.827-3.872, 8.934-6.010, 14.387-6.010c 5.44,0, 10.566,2.131, 14.464,6.003c 7.834,7.904, 7.827,20.813-0.038,28.794 + c-5.344,5.286-14.131,6.701-21.376,4.352l-10.912,21.376c 3.827,3.712, 6.221,8.883, 6.221,14.611c0,11.206-9.158,20.326-20.403,20.326 + c-11.213,0-20.333-9.12-20.333-20.326c0-8.864, 5.696-16.346, 13.574-19.155l-14.394-33.299l-30.010,0.013l-18.925,98.874 + c 7.078,2.758, 11.584,8.902, 11.584,16.832c0,11.238-9.133,20.39-20.352,20.39c-11.226,0-20.358-9.152-20.358-20.39 + c0-9.299, 6.176-16.16, 15.475-17.946l-18.118-62.298c-1.28,0.237-2.586,0.378-3.91,0.378c-5.478,0-10.726-2.125-14.355-5.805 + c-3.866-3.808-6.003-8.909-6.003-14.342c-0.013-5.453, 2.106-10.566, 5.965-14.419c 0.576-0.582, 1.229-1.037, 1.862-1.536L 53.018,151.488 + c-1.165,10.125-9.69,18.048-20.128,18.048c-11.226,0-20.358-9.133-20.358-20.352c0-11.226, 9.133-20.365, 20.358-20.365 + c 10.426,0, 18.944,7.91, 20.122,18.029L 134.4,146.842l 31.814-22.458l 1.882-9.805c-3.13-0.87-6.054-2.349-8.333-4.608 + c-3.853-3.859-5.971-8.96-5.971-14.394c 0.019-5.459, 2.15-10.566, 6.003-14.394c 3.834-3.853, 8.947-5.971, 14.4-5.971 + s 10.56,2.118, 14.381,5.965c 3.859,3.853, 5.984,8.96, 5.984,14.406c0,3.744-1.075,7.309-2.963,10.438l 38.394-27.795 + c-0.768-2.138-1.261-4.403-1.261-6.803c0-11.251, 9.126-20.403, 20.346-20.403c 11.251,0, 20.403,9.152, 20.403,20.403 + c0,4.058-1.222,7.814-3.29,10.989l 54.432,33.133c 3.597-2.874, 7.981-4.518, 12.672-4.518c 5.44,0, 10.541,2.112, 14.387,5.952 + c 7.667,7.744, 7.821,20.115, 0.576,28.051l 46.624,71.373c 2.611-1.018, 5.549-1.613, 8.787-1.613c 12,0, 20.39,7.578, 20.39,18.426 + C 424.051,244.461, 414.899,253.613, 403.661,253.613z M 214.982,183.853c 0.691-0.077, 1.363-0.211, 2.067-0.211c 3.782,0, 7.283,1.101, 10.317,2.899 + l 10.426-20.422c-1.12-0.698-2.163-1.51-3.091-2.445c-3.283-3.296-5.203-7.539-5.715-12.102l-27.955,0.013L 214.982,183.853z + M 179.482,114.797c-2.189,0.518-4.442,0.71-6.701,0.576l-0.954,4.966L 179.482,114.797z M 193.811,146.835l-12.038-27.834l-11.264,8.154 + l-3.782,19.686L 193.811,146.835z M 163.757,132.064l-20.518,14.867l 17.862-0.134L 163.757,132.064z M 122.675,150.4 + c 1.062,0.723, 2.074,1.542, 3.002,2.477c 0.16,0.16, 0.282,0.365, 0.435,0.531l 4.589-3.008L 122.675,150.4 z M 128.922,157.248 + c 1.728,3.027, 2.726,6.426, 2.726,10.010c0,5.446-2.118,10.534-5.946,14.323c-1.811,1.83-4.013,3.264-6.432,4.262l 18.515,63.046 + c 0.141,0, 0.269-0.032, 0.41-0.032c 1.408,0, 2.752,0.128, 4.051,0.326l 18.746-97.594l-24.269,0.013L 128.922,157.248z M 140.493,146.95 + l 0.064-0.051L 140.493,146.95L 140.493,146.95z" /> +<glyph unicode="V" d="M 405.024,100.55c-3.373,0-6.56-0.678-9.594-1.702c-24.224,32.8-79.098,68.166-132.582,79.808 + c 0.045,0.659, 0.192,1.267, 0.192,1.946c0,2.054-0.211,4.058-0.608,6.003c 57.286,12.883, 98.413-10.464, 120.71-29.21 + c-8.557-11.77-7.635-28.39, 2.963-39.072c 5.69-5.677, 13.267-8.8, 21.35-8.8c 8.045,0, 15.622,3.123, 21.357,8.826 + c 11.693,11.814, 11.686,30.957-0.013,42.662c-5.459,5.485-13.242,8.634-21.363,8.634c-7.629,0-14.886-2.861-20.262-7.744 + c-23.642,19.565-67.162,44.115-126.669,30.752c-3.014,6.918-8.544,12.467-15.424,15.546c 19.635,36.243, 59.142,83.616, 100.365,101.92 + c 5.28-6.714, 13.946-10.81, 24.614-10.81c 17.798,0, 30.234,11.232, 30.234,27.315c0,16.672-13.562,30.24-30.234,30.24 + c-16.646,0-30.189-13.562-30.189-30.24c0-3.987, 0.774-7.674, 2.195-10.97c-43.155-19.366-83.354-67.904-103.238-105.395 + c-1.062,0.218-2.189,0.237-3.29,0.339c 4.179,32.774, 15.648,83.405, 46.566,112.179c 5.35-5.984, 13.555-9.632, 23.565-9.632 + c 17.773,0, 30.189,11.232, 30.189,27.309c0,16.678-13.542,30.253-30.189,30.253c-16.653,0-30.195-13.568-30.195-30.253 + c0-4.506, 1.062-8.57, 2.854-12.173c-33.626-30.381-45.382-83.859-49.459-117.805c-2.746-0.365-5.414-0.96-7.878-2.016 + c-16.378,26.886-45.926,48.518-87.462,63.283c-18.547,6.592-36.538,10.784-50.854,13.44c 0.294,8.115-2.528,16.333-8.646,22.534 + c-5.478,5.44-13.28,8.557-21.395,8.557c-8.128,0-15.91-3.13-21.357-8.595c-11.757-11.782-11.738-30.925, 0.019-42.662 + c 5.67-5.734, 13.242-8.902, 21.338-8.902c 8.090,0, 15.706,3.155, 21.459,8.902c 3.878,3.923, 6.342,8.704, 7.622,13.734 + c 38.618-7.29, 104.883-26.394, 133.683-73.485c-4.883-3.482-8.646-8.371-10.784-14.074c-58.733,15.104-120.544,2.208-152.301-23.757 + c-3.629,1.453-7.584,2.227-11.616,2.227c-8.102,0-15.891-3.117-21.382-8.57c-5.709-5.709-8.851-13.286-8.851-21.35 + c 0.013-8.090, 3.174-15.674, 8.896-21.35c 4.134-4.134, 9.267-6.88, 14.854-8.102c-0.986-0.762-2.016-1.485-2.893-2.374 + c-5.728-5.658-8.883-13.21-8.883-21.267c-0.026-8.090, 3.11-15.686, 8.845-21.395c 5.683-5.702, 13.254-8.838, 21.325-8.838 + c 8.077,0, 15.667,3.149, 21.37,8.87c 5.715,5.683, 8.858,13.254, 8.858,21.318c0,7.059-2.522,13.638-6.918,18.963 + c 27.405,25.914, 77.843,66.81, 127.021,71.802c 0.563-5.056, 2.355-9.549, 5.254-13.229c-31.2-33.402-58.822-86.496-73.997-118.765 + c-1.965,0.634-3.994,1.088-6.093,1.312c-16.736,1.811-31.45-10.368-33.21-26.816c-1.766-16.55, 10.259-31.456, 26.81-33.229 + c 1.082-0.115, 2.144-0.166, 3.206-0.166c 15.475,0, 28.378,11.603, 30.003,26.995c 0.858,8.006-1.466,15.872-6.534,22.157 + c-2.342,2.899-5.178,5.235-8.301,7.066c 15.085,31.91, 42.496,84.486, 72.704,116.986c 5.19-3.968, 12.006-6.336, 20.006-6.336 + c 5.664,0, 10.694,1.242, 15.046,3.309c 39.322-52.954, 27.795-138.534, 27.648-139.514l 1.414-0.205c-15.827-0.902-28.454-13.926-28.454-29.965 + c0-16.678, 13.53-30.259, 30.163-30.259c 16.678,0, 30.259,13.574, 30.259,30.259c0,15.494-11.846,28.141-26.963,29.805 + c 0.563,4.23, 3.77,30.957-0.032,63.379c-3.814,32.55-13.741,59.398-28.774,79.712c 4.122,3.226, 7.072,7.514, 8.621,12.608 + c 52.666-11.379, 104.41-46.362, 127.552-76.422c-8.64-5.306-14.464-14.746-14.464-25.581c0-16.666, 13.555-30.227, 30.214-30.227 + c 16.646,0, 30.189,13.555, 30.189,30.227C 435.213,87.040, 421.67,100.55, 405.024,100.55z M 71.334,110.035c-3.584,2.803-7.776,4.838-12.358,5.786 + c 1.056,0.806, 2.131,1.581, 3.085,2.534c 5.715,5.709, 8.864,13.293, 8.864,21.363c0,8.058-3.149,15.642-8.883,21.357 + c-1.203,1.19-2.547,2.221-3.942,3.181c 33.478,26.554, 95.533,32.416, 144.934,20.512c-0.051-0.358-0.070-0.717-0.109-1.082 + c-28.307-2.752-60.282-16.845-94.15-41.984C 94.022,130.765, 81.261,119.456, 71.334,110.035z" /> +<glyph unicode="r" d="M 60,231.2l0-6.002 L 60,140 l-14.4,0 L 45.6,231.59 c 4.8,3.331, 6.4,8.315, 6.4,13.763l0,15.717 c0,9.822-6.648,16.53-16.49,16.53l-2.899,0 + C 22.779,277.6, 13.6,270.893, 13.6,261.070l0-15.717 c0-5.437, 3.2-10.422, 6.4-13.758L 20,114.4 l 40,0 l0-12.422 L 60,44 l 12.8,0 l0,57.6 l 48,0 l0-60.8 l 145.6,0 l0,60.8 l 20.8,0 l0-60.8 l 145.6,0 l0,61.178 + l0,12.55 L 432.8,231.2 L 60,231.2 z M 159.2,47.2l-11.2,0 l0,54.4 l 11.2,0 L 159.2,47.2 z M 186.4,47.2l-12.8,0 l0,54.4 l 12.8,0 L 186.4,47.2 z M 213.6,47.2l-12.8,0 l0,54.4 l 12.8,0 L 213.6,47.2 z M 239.2,47.2l-11.2,0 l0,54.4 l 11.2,0 + L 239.2,47.2 z M 325.6,47.2l-12.8,0 l0,54.4 l 12.8,0 L 325.6,47.2 z M 352.8,47.2l-12.8,0 l0,54.4 l 12.8,0 L 352.8,47.2 z M 380,47.2l-12.8,0 l0,54.4 l 12.8,0 L 380,47.2 z M 405.6,47.2l-11.2,0 l0,54.4 l 11.2,0 L 405.6,47.2 z" /> +<glyph unicode="R" d="M 5.6,248.8l0-179.2 l 184,0 l0,35.2 l 68.8,0 l0-35.2 l 180.8,0 l0,179.2 L 5.6,248.8 z M 50.4,76l-12.8,0 l0,35.2 l 12.8,0 L 50.4,76 z M 79.2,76l-12.8,0 l0,35.2 l 12.8,0 L 79.2,76 z M 108,76l-11.2,0 l0,35.2 l 11.2,0 + L 108,76 z M 136.8,76l-12.8,0 l0,35.2 l 12.8,0 L 136.8,76 z M 165.6,76l-11.2,0 l0,35.2 l 11.2,0 L 165.6,76 z M 293.6,76l-12.8,0 l0,35.2 l 12.8,0 L 293.6,76 z M 322.4,76l-12.8,0 l0,35.2 l 12.8,0 L 322.4,76 z + M 351.2,76l-12.8,0 l0,35.2 l 12.8,0 L 351.2,76 z M 380,76l-12.8,0 l0,35.2 l 12.8,0 L 380,76 z M 408.8,76l-12.8,0 l0,35.2 l 12.8,0 L 408.8,76 z" /> +<glyph unicode="P" d="M 60,231.2l0-6.371 L 60,140 l-14.4,0 L 45.6,231.222 c 4.8,3.331, 6.4,8.315, 6.4,13.762l0,15.718 c0,9.821-6.648,18.499-16.49,18.499l-2.899,0 + C 22.779,279.2, 13.6,270.522, 13.6,260.701l0-15.718 c0-5.435, 3.2-10.421, 6.4-13.758L 20,114.4 l 40,0 l0-12.79 L 60,44 l 12.8,0 l0,57.6 l 48,0 l0-60.8 l 145.6,0 l0,60.8 l 20.8,0 l0-60.8 l 145.6,0 l0,60.81 + l0,12.55 L 432.8,231.2 L 60,231.2 z M 59.218,127.2L 32.8,127.2 L 32.8,238.53 l-3.469,1.72c-1.837,0.899-2.931,2.712-2.931,4.733l0,15.718 + c0,2.901, 3.299,5.699, 6.213,5.699l 2.899,0 c 2.92,0, 5.29-2.797, 5.29-5.699l0-15.718 c0-2.027-1.888-3.838-3.718-4.728L 32.8,238.538L 32.8,127.2 + L 59.218,127.2 L 59.218,127.2z M 133.6,101.6l 14.4,0 l0-48 l-14.4,0 L 133.6,101.6 z M 228,101.6l0-48 l-14.4,0 l0,48 L 228,101.6 z M 200.8,53.6l-14.4,0 l0,48 l 14.4,0 L 200.8,53.6 z M 173.6,53.6l-14.4,0 + l0,48 l 14.4,0 L 173.6,53.6 z M 253.6,53.6l-14.4,0 l0,48 l 14.4,0 L 253.6,53.6 z M 300,101.6l 12.8,0 l0-48 l-12.8,0 L 300,101.6 z M 394.4,101.6l0-48 l-14.4,0 l0,48 L 394.4,101.6 z M 367.2,53.6l-14.4,0 l0,48 l 14.4,0 + L 367.2,53.6 z M 340,53.6l-14.4,0 l0,48 l 14.4,0 L 340,53.6 z M 420,53.6l-14.4,0 l0,48 l 14.4,0 L 420,53.6 z M 288.485,114.4l-21.502,0 L 122.357,114.4 L 72.8,114.4 l0,104 l 347.2,0 l0-104 L 288.485,114.4 z" /> +<glyph unicode="J" d="M 5.6,250.4l0-180.8 l 184,0 l0,35.2 l 68.8,0 l0-35.2 l 180.8,0 l0,180.8 L 5.6,250.4 z M 426.4,82.4l-17.6,0 l0,28.8 l-12.8,0 l0-28.8 l-16,0 l0,28.8 l-12.8,0 l0-28.8 l-16,0 l0,28.8 l-12.8,0 l0-28.8 l-16,0 l0,28.8 l-12.8,0 l0-28.8 l-16,0 + l0,28.8 l-12.8,0 l0-28.8 l-9.6,0 l0,35.2 l-94.4,0 l0-35.2 l-11.2,0 l0,28.8 l-11.2,0 l0-28.8 l-17.6,0 l0,28.8 l-12.8,0 l0-28.8 l-16,0 l0,28.8 l-11.2,0 l0-28.8 l-17.6,0 l0,28.8 l-12.8,0 l0-28.8 l-16,0 l0,28.8 l-12.8,0 l0-28.8 l-19.2,0 l0,155.2 l 408,0 L 426.4,82.4 z" /> +<glyph unicode="U" d="M 168.051,134.106c-2.278,3.597-4.166,7.45-5.638,11.52L 87.898,114.88c-5.254,6.982-13.542,11.571-22.938,11.571 + c-15.91,0-28.858-12.915-28.858-28.774c0-15.936, 12.941-28.89, 28.858-28.89c 15.872,0, 28.794,12.947, 28.794,28.89 + c0,1.901-0.211,3.744-0.557,5.536L 168.051,134.106zM 193.286,112.122l-31.949-76.896c-1.933,0.397-3.91,0.634-5.926,0.634c-7.693,0-14.938-2.989-20.403-8.435 + c-5.459-5.472-8.448-12.736-8.422-20.461c 0.019-7.699, 3.034-14.912, 8.467-20.326c 5.446-5.427, 12.678-8.41, 20.39-8.41 + s 14.95,2.989, 20.429,8.422c 11.162,11.213, 11.168,29.485-0.032,40.8c-0.896,0.89-1.907,1.606-2.893,2.355l 32.518,78.266 + C 201.203,108.966, 197.146,110.355, 193.286,112.122zM 262.4,172.8L 224,172.8L 224,211.2L 211.2,211.2L 211.2,172.8L 172.8,172.8L 172.8,160L 211.2,160L 211.2,121.6L 224,121.6L 224,160L 262.4,160 zM 61.92,134.138c 7.725,0, 14.989,3.027, 20.397,8.486c 4.051,4.013, 6.746,9.043, 7.866,14.534l 69.408-0.243 + c-0.416,2.81-0.698,5.67-0.698,8.602c0,1.421, 0.115,2.803, 0.211,4.198l-69.267,0.243c-1.248,5.030-3.738,9.677-7.494,13.434 + c-5.446,5.466-12.678,8.461-20.384,8.461s-14.944-3.002-20.403-8.448c-11.251-11.264-11.245-29.574, 0.006-40.838 + C 47.008,137.126, 54.24,134.138, 61.92,134.138zM 309.44,156.794c 2.739-13.075, 14.349-22.931, 28.23-22.931c 15.891,0, 28.826,12.934, 28.826,28.838c0,15.872-12.934,28.8-28.826,28.8 + c-13.51,0-24.806-9.363-27.923-21.907l-33.645-0.064c 0.090-1.331, 0.205-2.662, 0.205-4.019c0-2.995-0.294-5.907-0.73-8.781L 309.44,156.794 + zM 408.269,78.285c-5.459,5.453-12.704,8.461-20.403,8.461c-7.482,0-14.509-2.893-19.885-8.058 + c 0.038,0.045, 0.077,0.096, 0.122,0.134L 270.4,139.974c-1.875-3.872-4.141-7.514-6.784-10.848l 97.389-60.947 + c-3.885-10.272-1.76-22.349, 6.477-30.675c 5.453-5.44, 12.685-8.422, 20.397-8.422s 14.95,2.989, 20.416,8.461 + C 419.45,48.813, 419.437,67.085, 408.269,78.285zM 227.661,320.403l-7.763-96.294c 4.371-0.173, 8.614-0.813, 12.685-1.888l 7.853,97.414c 5.862,0.986, 11.283,3.686, 15.59,7.974 + c 11.194,11.238, 11.194,29.51-0.013,40.749c-5.459,5.453-12.704,8.448-20.416,8.448s-14.957-3.002-20.397-8.448 + c-5.453-5.453-8.442-12.685-8.435-20.39s 3.014-14.938, 8.454-20.365C 218.714,324.102, 223.014,321.734, 227.661,320.403zM 263.456,202.118c 2.669-3.334, 4.947-6.982, 6.848-10.854l 93.37,69.094c 4.755-3.142, 10.746-4.992, 17.651-4.992 + c 16.698,0, 28.826,10.95, 28.826,26.035c0,15.923-12.934,28.89-28.826,28.89c-15.878,0-28.794-12.96-28.794-28.89 + c0-4.218, 0.966-8.038, 2.618-11.424L 263.456,202.118zM 60.141,254.048c 7.475,0, 14.483,2.886, 19.827,8.013l 87.45-66.195c 2.227,3.674, 4.819,7.085, 7.77,10.176l-87.693,66.368 + c-1.434-3.373-3.507-6.483-6.189-9.139c-0.147-0.147-0.32-0.269-0.486-0.397c 5.242,5.35, 8.166,12.41, 8.186,19.949 + c 0.019,7.75-2.976,15.014-8.435,20.474c-5.446,5.466-12.678,8.461-20.384,8.461s-14.944-3.002-20.403-8.448 + c-11.245-11.264-11.245-29.574, 0.006-40.838C 45.235,257.037, 52.467,254.048, 60.141,254.048zM 265.242,0.883L 239.872,111.206c-3.91-1.606-8.045-2.771-12.326-3.501l 25.178-109.536c-9.843-4.589-16.717-14.522-16.717-26.080 + c0-15.936, 12.922-28.902, 28.8-28.902c 15.923,0, 28.883,12.96, 28.883,28.902C 293.69-12.16, 280.96,0.646, 265.242,0.883z" /> +<glyph unicode="T" d="M 168.051,134.106c-2.272,3.597-4.16,7.45-5.638,11.52L 87.898,114.88c-5.254,6.982-13.536,11.571-22.938,11.571 + c-15.91,0-28.858-12.915-28.858-28.774c0-15.936, 12.941-28.89, 28.858-28.89c 15.872,0, 28.794,12.947, 28.794,28.89 + c0,1.901-0.205,3.744-0.557,5.536L 168.051,134.106z M 64.96,81.587c-8.858,0-16.058,7.213-16.058,16.090c0,8.8, 7.2,15.974, 16.058,15.974 + c 8.819,0, 15.994-7.174, 15.994-15.974C 80.954,88.8, 73.779,81.587, 64.96,81.587zM 193.286,112.122l-31.949-76.896c-1.933,0.397-3.91,0.634-5.926,0.634c-7.693,0-14.938-2.989-20.403-8.435 + c-5.459-5.472-8.448-12.736-8.422-20.461c 0.019-7.699, 3.034-14.912, 8.467-20.326c 5.446-5.427, 12.678-8.41, 20.39-8.41 + s 14.95,2.989, 20.429,8.422c 11.162,11.213, 11.168,29.485-0.032,40.8c-0.902,0.89-1.914,1.606-2.893,2.355l 32.518,78.266 + C 201.21,108.966, 197.152,110.355, 193.286,112.122z M 166.822-4.294c-6.074-6.035-16.666-6.048-22.746,0 + c-3.027,3.002-4.691,7.014-4.698,11.277c-0.013,4.301, 1.651,8.352, 4.672,11.373c 3.040,3.040, 7.072,4.698, 11.354,4.698 + c 4.294,0, 8.352-1.677, 11.373-4.666C 173.011,12.109, 173.018,1.92, 166.822-4.294zM 262.4,172.8L 224,172.8L 224,211.2L 211.2,211.2L 211.2,172.8L 172.8,172.8L 172.8,160L 211.2,160L 211.2,121.6L 224,121.6L 224,160L 262.4,160 zM 61.926,134.138c 7.725,0, 14.989,3.027, 20.39,8.486c 4.058,4.013, 6.752,9.043, 7.866,14.534l 69.408-0.243 + c-0.41,2.81-0.698,5.67-0.698,8.602c0,1.421, 0.115,2.803, 0.211,4.198l-69.261,0.243c-1.248,5.030-3.738,9.677-7.501,13.434 + c-5.44,5.466-12.672,8.461-20.378,8.461s-14.95-3.002-20.41-8.448c-11.251-11.264-11.238-29.574, 0.013-40.838 + C 47.014,137.126, 54.24,134.138, 61.926,134.138z M 50.598,174.349c 3.040,3.027, 7.072,4.698, 11.36,4.698c 4.275,0, 8.288-1.664, 11.328-4.698 + s 4.698-7.085, 4.685-11.398c-0.013-4.275-1.664-8.262-4.71-11.277c-3.027-3.066-7.053-4.736-11.334-4.736c-4.275,0-8.288,1.664-11.328,4.685 + C 44.352,157.888, 44.352,168.077, 50.598,174.349zM 309.446,156.794c 2.739-13.075, 14.349-22.931, 28.23-22.931c 15.891,0, 28.826,12.934, 28.826,28.838c0,15.872-12.934,28.8-28.826,28.8 + c-13.51,0-24.806-9.363-27.923-21.907l-33.638-0.064c 0.090-1.331, 0.198-2.662, 0.198-4.019c0-2.995-0.294-5.907-0.723-8.781 + L 309.446,156.794z M 337.677,178.701c 8.838,0, 16.026-7.174, 16.026-16c0-8.838-7.187-16.038-16.026-16.038c-8.851,0-16.051,7.2-16.051,16.038 + C 321.626,171.526, 328.826,178.701, 337.677,178.701zM 408.275,78.285c-5.459,5.453-12.704,8.461-20.403,8.461c-7.482,0-14.509-2.893-19.885-8.058 + c 0.045,0.045, 0.077,0.096, 0.122,0.134L 270.4,139.974c-1.875-3.872-4.134-7.514-6.784-10.848l 97.395-60.947 + c-3.885-10.272-1.76-22.349, 6.477-30.675c 5.453-5.44, 12.685-8.422, 20.397-8.422s 14.95,2.989, 20.416,8.461 + C 419.45,48.813, 419.437,67.085, 408.275,78.285z M 399.226,46.56c-6.048-6.061-16.659-6.010-22.669-0.026 + c-6.234,6.285-6.246,16.474-0.013,22.701c 3.027,3.040, 7.053,4.71, 11.328,4.71c 4.282,0, 8.314-1.677, 11.347-4.71 + C 405.427,63.014, 405.421,52.826, 399.226,46.56zM 227.661,320.403l-7.763-96.294c 4.371-0.173, 8.614-0.813, 12.685-1.888l 7.853,97.414c 5.856,0.986, 11.277,3.686, 15.59,7.974 + c 11.194,11.238, 11.194,29.51-0.013,40.749c-5.459,5.453-12.704,8.448-20.416,8.448s-14.957-3.002-20.397-8.448 + c-5.453-5.453-8.442-12.685-8.435-20.39s 3.014-14.938, 8.454-20.365C 218.72,324.102, 223.014,321.734, 227.661,320.403z M 224.256,359.315 + c 3.027,3.040, 7.053,4.698, 11.334,4.698c 4.294,0, 8.333-1.677, 11.36-4.698c 6.234-6.253, 6.234-16.41, 0.013-22.65 + c-6.074-6.048-16.646-6.074-22.701,0c-3.034,3.027-4.704,7.053-4.704,11.328C 219.558,352.262, 221.222,356.288, 224.256,359.315zM 263.456,202.118c 2.669-3.334, 4.947-6.982, 6.842-10.854l 93.376,69.094c 4.749-3.142, 10.739-4.992, 17.651-4.992 + c 16.698,0, 28.826,10.95, 28.826,26.035c0,15.923-12.934,28.89-28.826,28.89c-15.878,0-28.794-12.96-28.794-28.89 + c0-4.218, 0.966-8.038, 2.618-11.424L 263.456,202.118z M 381.325,297.485c 8.838,0, 16.026-7.226, 16.026-16.090c0-9.773-8.634-13.235-16.026-13.235 + c-7.379,0-15.994,3.462-15.994,13.235C 365.331,290.266, 372.506,297.485, 381.325,297.485zM 60.141,254.048c 7.482,0, 14.483,2.886, 19.827,8.013l 87.443-66.195c 2.227,3.674, 4.826,7.085, 7.776,10.176l-87.686,66.368 + c-1.44-3.373-3.514-6.483-6.195-9.139c-0.147-0.147-0.32-0.269-0.48-0.397c 5.235,5.35, 8.16,12.41, 8.179,19.949 + c 0.019,7.75-2.976,15.014-8.435,20.474c-5.446,5.466-12.678,8.461-20.384,8.461s-14.944-3.002-20.403-8.448 + c-11.245-11.264-11.245-29.574, 0.006-40.838C 45.235,257.037, 52.467,254.048, 60.141,254.048z M 48.832,294.266c 3.040,3.027, 7.072,4.698, 11.354,4.698 + c 4.275,0, 8.294-1.664, 11.328-4.698c 3.040-3.040, 4.704-7.085, 4.691-11.398c-0.013-4.275-1.67-8.262-4.71-11.277 + c-6.042-6.112-16.595-6.112-22.656-0.064C 42.573,277.786, 42.573,287.987, 48.832,294.266zM 265.242,0.883L 239.872,111.206c-3.91-1.606-8.045-2.771-12.326-3.501l 25.178-109.536c-9.837-4.589-16.71-14.522-16.71-26.080 + c0-15.936, 12.922-28.902, 28.8-28.902c 15.923,0, 28.883,12.96, 28.883,28.902C 293.696-12.16, 280.96,0.646, 265.242,0.883z M 264.813-44.013 + c-8.826,0-16,7.226-16,16.102c0,8.838, 7.174,16.038, 16,16.038c 8.87,0, 16.083-7.2, 16.083-16.038 + C 280.896-36.934, 273.83-44.013, 264.813-44.013z" /> +<glyph unicode="S" d="M 253.075,45.901l0,44.365 l 155.258,0 L 408.333,371.2 L 46.067,371.2 l0-280.934 l 162.65,0 l0-44.365 L 23.891,45.901 L 23.891-57.6 l 406.624,0 L 430.515,45.901 L 253.075,45.901 z M 245.683,178.976l-14.784,0 l0,51.75 + l-51.757,0 l0,14.79 l 51.757,0 l0,44.358 l 14.784,0 l0-44.358 l 44.365,0 l0-14.79 l-44.365,0 L 245.683,178.976 z" /> +<glyph unicode="Q" d="M 6.4,384l0-441.6 l 435.2,0 L 441.6,384 L 6.4,384 z M 345.6,128L 249.6,128 l0-44.8 l 96,0 l0-57.6 L 108.8,25.6 l0,57.6 l 96,0 l0,44.8 L 108.8,128 L 108.8,313.6 l 236.8,0 L 345.6,128 z" /> +<glyph unicode="6" d="M 236.090-16.141c-52.864,0-100.858,21.158-136.038,55.411l-7.853-8.346c 33.37-43.181, 85.363-72.282, 143.712-72.282 + c 83.36,0, 153.773,59.347, 175.334,134.406C 379.411,28.429, 312.87-16.141, 236.090-16.141zM 66.528,82.566c-2.272,3.994-4.461,8.045-6.458,12.205c 1.242-4.512, 2.656-8.966, 4.25-13.357L 66.528,82.566zM 29.536,48.8L 58.739,14.042L 151.328,112.435 zM 179.014,141.357l 7.629-10.061L 108.813,48.582c 32.89-32.096, 77.798-51.923, 127.277-51.923c 100.563,0, 182.374,81.798, 182.374,182.349 + S 336.653,361.357, 236.090,361.357c-100.557,0-182.362-81.798-182.362-182.349c0-32.928, 8.819-63.814, 24.147-90.509L 179.014,141.357z M 192,185.6l 38.4,0 l0,38.4 l 12.8,0 + l0-38.4 l 38.4,0 l0-12.8 l-38.4,0 l0-38.4 l-12.8,0 l0,38.4 l-38.4,0 L 192,185.6 z" /> +<glyph unicode="p" d="M 223.744,377.235c-119.891,0-217.427-97.542-217.427-217.434C 6.317,39.923, 103.853-57.6, 223.744-57.6c 119.917,0, 217.478,97.523, 217.478,217.402 + C 441.222,279.699, 343.661,377.235, 223.744,377.235z M 248.838,42.957l-46.963,0 L 201.875,185.914 l 46.963,0 L 248.838,42.957 z M 244.627,237.683 + c-4.275-3.942-10.688-5.914-19.2-5.914c-17.024,0.006-25.549,7.514-25.549,22.515c0,14.931, 8.518,22.387, 25.549,22.387 + c 17.069,0, 25.581-7.456, 25.581-22.387C 251.002,247.187, 248.877,241.651, 244.627,237.683z" /> +<glyph unicode="y" d="M 247.059,291.2c-0.627,0-1.248,0-1.875,0c-36.845,0-74.56,9.798-75.328,29.946l 12.794-0.173 + c 0.243-6.445, 21.978-18.97, 62.643-19.29c 39.597-0.512, 65.6,10.771, 66.157,18.707l 12.768,0.352C 322.778,300.403, 284.634,291.2, 247.059,291.2zM 246.88,246.4c-0.646,0-1.28,0-1.907,0c-30.643,0-82.803,7.053-83.936,27.616l 12.781,1.197 + c 1.626-4.826, 27.552-13.99, 71.238-14.291c 45.638-0.282, 73.498,9.67, 75.136,14.822l 12.73-2.17C 331.027,252.8, 278.848,246.4, 246.88,246.4zM 247.494,187.072c-1.146,0-2.272,0.006-3.411,0.019c-57.92,0.474-117.222,15.11-118.477,42.17l 12.787,0.589 + c 0.563-12.147, 41.082-29.427, 105.805-29.958c 67.584-0.704, 110.451,16.858, 111.424,29.715l 12.762-0.96C 366.323,201.421, 305.203,187.072, 247.494,187.072 + zM 246.976,121.126c-1.171,0-2.33,0.006-3.494,0.013c-70.662,0.486-142.931,17.082-144.326,47.853l 12.787,0.576 + c 0.646-14.195, 47.891-35.046, 131.635-35.622c 84.115-0.845, 137.325,20.154, 138.566,35.354l 12.755-1.037 + C 392.378,137.427, 317.466,121.126, 246.976,121.126zM 247.36,54.381c-1.453,0-2.893,0.006-4.346,0.019c-79.046,0.461-159.936,18.95-161.574,53.382l 12.787,0.614 + c 0.909-19.078, 62.272-40.685, 148.877-41.197c 90.291-0.678, 155.053,20.915, 156.736,40.851l 12.755-1.075 + C 409.696,72.55, 326.125,54.381, 247.36,54.381zM 247.053-3.718c-0.992,0-1.978,0.006-2.97,0.013c-57.926,0.429-117.229,16.077-118.477,45.126l 12.787,0.55 + c 0.653-15.181, 44.262-32.41, 105.798-32.877c 1.005-0.006, 2.010-0.013, 3.002-0.013c 60.96,0, 107.328,17.037, 108.422,32.672l 12.768-0.902 + C 366.342,11.693, 304.915-3.718, 247.053-3.718zM 246.726-54.4c-0.518,0-1.030,0-1.542,0c-36.794,0-74.381,12.762-75.066,35.45L 182.912-18.56 + c 0.282-9.306, 24.16-22.573, 62.413-23.040c 0.531-0.006, 1.069-0.006, 1.6-0.006c 38.938,0, 63.622,13.293, 64.237,23.027l 12.774-0.806 + C 322.49-42.266, 283.366-54.4, 246.726-54.4zM 247.181,336l-2.842,0 c-11.091,0-108.461,2.374-109.843,28.787l-0.352,7.29l 112.851,0.442l 113.427-0.397l-0.883-8.205 + C 356.301,337.798, 258.336,336, 247.181,336z M 161.901,358.758C 177.325,354.413, 204.8,348.8, 244.397,348.8l 2.784,0 c 38.419,0, 66.899,5.21, 83.424,9.69 + L 246.963,358.874L 161.901,358.758z" /> +<glyph unicode="z" d="M 223.104,352.871c-121.023,0-219.482-98.685-219.482-219.991 0-61.518 25.113-119.179 72.63-164.638l 1.159-2.231l 291.368,0 l 1.161,2.218c 47.52,45.47 72.646,102.624 72.646,164.237 0,121.225-98.459,220.405-219.482,220.405zM 365.572-26.006l-284.952,0 c-45.164,45.237-69.021,98.967-69.021,157.762 0,116.899 94.879,212.569 211.497,212.569s 211.497-95.874 211.497-212.698c 0.008-58.878-23.852-112.394-69.021-157.633zM 219.111,311.958l 7.986,0 l0-50.567 l-7.986,0 zM 354.83,141.641l 50.556,0 l0-7.982 l-50.556,0 zM 43.481,141.641l 50.563,0 l0-7.982 l-50.563,0 zM 128.402,224.84l-36.003,35.998 6.351,6.352 36.008-35.999zM 317.019,231.21l 36.001,36.004 6.358-6.349-36.008-36.002zM 219.285,234.983l-13.87-184.101-0.263-4.251-1.848-6.109l 2.385,0 c 1.862-7.985 8.49-14.2 18.682-14.2 10.297,0 16.959,6.215 18.739,14.2l 0.989,0 l-16.854,193.389-7.96,1.072zM 235.576,46.93c-0.346-7.283-3.801-10.532-11.211-10.532-7.412,0-10.865,3.249-11.213,10.533l-0.020,0.263 10.426,138.341 12.057-137.572-0.039-1.033z" /> +<glyph unicode="t" d="M 175.283,283l 98.042,0 l0-98.061 l-98.042,0 l0,98.061 zM 0.55,282.968l 97.99,0 l0-97.99 l-97.99,0 l0,97.99 zM 175.283,124.997l 98.042,0 l0-98.003 l-98.042,0 l0,98.003 zM 349.414,283l 97.978,0 l0-98.061 l-97.978,0 l0,98.061 zM 349.414,124.997l 97.978,0 l0-98.003 l-97.978,0 l0,98.003 zM 0.55,124.997l 97.99,0 l0-98.003 l-97.99,0 l0,98.003 z" /> +<glyph unicode="s" d="M 0.928,283l 48.026,0 l0-48.038 l-48.026,0 l0,48.038 zM 0.928,179.013l 48.026,0 l0-48.083 l-48.026,0 l0,48.083 zM 0.928,75.026l 48.026,0 l0-48.026 l-48.026,0 l0,48.026 zM 122.266,271.038l 235.213,0 l0-24.013 l-235.213,0 l0,24.013 zM 122.266,167.013l 235.213,0 l0-24.032 l-235.213,0 l0,24.032 zM 119.712,62.552l 235.29,0 l0-24.019 l-235.29,0 l0,24.019 z" /> +<glyph unicode="n" d="M 303.938,144l0,32 q0,6.5-4.75,11.25t-11.25,4.75l-192,0 q-6.5,0-11.25-4.75t-4.75-11.25l0-32 q0-6.5 4.75-11.25t 11.25-4.75l 192,0 q 6.5,0 11.25,4.75t 4.75,11.25zM 383.938,160q0-52.25-25.75-96.375t-69.875-69.875-96.375-25.75-96.375,25.75-69.875,69.875-25.75,96.375 25.75,96.375 69.875,69.875 96.375,25.75 96.375-25.75 69.875-69.875 25.75-96.375z" horiz-adv-x="384" /> +<glyph unicode="l" d="M 320.938,200.5q0,7-4.5,11.5l-22.75,22.5q-4.75,4.75-11.25,4.75t-11.25-4.75l-102-101.75-56.5,56.5q-4.75,4.75-11.25,4.75t-11.25-4.75l-22.75-22.5q-4.5-4.5-4.5-11.5 0-6.75 4.5-11.25l 90.5-90.5q 4.75-4.75 11.25-4.75 6.75,0 11.5,4.75l 135.75,135.75q 4.5,4.5 4.5,11.25zM 383.938,160q0-52.25-25.75-96.375t-69.875-69.875-96.375-25.75-96.375,25.75-69.875,69.875-25.75,96.375 25.75,96.375 69.875,69.875 96.375,25.75 96.375-25.75 69.875-69.875 25.75-96.375z" horiz-adv-x="384" /> +<glyph unicode="j" d="M 327.938,160.75q0,40.25-21.75,73.75l-188.5-188.25q 34.25-22.25 74.25-22.25 27.75,0 52.875,10.875t 43.375,29.125 29,43.625 10.75,53.125zM 78.188,86l 188.75,188.5q-33.75,22.75-75,22.75-37,0-68.25-18.25t-49.5-49.75-18.25-68.5q0-40.5 22.25-74.75zM 383.938,160.75q0-39.25-15.25-75t-40.875-61.5-61.25-41-74.625-15.25-74.625,15.25-61.25,41-40.875,61.5-15.25,75 15.25,74.875 40.875,61.375 61.25,41 74.625,15.25 74.625-15.25 61.25-41 40.875-61.375 15.25-74.875z" horiz-adv-x="384" /> +<glyph unicode="i" d="M 280,319.868l0-59.482 c 15.84-6.914 30.406-16.803 42.995-29.391 26.443-26.442 41.005-61.6 41.005-98.995s-14.563-72.552-41.005-98.995c-26.442-26.442-61.599-41.005-98.995-41.005s-72.552,14.563-98.995,41.005c-26.442,26.442-41.005,61.6-41.005,98.995s 14.563,72.552 41.005,98.995c 12.589,12.589 27.155,22.478 42.995,29.392l0,59.481 c-80.959-24.098-140-99.082-140-187.868 0-108.248 87.753-196 196-196 108.248,0 196,87.752 196,196 0,88.786-59.041,163.77-140,187.868zM 196,384l 56,0 l0-224 l-56,0 z" /> +<glyph unicode="h" d="M 417.75,242.5q0-10-7-17l-215-215q-7-7-17-7t-17,7l-124.5,124.5q-7,7-7,17t 7,17l 34,34q 7,7 17,7t 17-7l 73.5-73.75 164,164.25q 7,7 17,7t 17-7l 34-34q 7-7 7-17z" /> +<glyph unicode="g" d="M 483.219,311.984c-17.962-7.966-37.394-13.426-57.68-15.82 20.678,12.404 36.708,32.144 44.324,55.636-19.628-11.494-41.118-19.908-63.882-24.402-18.438,19.53-44.576,31.794-73.374,31.794-55.636,0-100.548-45.108-100.548-100.548 0-7.882 0.756-15.582 2.548-22.932-83.552,4.116-157.542,44.226-207.228,104.986-8.638-14.812-13.594-32.13-13.594-50.498 0-34.902 17.766-65.688 44.786-83.622-16.562,0.42-32.13,4.97-45.654,12.544 0-0.504 0-0.812 0-1.33 0-48.58 34.72-89.334 80.682-98.574-8.456-2.282-17.276-3.486-26.53-3.486-6.496,0-12.726,0.56-18.9,1.708 12.768-39.858 49.91-68.95 93.884-69.776-34.3-27.048-77.672-42.966-124.866-42.966-7.994,0-16.128,0.49-23.982,1.344 44.548-28.476 97.44-45.276 154.21-45.276 184.856,0 285.978,153.328 285.978,286.16 0,4.382-0.154,8.666-0.294,12.95 19.67,14.28 36.61,31.962 50.12,52.108z" horiz-adv-x="490" /> +<glyph unicode="d" d="M 446.285-9.658l-150.957,151.258c 14.278,23.603 22.957,51.168 22.957,80.922 0,86.893-70.739,157.613-157.626,157.613-86.886,0-157.613-70.726-157.613-157.613 0-86.905 70.733-157.613 157.613-157.613 30.611,0 59.008,9.069 83.072,24.199l 150.765-150.566 51.789,51.802zM 39.687,222.522c0,66.65 54.298,120.947 120.953,120.947s 120.96-54.298 120.96-120.947c0-66.669-54.304-120.953-120.96-120.953s-120.953,54.291-120.953,120.953z" /> +<glyph unicode="c" d="M 286.262,103.552q0,6.72-4.928,11.2l-45.248,45.248 45.248,45.248q 4.928,4.928 4.928,11.2 0,6.72-4.928,11.648l-22.4,22.4q-4.928,4.928-11.648,4.928t-11.2-4.928l-45.248-45.248-45.248,45.248q-4.928,4.928-11.2,4.928-6.72,0-11.648-4.928l-22.4-22.4q-4.928-4.928-4.928-11.648t 4.928-11.2l 45.248-45.248-45.248-45.248q-4.928-4.928-4.928-11.2 0-6.72 4.928-11.648l 22.4-22.4q 4.928-4.928 11.648-4.928t 11.2,4.928l 45.248,45.248 45.248-45.248q 4.928-4.928 11.2-4.928 6.72,0 11.648,4.928l 22.4,22.4q 4.928,4.928 4.928,11.648zM 383.029,160q0-52.416-25.536-96.32t-69.888-69.888-96.32-25.536-96.32,25.536-69.888,69.888-25.536,96.32 25.536,96.32 69.888,69.888 96.32,25.536 96.32-25.536 69.888-69.888 25.536-96.32z" horiz-adv-x="382" /> +<glyph unicode="b" d="M0,384l0-448 l 448,0 l0,448 l-448,0 zM 420-36l-392,0 l0,392 l 392,0 l0-392 zM 336,286l-140-140-84,84-56-56 140-140 196,196-56,56z" /> +<glyph unicode="a" d="M0,384l0-448 l 448,0 l0,448 l-448,0 zM 420-36l-392,0 l0,392 l 392,0 l0-392 z" /> +<glyph unicode="M" d="M 346.819,192.189c-9.117-9.363-105.168-100.867-105.168-100.867-4.883-4.996-11.267-7.482-17.651-7.482-6.406,0-12.79,2.486-17.651,7.482 0,0-96.051,91.504-105.19,100.867-9.117,9.363-9.744,26.186 0,36.198 9.766,9.99 23.363,10.774 35.302,0l 87.539-83.955 87.517,83.933c 11.962,10.774 25.581,9.99 35.302,0 9.767-9.991 9.162-26.835 0-36.176z" /> +<glyph unicode="L" d="M 346.819,127.811c-9.117,9.363-105.168,100.845-105.168,100.845-4.883,4.995-11.267,7.504-17.651,7.504-6.406,0-12.79-2.509-17.651-7.504 0,0-96.051-91.482-105.19-100.845-9.117-9.363-9.744-26.208 0-36.198 9.766-9.968 23.363-10.774 35.302,0l 87.539,83.933 87.517-83.933c 11.962-10.774 25.581-9.968 35.302,0 9.767,10.013 9.162,26.857 0,36.198z" /> +<glyph unicode="I" d="M 109.63,134.49c-1.039-4.35-1.688-9.733-1.915-9.733l-0.734,0 c-1.182,10.077-3.461,15.284-6.824,25.374l-17.66,55.162l-16.186,0 l-18.082-54.124c-1.078-3.13-2.35-7.733-3.811-13.024-1.488-5.278-2.54-3.311-3.202-13.388l-0.734,0 c-0.461,10.077-2.396,12.797-5.701,25.445-3.337,12.648-8.174,34.964-14.537,55.091l-15.654,0 l 26.912-90.613l 17.264,0 l 18.641,56.584c 1.786,5.552 3.89,13.902 6.286,23.979l 0.721,0 c 3.13-10.077 5.259-20.355 6.376-23.822l 18.082-56.714l 17.712,0 l 27.276,90.613l-15.485,0 c-11.441-40.282-17.68-66.434-18.745-70.83zM 259.398,134.49c-1.038-4.35-1.688-9.733-1.921-9.733l-0.727,0 c-1.188,10.077-3.454,15.284-6.817,25.374l-17.661,55.162l-16.186,0 l-18.082-54.124c-1.078-3.13-2.35-7.733-3.805-13.024-1.494-5.278-2.546-3.311-3.202-13.388l-0.727,0 c-0.493,10.077-2.396,12.797-5.707,25.445-3.35,12.648-8.187,34.964-14.544,55.091l-15.654,0 l 26.912-90.613l 17.264,0 l 18.641,56.584c 1.78,5.552 3.883,13.902 6.286,23.979l 0.721,0 c 3.123-10.077 5.253-20.355 6.376-23.822l 18.088-56.714l 17.712,0 l 27.263,90.613l-15.485,0 c-11.434-40.282-17.699-66.434-18.745-70.83zM 409.186,134.49c-1.032-4.35-1.688-9.733-1.915-9.733l-0.727,0 c-1.188,10.077-3.493,15.284-6.856,25.374l-17.635,55.162l-16.186,0 l-18.082-54.124c-1.045-3.13-2.344-7.733-3.798-13.024-1.494-5.278-2.585-3.311-3.202-13.388l-0.734,0 c-0.461,10.077-2.363,12.797-5.701,25.445-3.356,12.648-8.193,34.964-14.549,55.091l-15.661,0 l 26.919-90.613l 17.264,0 l 18.647,56.584c 1.773,5.552 3.87,13.902 6.285,23.979l 0.714,0 c 3.123-10.077 5.253-20.355 6.376-23.822l 18.089-56.714l 17.712,0 l 27.263,90.613l-15.485,0 c-11.433-40.282-17.699-66.434-18.738-70.83z" /> +<glyph unicode="D" d="M 223.994-52.514c-117.175,0-212.508,95.346-212.508,212.521 0,117.168 95.34,212.508 212.508,212.508 117.175,0 212.521-95.34 212.521-212.508-0.001-117.175-95.347-212.521-212.521-212.521zM 223.994,358.074c-109.228,0-198.068-88.84-198.068-198.068 0-109.234 88.847-198.12 198.068-198.12 109.234,0 198.12,88.847 198.12,198.12-0.040,109.228-88.886,198.068-198.12,198.068zM 289.044,91.93l-19.537,0 l-39.736,63.402-40.403-63.402l-18.206,0 l 49.15,75.316-45.819,68.843l 18.991,0 l 36.697-57.097 36.989,57.097l 18.29,0 l-45.819-68.226 49.403-75.933z" /> +<glyph unicode="C" d="M 224,384c-123.712,0-224-100.288-224-224s 100.288-224 224-224 224,100.288 224,224-100.288,224-224,224zM 224-8c-92.784,0-168,75.216-168,168s 75.216,168 168,168c 92.784,0 168-75.216 168-168 0-92.784-75.216-168-168-168z" /> +<glyph unicode="A" d="M 416.192,143.872q-38.080,59.136-95.424,88.256 15.232-25.984 15.232-56.448 0-46.144-32.704-79.296t-79.296-32.704-79.296,32.704-32.704,79.296q0,30.464 15.232,56.448-57.344-29.12-95.424-88.256 33.152-51.072 83.328-81.536t 108.416-30.464 108.416,30.464 83.328,81.536zM 236.096,239.744q0,4.928-3.584,8.512t-8.512,3.584q-31.36,0-53.76-22.4t-22.4-53.76q0-4.928 3.584-8.512t 8.512-3.584 8.512,3.584 3.584,8.512q0,21.504 15.232,36.736t 36.736,15.232q 4.928,0 8.512,3.584t 3.584,8.512zM 448,143.872q0-8.512-4.928-17.472-34.944-57.344-94.080-92.288t-124.992-34.496-124.992,34.944-94.080,91.84q-4.928,8.96-4.928,17.472t 4.928,17.472q 34.944,57.344 94.080,91.84t 124.992,34.944 124.992-34.944 94.080-91.84q 4.928-8.96 4.928-17.472z" /> +<glyph unicode="B" d="M 224-64c-123.703,0-224,100.297-224,224s 100.297,224 224,224 224-100.297 224-224-100.297-224-224-224zM 224,328c-92.777,0-168-75.223-168-168s 75.223-168 168-168 168,75.223 168,168-75.223,168-168,168zM 224,48c-61.852,0-112,50.148-112,112s 50.148,112 112,112 112-50.148 112-112-50.148-112-112-112z" /> +<glyph unicode="?" d="M 361.229,318.042L 361.229,377.082 L 58.080,377.082 l0-375.2 l 59.949,0 l0-59.040 L 421.184-57.158 L 421.184,318.042 L 361.229,318.042 z M 88.781,346.394l 241.76,0 l0-313.811 L 88.781,32.582 L 88.781,346.394 z + M 390.483-26.458L 148.717-26.458 l0,28.339 l 212.512,0 L 361.229,287.341 l 29.248,0 L 390.477-26.458 z" /> +<glyph unicode="%" d="M 350.221,107.296L 251.974,11.974L 251.974,354.918L 215.622,354.918L 215.622,14.688L 128.55,106.746L 102.138,81.773L 233.242-56.832L 375.526,81.222 z" /> +<glyph unicode="&" d="M 361.248,352.666c-17.728,17.638-41.248,27.347-66.246,27.347c-25.005,0-48.499-9.728-66.118-27.379L 142.208,265.978 + c-17.702-17.696-27.456-41.21-27.456-66.227c0-18.899, 5.587-36.954, 15.968-52.282l-47.738-47.808c-17.683-17.638-27.437-41.133-27.456-66.144 + c-0.013-25.030, 9.709-48.531, 27.379-66.202c 17.696-17.664, 41.203-27.405, 66.189-27.405s 48.474,9.734, 66.157,27.411l 86.778,86.797 + c 32.051,32.166, 35.84,82.022, 11.43,118.374l 47.814,47.853C 397.741,256.774, 397.741,316.115, 361.248,352.666z M 276.614,79.488L 189.85-7.283 + c-10.886-10.886-25.376-16.89-40.755-16.89c-15.405,0-29.894,5.997-40.8,16.89c-10.874,10.874-16.858,25.357-16.858,40.768 + c 0.013,15.418, 6.022,29.882, 16.941,40.768l 47.757,47.834c 15.315-10.336, 33.35-15.904, 52.269-15.904c 24.998,0, 48.493,9.715, 66.15,27.366l 12.717,12.717 + C 298.131,124.653, 294.605,97.542, 276.614,79.488z M 182.317,148.32l 12.717,12.73c 10.899,10.886, 25.382,16.89, 40.794,16.89 + c 9.21,0, 18.080-2.195, 26.061-6.246l-12.755-12.755c-10.867-10.867-25.344-16.851-40.749-16.851C 199.168,142.093, 190.304,144.282, 182.317,148.32z + M 335.872,245.741l-47.782-47.814c-15.322,10.349-33.357,15.93-52.269,15.93c-24.998,0-48.506-9.728-66.202-27.405l-12.723-12.749 + c-4.051,7.981-6.246,16.851-6.246,26.054c0,15.411, 6.016,29.901, 16.941,40.826l 86.675,86.669c 10.854,10.861, 25.318,16.845, 40.723,16.845 + c 15.443,0, 29.971-5.997, 40.87-16.845C 358.336,304.749, 358.349,268.186, 335.872,245.741z" /> +<glyph unicode=")" d="M 310.886,347.149l-14.771-36.915c 60.742-24.301, 101.555-84.787, 101.555-150.522c0-89.203-73.024-161.779-162.797-161.779 + c-88.659,0-160.806,72.576-160.806,161.779c0,52.966, 23.392,99.226, 63.725,128.742l 8.998-75.59l 39.482,4.698L 169.83,355.597L 32.198,361.741 + l-1.779-39.718l 81.133-3.629c-48.403-37.069-77.235-94.867-77.235-158.688c0-111.123, 89.965-201.523, 200.55-201.523 + c 111.693,0, 202.56,90.4, 202.56,201.523C 437.434,242.822, 387.763,316.397, 310.886,347.149z" /> +<glyph unicode="=" d="M 366.259,300.909l-57.6,0 l0,64 l-153.6,0 l0-64 l-57.6,0 l-25.6,0 l0-32 l 25.6,0 l0-313.6 l 268.8,0 l0,313.6 l 25.6,0 l0,32 L 366.259,300.909 z M 187.059,19.309l-32,0 l0,217.6 l 32,0 L 187.059,19.309 z M 251.059,19.309l-32,0 l0,217.6 l 32,0 + L 251.059,19.309 z M 308.659,19.309l-32,0 l0,217.6 l 32,0 L 308.659,19.309 z" /> +<glyph unicode="$" d="M 366.259,304.109l-57.6,0 l0,64 l-153.6,0 l0-64 l-57.6,0 l-25.6,0 l0-32 l 25.6,0 l0-313.6 l 268.8,0 l0,313.6 l 25.6,0 l0,32 L 366.259,304.109 z M 334.259-9.491l-204.8,0 l0,281.6 l 204.8,0 L 334.259-9.491 z M 187.059,336.109l 89.6,0 l0-32 l-89.6,0 + L 187.059,336.109 zM 219.059,240.109L 251.059,240.109L 251.059,22.509L 219.059,22.509zM 155.059,240.109L 187.059,240.109L 187.059,22.509L 155.059,22.509zM 276.659,240.109L 308.659,240.109L 308.659,22.509L 276.659,22.509z" /> +<glyph unicode="_" d="M 176.201,319.799l-140-140c-10.935-10.934-10.935-28.663,0-39.597l 140-140c 10.935-10.934, 28.663-10.934, 39.598,0 + c 10.936,10.934, 10.936,28.663,0,39.597L 123.597,132L 392,132 c 15.464,0, 28,12.535, 28,28c0,15.464-12.536,28-28,28L 123.597,188 l 92.201,92.201 + C 221.266,285.668, 224,292.835, 224,300s-2.734,14.332-8.2,19.799C 204.864,330.733, 187.136,330.733, 176.201,319.799z" /> +<glyph unicode="-" d="M 271.799,0.201l 140,140c 10.935,10.934, 10.935,28.663,0,39.598l-140,140c-10.935,10.934-28.663,10.934-39.598,0 + c-10.935-10.935-10.935-28.663,0-39.598L 324.402,188L 56,188 c-15.464,0-28-12.536-28-28s 12.536-28, 28-28l 268.402,0 L 232.201,39.799 + C 226.734,34.332, 224,27.165, 224,20s 2.734-14.332, 8.2-19.799C 243.135-10.734, 260.864-10.734, 271.799,0.201z" /> +<glyph unicode=""" d="M 359.859,281.709L 359.859,0.109L 148.666,0.109L 148.666-31.891L 391.859-31.891L 391.859,281.709 zM 91.059,345.709L 327.859,345.709L 327.859,32.109L 91.059,32.109z" /> +<glyph unicode=" " horiz-adv-x="224" /> +</font></defs></svg> \ No newline at end of file diff --git a/snf-admin-app/synnefo_admin/admin/static/fonts/snf-font.ttf b/snf-admin-app/synnefo_admin/admin/static/fonts/snf-font.ttf new file mode 100644 index 0000000000000000000000000000000000000000..13a7cc29efeaefe5286ed51ae64fe510d84a9b29 Binary files /dev/null and b/snf-admin-app/synnefo_admin/admin/static/fonts/snf-font.ttf differ diff --git a/snf-admin-app/synnefo_admin/admin/static/fonts/snf-font.woff b/snf-admin-app/synnefo_admin/admin/static/fonts/snf-font.woff new file mode 100644 index 0000000000000000000000000000000000000000..8ef6af3267ba2ed4945cc06824381e435c511ec0 Binary files /dev/null and b/snf-admin-app/synnefo_admin/admin/static/fonts/snf-font.woff differ diff --git a/snf-admin-app/synnefo_admin/admin/static/images/Sorting icons.psd b/snf-admin-app/synnefo_admin/admin/static/images/Sorting icons.psd new file mode 100644 index 0000000000000000000000000000000000000000..53b2e06850767cb57c52b316f0b845b1a8e0ca0e Binary files /dev/null and b/snf-admin-app/synnefo_admin/admin/static/images/Sorting icons.psd differ diff --git a/snf-admin-app/synnefo_admin/admin/static/images/back_disabled.png b/snf-admin-app/synnefo_admin/admin/static/images/back_disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..881de7976ff98955e2a5487dca66e618a0655f3d Binary files /dev/null and b/snf-admin-app/synnefo_admin/admin/static/images/back_disabled.png differ diff --git a/snf-admin-app/synnefo_admin/admin/static/images/back_enabled.png b/snf-admin-app/synnefo_admin/admin/static/images/back_enabled.png new file mode 100644 index 0000000000000000000000000000000000000000..c608682b04a6d9b8002602450c8ef7e80ebba099 Binary files /dev/null and b/snf-admin-app/synnefo_admin/admin/static/images/back_enabled.png differ diff --git a/snf-admin-app/synnefo_admin/admin/static/images/back_enabled_hover.png b/snf-admin-app/synnefo_admin/admin/static/images/back_enabled_hover.png new file mode 100644 index 0000000000000000000000000000000000000000..d300f1064b3beac1d7d5274e294494d3143e53a2 Binary files /dev/null and b/snf-admin-app/synnefo_admin/admin/static/images/back_enabled_hover.png differ diff --git a/snf-admin-app/synnefo_admin/admin/static/images/favicon.ico b/snf-admin-app/synnefo_admin/admin/static/images/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..6eeaa2a0d393190ce748107222d9a026f992e4a7 Binary files /dev/null and b/snf-admin-app/synnefo_admin/admin/static/images/favicon.ico differ diff --git a/snf-admin-app/synnefo_admin/admin/static/images/forward_disabled.png b/snf-admin-app/synnefo_admin/admin/static/images/forward_disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..6a6ded7de821619aedc71d1738c0b73463a4452e Binary files /dev/null and b/snf-admin-app/synnefo_admin/admin/static/images/forward_disabled.png differ diff --git a/snf-admin-app/synnefo_admin/admin/static/images/forward_enabled.png b/snf-admin-app/synnefo_admin/admin/static/images/forward_enabled.png new file mode 100644 index 0000000000000000000000000000000000000000..a4e6b5384b8454ee7f44a8f7c75b0321b7eeb9b1 Binary files /dev/null and b/snf-admin-app/synnefo_admin/admin/static/images/forward_enabled.png differ diff --git a/snf-admin-app/synnefo_admin/admin/static/images/forward_enabled_hover.png b/snf-admin-app/synnefo_admin/admin/static/images/forward_enabled_hover.png new file mode 100644 index 0000000000000000000000000000000000000000..fc46c5ebf0524b72a509fe2d7c1bc74995cb8a9d Binary files /dev/null and b/snf-admin-app/synnefo_admin/admin/static/images/forward_enabled_hover.png differ diff --git a/snf-admin-app/synnefo_admin/admin/static/images/sort_asc.png b/snf-admin-app/synnefo_admin/admin/static/images/sort_asc.png new file mode 100644 index 0000000000000000000000000000000000000000..a88d7975fe9017e4e5f2289a94bd1ed66a5f59dc Binary files /dev/null and b/snf-admin-app/synnefo_admin/admin/static/images/sort_asc.png differ diff --git a/snf-admin-app/synnefo_admin/admin/static/images/sort_asc_disabled.png b/snf-admin-app/synnefo_admin/admin/static/images/sort_asc_disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..dcd7b7b8cab2304b374e6e4b9dc8c05faa2e1130 Binary files /dev/null and b/snf-admin-app/synnefo_admin/admin/static/images/sort_asc_disabled.png differ diff --git a/snf-admin-app/synnefo_admin/admin/static/images/sort_both.png b/snf-admin-app/synnefo_admin/admin/static/images/sort_both.png new file mode 100644 index 0000000000000000000000000000000000000000..18670406bc01ab2721781822dd6478917745ff54 Binary files /dev/null and b/snf-admin-app/synnefo_admin/admin/static/images/sort_both.png differ diff --git a/snf-admin-app/synnefo_admin/admin/static/images/sort_desc.png b/snf-admin-app/synnefo_admin/admin/static/images/sort_desc.png new file mode 100644 index 0000000000000000000000000000000000000000..def071ed5afd264a036f6d9e75856366fd6ad153 Binary files /dev/null and b/snf-admin-app/synnefo_admin/admin/static/images/sort_desc.png differ diff --git a/snf-admin-app/synnefo_admin/admin/static/images/sort_desc_disabled.png b/snf-admin-app/synnefo_admin/admin/static/images/sort_desc_disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..7824973cc60fc1841b16f2cb39323cefcdc3f942 Binary files /dev/null and b/snf-admin-app/synnefo_admin/admin/static/images/sort_desc_disabled.png differ diff --git a/snf-admin-app/synnefo_admin/admin/static/js/bootstrap.js b/snf-admin-app/synnefo_admin/admin/static/js/bootstrap.js new file mode 100644 index 0000000000000000000000000000000000000000..8ae571b6da5be9c7dcd95ba25896ae39e1917445 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/js/bootstrap.js @@ -0,0 +1,1951 @@ +/*! + * Bootstrap v3.1.1 (http://getbootstrap.com) + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ + +if (typeof jQuery === 'undefined') { throw new Error('Bootstrap\'s JavaScript requires jQuery') } + +/* ======================================================================== + * Bootstrap: transition.js v3.1.1 + * http://getbootstrap.com/javascript/#transitions + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/) + // ============================================================ + + function transitionEnd() { + var el = document.createElement('bootstrap') + + var transEndEventNames = { + 'WebkitTransition' : 'webkitTransitionEnd', + 'MozTransition' : 'transitionend', + 'OTransition' : 'oTransitionEnd otransitionend', + 'transition' : 'transitionend' + } + + for (var name in transEndEventNames) { + if (el.style[name] !== undefined) { + return { end: transEndEventNames[name] } + } + } + + return false // explicit for ie8 ( ._.) + } + + // http://blog.alexmaccaw.com/css-transitions + $.fn.emulateTransitionEnd = function (duration) { + var called = false, $el = this + $(this).one($.support.transition.end, function () { called = true }) + var callback = function () { if (!called) $($el).trigger($.support.transition.end) } + setTimeout(callback, duration) + return this + } + + $(function () { + $.support.transition = transitionEnd() + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: alert.js v3.1.1 + * http://getbootstrap.com/javascript/#alerts + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // ALERT CLASS DEFINITION + // ====================== + + var dismiss = '[data-dismiss="alert"]' + var Alert = function (el) { + $(el).on('click', dismiss, this.close) + } + + Alert.prototype.close = function (e) { + var $this = $(this) + var selector = $this.attr('data-target') + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 + } + + var $parent = $(selector) + + if (e) e.preventDefault() + + if (!$parent.length) { + $parent = $this.hasClass('alert') ? $this : $this.parent() + } + + $parent.trigger(e = $.Event('close.bs.alert')) + + if (e.isDefaultPrevented()) return + + $parent.removeClass('in') + + function removeElement() { + $parent.trigger('closed.bs.alert').remove() + } + + $.support.transition && $parent.hasClass('fade') ? + $parent + .one($.support.transition.end, removeElement) + .emulateTransitionEnd(150) : + removeElement() + } + + + // ALERT PLUGIN DEFINITION + // ======================= + + var old = $.fn.alert + + $.fn.alert = function (option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.alert') + + if (!data) $this.data('bs.alert', (data = new Alert(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + $.fn.alert.Constructor = Alert + + + // ALERT NO CONFLICT + // ================= + + $.fn.alert.noConflict = function () { + $.fn.alert = old + return this + } + + + // ALERT DATA-API + // ============== + + $(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: button.js v3.1.1 + * http://getbootstrap.com/javascript/#buttons + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // BUTTON PUBLIC CLASS DEFINITION + // ============================== + + var Button = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, Button.DEFAULTS, options) + this.isLoading = false + } + + Button.DEFAULTS = { + loadingText: 'loading...' + } + + Button.prototype.setState = function (state) { + var d = 'disabled' + var $el = this.$element + var val = $el.is('input') ? 'val' : 'html' + var data = $el.data() + + state = state + 'Text' + + if (!data.resetText) $el.data('resetText', $el[val]()) + + $el[val](data[state] || this.options[state]) + + // push to event loop to allow forms to submit + setTimeout($.proxy(function () { + if (state == 'loadingText') { + this.isLoading = true + $el.addClass(d).attr(d, d) + } else if (this.isLoading) { + this.isLoading = false + $el.removeClass(d).removeAttr(d) + } + }, this), 0) + } + + Button.prototype.toggle = function () { + var changed = true + var $parent = this.$element.closest('[data-toggle="buttons"]') + + if ($parent.length) { + var $input = this.$element.find('input') + if ($input.prop('type') == 'radio') { + if ($input.prop('checked') && this.$element.hasClass('active')) changed = false + else $parent.find('.active').removeClass('active') + } + if (changed) $input.prop('checked', !this.$element.hasClass('active')).trigger('change') + } + + if (changed) this.$element.toggleClass('active') + } + + + // BUTTON PLUGIN DEFINITION + // ======================== + + var old = $.fn.button + + $.fn.button = function (option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.button') + var options = typeof option == 'object' && option + + if (!data) $this.data('bs.button', (data = new Button(this, options))) + + if (option == 'toggle') data.toggle() + else if (option) data.setState(option) + }) + } + + $.fn.button.Constructor = Button + + + // BUTTON NO CONFLICT + // ================== + + $.fn.button.noConflict = function () { + $.fn.button = old + return this + } + + + // BUTTON DATA-API + // =============== + + $(document).on('click.bs.button.data-api', '[data-toggle^=button]', function (e) { + var $btn = $(e.target) + if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') + $btn.button('toggle') + e.preventDefault() + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: carousel.js v3.1.1 + * http://getbootstrap.com/javascript/#carousel + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // CAROUSEL CLASS DEFINITION + // ========================= + + var Carousel = function (element, options) { + this.$element = $(element) + this.$indicators = this.$element.find('.carousel-indicators') + this.options = options + this.paused = + this.sliding = + this.interval = + this.$active = + this.$items = null + + this.options.pause == 'hover' && this.$element + .on('mouseenter', $.proxy(this.pause, this)) + .on('mouseleave', $.proxy(this.cycle, this)) + } + + Carousel.DEFAULTS = { + interval: 5000, + pause: 'hover', + wrap: true + } + + Carousel.prototype.cycle = function (e) { + e || (this.paused = false) + + this.interval && clearInterval(this.interval) + + this.options.interval + && !this.paused + && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) + + return this + } + + Carousel.prototype.getActiveIndex = function () { + this.$active = this.$element.find('.item.active') + this.$items = this.$active.parent().children() + + return this.$items.index(this.$active) + } + + Carousel.prototype.to = function (pos) { + var that = this + var activeIndex = this.getActiveIndex() + + if (pos > (this.$items.length - 1) || pos < 0) return + + if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) + if (activeIndex == pos) return this.pause().cycle() + + return this.slide(pos > activeIndex ? 'next' : 'prev', $(this.$items[pos])) + } + + Carousel.prototype.pause = function (e) { + e || (this.paused = true) + + if (this.$element.find('.next, .prev').length && $.support.transition) { + this.$element.trigger($.support.transition.end) + this.cycle(true) + } + + this.interval = clearInterval(this.interval) + + return this + } + + Carousel.prototype.next = function () { + if (this.sliding) return + return this.slide('next') + } + + Carousel.prototype.prev = function () { + if (this.sliding) return + return this.slide('prev') + } + + Carousel.prototype.slide = function (type, next) { + var $active = this.$element.find('.item.active') + var $next = next || $active[type]() + var isCycling = this.interval + var direction = type == 'next' ? 'left' : 'right' + var fallback = type == 'next' ? 'first' : 'last' + var that = this + + if (!$next.length) { + if (!this.options.wrap) return + $next = this.$element.find('.item')[fallback]() + } + + if ($next.hasClass('active')) return this.sliding = false + + var e = $.Event('slide.bs.carousel', { relatedTarget: $next[0], direction: direction }) + this.$element.trigger(e) + if (e.isDefaultPrevented()) return + + this.sliding = true + + isCycling && this.pause() + + if (this.$indicators.length) { + this.$indicators.find('.active').removeClass('active') + this.$element.one('slid.bs.carousel', function () { + var $nextIndicator = $(that.$indicators.children()[that.getActiveIndex()]) + $nextIndicator && $nextIndicator.addClass('active') + }) + } + + if ($.support.transition && this.$element.hasClass('slide')) { + $next.addClass(type) + $next[0].offsetWidth // force reflow + $active.addClass(direction) + $next.addClass(direction) + $active + .one($.support.transition.end, function () { + $next.removeClass([type, direction].join(' ')).addClass('active') + $active.removeClass(['active', direction].join(' ')) + that.sliding = false + setTimeout(function () { that.$element.trigger('slid.bs.carousel') }, 0) + }) + .emulateTransitionEnd($active.css('transition-duration').slice(0, -1) * 1000) + } else { + $active.removeClass('active') + $next.addClass('active') + this.sliding = false + this.$element.trigger('slid.bs.carousel') + } + + isCycling && this.cycle() + + return this + } + + + // CAROUSEL PLUGIN DEFINITION + // ========================== + + var old = $.fn.carousel + + $.fn.carousel = function (option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.carousel') + var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option) + var action = typeof option == 'string' ? option : options.slide + + if (!data) $this.data('bs.carousel', (data = new Carousel(this, options))) + if (typeof option == 'number') data.to(option) + else if (action) data[action]() + else if (options.interval) data.pause().cycle() + }) + } + + $.fn.carousel.Constructor = Carousel + + + // CAROUSEL NO CONFLICT + // ==================== + + $.fn.carousel.noConflict = function () { + $.fn.carousel = old + return this + } + + + // CAROUSEL DATA-API + // ================= + + $(document).on('click.bs.carousel.data-api', '[data-slide], [data-slide-to]', function (e) { + var $this = $(this), href + var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 + var options = $.extend({}, $target.data(), $this.data()) + var slideIndex = $this.attr('data-slide-to') + if (slideIndex) options.interval = false + + $target.carousel(options) + + if (slideIndex = $this.attr('data-slide-to')) { + $target.data('bs.carousel').to(slideIndex) + } + + e.preventDefault() + }) + + $(window).on('load', function () { + $('[data-ride="carousel"]').each(function () { + var $carousel = $(this) + $carousel.carousel($carousel.data()) + }) + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: collapse.js v3.1.1 + * http://getbootstrap.com/javascript/#collapse + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // COLLAPSE PUBLIC CLASS DEFINITION + // ================================ + + var Collapse = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, Collapse.DEFAULTS, options) + this.transitioning = null + + if (this.options.parent) this.$parent = $(this.options.parent) + if (this.options.toggle) this.toggle() + } + + Collapse.DEFAULTS = { + toggle: true + } + + Collapse.prototype.dimension = function () { + var hasWidth = this.$element.hasClass('width') + return hasWidth ? 'width' : 'height' + } + + Collapse.prototype.show = function () { + if (this.transitioning || this.$element.hasClass('in')) return + + var startEvent = $.Event('show.bs.collapse') + this.$element.trigger(startEvent) + if (startEvent.isDefaultPrevented()) return + + var actives = this.$parent && this.$parent.find('> .panel > .in') + + if (actives && actives.length) { + var hasData = actives.data('bs.collapse') + if (hasData && hasData.transitioning) return + actives.collapse('hide') + hasData || actives.data('bs.collapse', null) + } + + var dimension = this.dimension() + + this.$element + .removeClass('collapse') + .addClass('collapsing') + [dimension](0) + + this.transitioning = 1 + + var complete = function () { + this.$element + .removeClass('collapsing') + .addClass('collapse in') + [dimension]('auto') + this.transitioning = 0 + this.$element.trigger('shown.bs.collapse') + } + + if (!$.support.transition) return complete.call(this) + + var scrollSize = $.camelCase(['scroll', dimension].join('-')) + + this.$element + .one($.support.transition.end, $.proxy(complete, this)) + .emulateTransitionEnd(350) + [dimension](this.$element[0][scrollSize]) + } + + Collapse.prototype.hide = function () { + if (this.transitioning || !this.$element.hasClass('in')) return + + var startEvent = $.Event('hide.bs.collapse') + this.$element.trigger(startEvent) + if (startEvent.isDefaultPrevented()) return + + var dimension = this.dimension() + + this.$element + [dimension](this.$element[dimension]()) + [0].offsetHeight + + this.$element + .addClass('collapsing') + .removeClass('collapse') + .removeClass('in') + + this.transitioning = 1 + + var complete = function () { + this.transitioning = 0 + this.$element + .trigger('hidden.bs.collapse') + .removeClass('collapsing') + .addClass('collapse') + } + + if (!$.support.transition) return complete.call(this) + + this.$element + [dimension](0) + .one($.support.transition.end, $.proxy(complete, this)) + .emulateTransitionEnd(350) + } + + Collapse.prototype.toggle = function () { + this[this.$element.hasClass('in') ? 'hide' : 'show']() + } + + + // COLLAPSE PLUGIN DEFINITION + // ========================== + + var old = $.fn.collapse + + $.fn.collapse = function (option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.collapse') + var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option) + + if (!data && options.toggle && option == 'show') option = !option + if (!data) $this.data('bs.collapse', (data = new Collapse(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.collapse.Constructor = Collapse + + + // COLLAPSE NO CONFLICT + // ==================== + + $.fn.collapse.noConflict = function () { + $.fn.collapse = old + return this + } + + + // COLLAPSE DATA-API + // ================= + + $(document).on('click.bs.collapse.data-api', '[data-toggle=collapse]', function (e) { + var $this = $(this), href + var target = $this.attr('data-target') + || e.preventDefault() + || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 + var $target = $(target) + var data = $target.data('bs.collapse') + var option = data ? 'toggle' : $this.data() + var parent = $this.attr('data-parent') + var $parent = parent && $(parent) + + if (!data || !data.transitioning) { + if ($parent) $parent.find('[data-toggle=collapse][data-parent="' + parent + '"]').not($this).addClass('collapsed') + $this[$target.hasClass('in') ? 'addClass' : 'removeClass']('collapsed') + } + + $target.collapse(option) + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: dropdown.js v3.1.1 + * http://getbootstrap.com/javascript/#dropdowns + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // DROPDOWN CLASS DEFINITION + // ========================= + + var backdrop = '.dropdown-backdrop' + var toggle = '[data-toggle=dropdown]' + var Dropdown = function (element) { + $(element).on('click.bs.dropdown', this.toggle) + } + + Dropdown.prototype.toggle = function (e) { + var $this = $(this) + + if ($this.is('.disabled, :disabled')) return + + var $parent = getParent($this) + var isActive = $parent.hasClass('open') + + clearMenus() + + if (!isActive) { + if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { + // if mobile we use a backdrop because click events don't delegate + $('<div class="dropdown-backdrop"/>').insertAfter($(this)).on('click', clearMenus) + } + + var relatedTarget = { relatedTarget: this } + $parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget)) + + if (e.isDefaultPrevented()) return + + $parent + .toggleClass('open') + .trigger('shown.bs.dropdown', relatedTarget) + + $this.focus() + } + + return false + } + + Dropdown.prototype.keydown = function (e) { + if (!/(38|40|27)/.test(e.keyCode)) return + + var $this = $(this) + + e.preventDefault() + e.stopPropagation() + + if ($this.is('.disabled, :disabled')) return + + var $parent = getParent($this) + var isActive = $parent.hasClass('open') + + if (!isActive || (isActive && e.keyCode == 27)) { + if (e.which == 27) $parent.find(toggle).focus() + return $this.click() + } + + var desc = ' li:not(.divider):visible a' + var $items = $parent.find('[role=menu]' + desc + ', [role=listbox]' + desc) + + if (!$items.length) return + + var index = $items.index($items.filter(':focus')) + + if (e.keyCode == 38 && index > 0) index-- // up + if (e.keyCode == 40 && index < $items.length - 1) index++ // down + if (!~index) index = 0 + + $items.eq(index).focus() + } + + function clearMenus(e) { + $(backdrop).remove() + $(toggle).each(function () { + var $parent = getParent($(this)) + var relatedTarget = { relatedTarget: this } + if (!$parent.hasClass('open')) return + $parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget)) + if (e.isDefaultPrevented()) return + $parent.removeClass('open').trigger('hidden.bs.dropdown', relatedTarget) + }) + } + + function getParent($this) { + var selector = $this.attr('data-target') + + if (!selector) { + selector = $this.attr('href') + selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + var $parent = selector && $(selector) + + return $parent && $parent.length ? $parent : $this.parent() + } + + + // DROPDOWN PLUGIN DEFINITION + // ========================== + + var old = $.fn.dropdown + + $.fn.dropdown = function (option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.dropdown') + + if (!data) $this.data('bs.dropdown', (data = new Dropdown(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + $.fn.dropdown.Constructor = Dropdown + + + // DROPDOWN NO CONFLICT + // ==================== + + $.fn.dropdown.noConflict = function () { + $.fn.dropdown = old + return this + } + + + // APPLY TO STANDARD DROPDOWN ELEMENTS + // =================================== + + $(document) + .on('click.bs.dropdown.data-api', clearMenus) + .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) + .on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle) + .on('keydown.bs.dropdown.data-api', toggle + ', [role=menu], [role=listbox]', Dropdown.prototype.keydown) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: modal.js v3.1.1 + * http://getbootstrap.com/javascript/#modals + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // MODAL CLASS DEFINITION + // ====================== + + var Modal = function (element, options) { + this.options = options + this.$element = $(element) + this.$backdrop = + this.isShown = null + + if (this.options.remote) { + this.$element + .find('.modal-content') + .load(this.options.remote, $.proxy(function () { + this.$element.trigger('loaded.bs.modal') + }, this)) + } + } + + Modal.DEFAULTS = { + backdrop: true, + keyboard: true, + show: true + } + + Modal.prototype.toggle = function (_relatedTarget) { + return this[!this.isShown ? 'show' : 'hide'](_relatedTarget) + } + + Modal.prototype.show = function (_relatedTarget) { + var that = this + var e = $.Event('show.bs.modal', { relatedTarget: _relatedTarget }) + + this.$element.trigger(e) + + if (this.isShown || e.isDefaultPrevented()) return + + this.isShown = true + + this.escape() + + this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this)) + + this.backdrop(function () { + var transition = $.support.transition && that.$element.hasClass('fade') + + if (!that.$element.parent().length) { + that.$element.appendTo(document.body) // don't move modals dom position + } + + that.$element + .show() + .scrollTop(0) + + if (transition) { + that.$element[0].offsetWidth // force reflow + } + + that.$element + .addClass('in') + .attr('aria-hidden', false) + + that.enforceFocus() + + var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget }) + + transition ? + that.$element.find('.modal-dialog') // wait for modal to slide in + .one($.support.transition.end, function () { + that.$element.focus().trigger(e) + }) + .emulateTransitionEnd(300) : + that.$element.focus().trigger(e) + }) + } + + Modal.prototype.hide = function (e) { + if (e) e.preventDefault() + + e = $.Event('hide.bs.modal') + + this.$element.trigger(e) + + if (!this.isShown || e.isDefaultPrevented()) return + + this.isShown = false + + this.escape() + + $(document).off('focusin.bs.modal') + + this.$element + .removeClass('in') + .attr('aria-hidden', true) + .off('click.dismiss.bs.modal') + + $.support.transition && this.$element.hasClass('fade') ? + this.$element + .one($.support.transition.end, $.proxy(this.hideModal, this)) + .emulateTransitionEnd(300) : + this.hideModal() + } + + Modal.prototype.enforceFocus = function () { + $(document) + .off('focusin.bs.modal') // guard against infinite focus loop + .on('focusin.bs.modal', $.proxy(function (e) { + if (this.$element[0] !== e.target && !this.$element.has(e.target).length) { + this.$element.focus() + } + }, this)) + } + + Modal.prototype.escape = function () { + if (this.isShown && this.options.keyboard) { + this.$element.on('keyup.dismiss.bs.modal', $.proxy(function (e) { + e.which == 27 && this.hide() + }, this)) + } else if (!this.isShown) { + this.$element.off('keyup.dismiss.bs.modal') + } + } + + Modal.prototype.hideModal = function () { + var that = this + this.$element.hide() + this.backdrop(function () { + that.removeBackdrop() + that.$element.trigger('hidden.bs.modal') + }) + } + + Modal.prototype.removeBackdrop = function () { + this.$backdrop && this.$backdrop.remove() + this.$backdrop = null + } + + Modal.prototype.backdrop = function (callback) { + var animate = this.$element.hasClass('fade') ? 'fade' : '' + + if (this.isShown && this.options.backdrop) { + var doAnimate = $.support.transition && animate + + this.$backdrop = $('<div class="modal-backdrop ' + animate + '" />') + .appendTo(document.body) + + this.$element.on('click.dismiss.bs.modal', $.proxy(function (e) { + if (e.target !== e.currentTarget) return + this.options.backdrop == 'static' + ? this.$element[0].focus.call(this.$element[0]) + : this.hide.call(this) + }, this)) + + if (doAnimate) this.$backdrop[0].offsetWidth // force reflow + + this.$backdrop.addClass('in') + + if (!callback) return + + doAnimate ? + this.$backdrop + .one($.support.transition.end, callback) + .emulateTransitionEnd(150) : + callback() + + } else if (!this.isShown && this.$backdrop) { + this.$backdrop.removeClass('in') + + $.support.transition && this.$element.hasClass('fade') ? + this.$backdrop + .one($.support.transition.end, callback) + .emulateTransitionEnd(150) : + callback() + + } else if (callback) { + callback() + } + } + + + // MODAL PLUGIN DEFINITION + // ======================= + + var old = $.fn.modal + + $.fn.modal = function (option, _relatedTarget) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.modal') + var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option) + + if (!data) $this.data('bs.modal', (data = new Modal(this, options))) + if (typeof option == 'string') data[option](_relatedTarget) + else if (options.show) data.show(_relatedTarget) + }) + } + + $.fn.modal.Constructor = Modal + + + // MODAL NO CONFLICT + // ================= + + $.fn.modal.noConflict = function () { + $.fn.modal = old + return this + } + + + // MODAL DATA-API + // ============== + + $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) { + var $this = $(this) + var href = $this.attr('href') + var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) //strip for ie7 + var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data()) + + if ($this.is('a')) e.preventDefault() + + $target + .modal(option, this) + .one('hide', function () { + $this.is(':visible') && $this.focus() + }) + }) + + $(document) + .on('show.bs.modal', '.modal', function () { $(document.body).addClass('modal-open') }) + .on('hidden.bs.modal', '.modal', function () { $(document.body).removeClass('modal-open') }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: tooltip.js v3.1.1 + * http://getbootstrap.com/javascript/#tooltip + * Inspired by the original jQuery.tipsy by Jason Frame + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // TOOLTIP PUBLIC CLASS DEFINITION + // =============================== + + var Tooltip = function (element, options) { + this.type = + this.options = + this.enabled = + this.timeout = + this.hoverState = + this.$element = null + + this.init('tooltip', element, options) + } + + Tooltip.DEFAULTS = { + animation: true, + placement: 'top', + selector: false, + template: '<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>', + trigger: 'hover focus', + title: '', + delay: 0, + html: false, + container: false + } + + Tooltip.prototype.init = function (type, element, options) { + this.enabled = true + this.type = type + this.$element = $(element) + this.options = this.getOptions(options) + + var triggers = this.options.trigger.split(' ') + + for (var i = triggers.length; i--;) { + var trigger = triggers[i] + + if (trigger == 'click') { + this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this)) + } else if (trigger != 'manual') { + var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin' + var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout' + + this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this)) + this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this)) + } + } + + this.options.selector ? + (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : + this.fixTitle() + } + + Tooltip.prototype.getDefaults = function () { + return Tooltip.DEFAULTS + } + + Tooltip.prototype.getOptions = function (options) { + options = $.extend({}, this.getDefaults(), this.$element.data(), options) + + if (options.delay && typeof options.delay == 'number') { + options.delay = { + show: options.delay, + hide: options.delay + } + } + + return options + } + + Tooltip.prototype.getDelegateOptions = function () { + var options = {} + var defaults = this.getDefaults() + + this._options && $.each(this._options, function (key, value) { + if (defaults[key] != value) options[key] = value + }) + + return options + } + + Tooltip.prototype.enter = function (obj) { + var self = obj instanceof this.constructor ? + obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type) + + clearTimeout(self.timeout) + + self.hoverState = 'in' + + if (!self.options.delay || !self.options.delay.show) return self.show() + + self.timeout = setTimeout(function () { + if (self.hoverState == 'in') self.show() + }, self.options.delay.show) + } + + Tooltip.prototype.leave = function (obj) { + var self = obj instanceof this.constructor ? + obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type) + + clearTimeout(self.timeout) + + self.hoverState = 'out' + + if (!self.options.delay || !self.options.delay.hide) return self.hide() + + self.timeout = setTimeout(function () { + if (self.hoverState == 'out') self.hide() + }, self.options.delay.hide) + } + + Tooltip.prototype.show = function () { + var e = $.Event('show.bs.' + this.type) + + if (this.hasContent() && this.enabled) { + this.$element.trigger(e) + + if (e.isDefaultPrevented()) return + var that = this; + + var $tip = this.tip() + + this.setContent() + + if (this.options.animation) $tip.addClass('fade') + + var placement = typeof this.options.placement == 'function' ? + this.options.placement.call(this, $tip[0], this.$element[0]) : + this.options.placement + + var autoToken = /\s?auto?\s?/i + var autoPlace = autoToken.test(placement) + if (autoPlace) placement = placement.replace(autoToken, '') || 'top' + + $tip + .detach() + .css({ top: 0, left: 0, display: 'block' }) + .addClass(placement) + + this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element) + + var pos = this.getPosition() + var actualWidth = $tip[0].offsetWidth + var actualHeight = $tip[0].offsetHeight + + if (autoPlace) { + var $parent = this.$element.parent() + + var orgPlacement = placement + var docScroll = document.documentElement.scrollTop || document.body.scrollTop + var parentWidth = this.options.container == 'body' ? window.innerWidth : $parent.outerWidth() + var parentHeight = this.options.container == 'body' ? window.innerHeight : $parent.outerHeight() + var parentLeft = this.options.container == 'body' ? 0 : $parent.offset().left + + placement = placement == 'bottom' && pos.top + pos.height + actualHeight - docScroll > parentHeight ? 'top' : + placement == 'top' && pos.top - docScroll - actualHeight < 0 ? 'bottom' : + placement == 'right' && pos.right + actualWidth > parentWidth ? 'left' : + placement == 'left' && pos.left - actualWidth < parentLeft ? 'right' : + placement + + $tip + .removeClass(orgPlacement) + .addClass(placement) + } + + var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight) + + this.applyPlacement(calculatedOffset, placement) + this.hoverState = null + + var complete = function() { + that.$element.trigger('shown.bs.' + that.type) + } + + $.support.transition && this.$tip.hasClass('fade') ? + $tip + .one($.support.transition.end, complete) + .emulateTransitionEnd(150) : + complete() + } + } + + Tooltip.prototype.applyPlacement = function (offset, placement) { + var replace + var $tip = this.tip() + var width = $tip[0].offsetWidth + var height = $tip[0].offsetHeight + + // manually read margins because getBoundingClientRect includes difference + var marginTop = parseInt($tip.css('margin-top'), 10) + var marginLeft = parseInt($tip.css('margin-left'), 10) + + // we must check for NaN for ie 8/9 + if (isNaN(marginTop)) marginTop = 0 + if (isNaN(marginLeft)) marginLeft = 0 + + offset.top = offset.top + marginTop + offset.left = offset.left + marginLeft + + // $.fn.offset doesn't round pixel values + // so we use setOffset directly with our own function B-0 + $.offset.setOffset($tip[0], $.extend({ + using: function (props) { + $tip.css({ + top: Math.round(props.top), + left: Math.round(props.left) + }) + } + }, offset), 0) + + $tip.addClass('in') + + // check to see if placing tip in new offset caused the tip to resize itself + var actualWidth = $tip[0].offsetWidth + var actualHeight = $tip[0].offsetHeight + + if (placement == 'top' && actualHeight != height) { + replace = true + offset.top = offset.top + height - actualHeight + } + + if (/bottom|top/.test(placement)) { + var delta = 0 + + if (offset.left < 0) { + delta = offset.left * -2 + offset.left = 0 + + $tip.offset(offset) + + actualWidth = $tip[0].offsetWidth + actualHeight = $tip[0].offsetHeight + } + + this.replaceArrow(delta - width + actualWidth, actualWidth, 'left') + } else { + this.replaceArrow(actualHeight - height, actualHeight, 'top') + } + + if (replace) $tip.offset(offset) + } + + Tooltip.prototype.replaceArrow = function (delta, dimension, position) { + this.arrow().css(position, delta ? (50 * (1 - delta / dimension) + '%') : '') + } + + Tooltip.prototype.setContent = function () { + var $tip = this.tip() + var title = this.getTitle() + + $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) + $tip.removeClass('fade in top bottom left right') + } + + Tooltip.prototype.hide = function () { + var that = this + var $tip = this.tip() + var e = $.Event('hide.bs.' + this.type) + + function complete() { + if (that.hoverState != 'in') $tip.detach() + that.$element.trigger('hidden.bs.' + that.type) + } + + this.$element.trigger(e) + + if (e.isDefaultPrevented()) return + + $tip.removeClass('in') + + $.support.transition && this.$tip.hasClass('fade') ? + $tip + .one($.support.transition.end, complete) + .emulateTransitionEnd(150) : + complete() + + this.hoverState = null + + return this + } + + Tooltip.prototype.fixTitle = function () { + var $e = this.$element + if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') { + $e.attr('data-original-title', $e.attr('title') || '').attr('title', '') + } + } + + Tooltip.prototype.hasContent = function () { + return this.getTitle() + } + + Tooltip.prototype.getPosition = function () { + var el = this.$element[0] + return $.extend({}, (typeof el.getBoundingClientRect == 'function') ? el.getBoundingClientRect() : { + width: el.offsetWidth, + height: el.offsetHeight + }, this.$element.offset()) + } + + Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) { + return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } : + placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } : + placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } : + /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width } + } + + Tooltip.prototype.getTitle = function () { + var title + var $e = this.$element + var o = this.options + + title = $e.attr('data-original-title') + || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) + + return title + } + + Tooltip.prototype.tip = function () { + return this.$tip = this.$tip || $(this.options.template) + } + + Tooltip.prototype.arrow = function () { + return this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow') + } + + Tooltip.prototype.validate = function () { + if (!this.$element[0].parentNode) { + this.hide() + this.$element = null + this.options = null + } + } + + Tooltip.prototype.enable = function () { + this.enabled = true + } + + Tooltip.prototype.disable = function () { + this.enabled = false + } + + Tooltip.prototype.toggleEnabled = function () { + this.enabled = !this.enabled + } + + Tooltip.prototype.toggle = function (e) { + var self = e ? $(e.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type) : this + self.tip().hasClass('in') ? self.leave(self) : self.enter(self) + } + + Tooltip.prototype.destroy = function () { + clearTimeout(this.timeout) + this.hide().$element.off('.' + this.type).removeData('bs.' + this.type) + } + + + // TOOLTIP PLUGIN DEFINITION + // ========================= + + var old = $.fn.tooltip + + $.fn.tooltip = function (option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.tooltip') + var options = typeof option == 'object' && option + + if (!data && option == 'destroy') return + if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.tooltip.Constructor = Tooltip + + + // TOOLTIP NO CONFLICT + // =================== + + $.fn.tooltip.noConflict = function () { + $.fn.tooltip = old + return this + } + +}(jQuery); + +/* ======================================================================== + * Bootstrap: popover.js v3.1.1 + * http://getbootstrap.com/javascript/#popovers + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // POPOVER PUBLIC CLASS DEFINITION + // =============================== + + var Popover = function (element, options) { + this.init('popover', element, options) + } + + if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js') + + Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, { + placement: 'right', + trigger: 'click', + content: '', + template: '<div class="popover"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>' + }) + + + // NOTE: POPOVER EXTENDS tooltip.js + // ================================ + + Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype) + + Popover.prototype.constructor = Popover + + Popover.prototype.getDefaults = function () { + return Popover.DEFAULTS + } + + Popover.prototype.setContent = function () { + var $tip = this.tip() + var title = this.getTitle() + var content = this.getContent() + + $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title) + $tip.find('.popover-content')[ // we use append for html objects to maintain js events + this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text' + ](content) + + $tip.removeClass('fade top bottom left right in') + + // IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do + // this manually by checking the contents. + if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide() + } + + Popover.prototype.hasContent = function () { + return this.getTitle() || this.getContent() + } + + Popover.prototype.getContent = function () { + var $e = this.$element + var o = this.options + + return $e.attr('data-content') + || (typeof o.content == 'function' ? + o.content.call($e[0]) : + o.content) + } + + Popover.prototype.arrow = function () { + return this.$arrow = this.$arrow || this.tip().find('.arrow') + } + + Popover.prototype.tip = function () { + if (!this.$tip) this.$tip = $(this.options.template) + return this.$tip + } + + + // POPOVER PLUGIN DEFINITION + // ========================= + + var old = $.fn.popover + + $.fn.popover = function (option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.popover') + var options = typeof option == 'object' && option + + if (!data && option == 'destroy') return + if (!data) $this.data('bs.popover', (data = new Popover(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.popover.Constructor = Popover + + + // POPOVER NO CONFLICT + // =================== + + $.fn.popover.noConflict = function () { + $.fn.popover = old + return this + } + +}(jQuery); + +/* ======================================================================== + * Bootstrap: scrollspy.js v3.1.1 + * http://getbootstrap.com/javascript/#scrollspy + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // SCROLLSPY CLASS DEFINITION + // ========================== + + function ScrollSpy(element, options) { + var href + var process = $.proxy(this.process, this) + + this.$element = $(element).is('body') ? $(window) : $(element) + this.$body = $('body') + this.$scrollElement = this.$element.on('scroll.bs.scroll-spy.data-api', process) + this.options = $.extend({}, ScrollSpy.DEFAULTS, options) + this.selector = (this.options.target + || ((href = $(element).attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 + || '') + ' .nav li > a' + this.offsets = $([]) + this.targets = $([]) + this.activeTarget = null + + this.refresh() + this.process() + } + + ScrollSpy.DEFAULTS = { + offset: 10 + } + + ScrollSpy.prototype.refresh = function () { + var offsetMethod = this.$element[0] == window ? 'offset' : 'position' + + this.offsets = $([]) + this.targets = $([]) + + var self = this + var $targets = this.$body + .find(this.selector) + .map(function () { + var $el = $(this) + var href = $el.data('target') || $el.attr('href') + var $href = /^#./.test(href) && $(href) + + return ($href + && $href.length + && $href.is(':visible') + && [[ $href[offsetMethod]().top + (!$.isWindow(self.$scrollElement.get(0)) && self.$scrollElement.scrollTop()), href ]]) || null + }) + .sort(function (a, b) { return a[0] - b[0] }) + .each(function () { + self.offsets.push(this[0]) + self.targets.push(this[1]) + }) + } + + ScrollSpy.prototype.process = function () { + var scrollTop = this.$scrollElement.scrollTop() + this.options.offset + var scrollHeight = this.$scrollElement[0].scrollHeight || this.$body[0].scrollHeight + var maxScroll = scrollHeight - this.$scrollElement.height() + var offsets = this.offsets + var targets = this.targets + var activeTarget = this.activeTarget + var i + + if (scrollTop >= maxScroll) { + return activeTarget != (i = targets.last()[0]) && this.activate(i) + } + + if (activeTarget && scrollTop <= offsets[0]) { + return activeTarget != (i = targets[0]) && this.activate(i) + } + + for (i = offsets.length; i--;) { + activeTarget != targets[i] + && scrollTop >= offsets[i] + && (!offsets[i + 1] || scrollTop <= offsets[i + 1]) + && this.activate( targets[i] ) + } + } + + ScrollSpy.prototype.activate = function (target) { + this.activeTarget = target + + $(this.selector) + .parentsUntil(this.options.target, '.active') + .removeClass('active') + + var selector = this.selector + + '[data-target="' + target + '"],' + + this.selector + '[href="' + target + '"]' + + var active = $(selector) + .parents('li') + .addClass('active') + + if (active.parent('.dropdown-menu').length) { + active = active + .closest('li.dropdown') + .addClass('active') + } + + active.trigger('activate.bs.scrollspy') + } + + + // SCROLLSPY PLUGIN DEFINITION + // =========================== + + var old = $.fn.scrollspy + + $.fn.scrollspy = function (option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.scrollspy') + var options = typeof option == 'object' && option + + if (!data) $this.data('bs.scrollspy', (data = new ScrollSpy(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.scrollspy.Constructor = ScrollSpy + + + // SCROLLSPY NO CONFLICT + // ===================== + + $.fn.scrollspy.noConflict = function () { + $.fn.scrollspy = old + return this + } + + + // SCROLLSPY DATA-API + // ================== + + $(window).on('load', function () { + $('[data-spy="scroll"]').each(function () { + var $spy = $(this) + $spy.scrollspy($spy.data()) + }) + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: tab.js v3.1.1 + * http://getbootstrap.com/javascript/#tabs + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // TAB CLASS DEFINITION + // ==================== + + var Tab = function (element) { + this.element = $(element) + } + + Tab.prototype.show = function () { + var $this = this.element + var $ul = $this.closest('ul:not(.dropdown-menu)') + var selector = $this.data('target') + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + if ($this.parent('li').hasClass('active')) return + + var previous = $ul.find('.active:last a')[0] + var e = $.Event('show.bs.tab', { + relatedTarget: previous + }) + + $this.trigger(e) + + if (e.isDefaultPrevented()) return + + var $target = $(selector) + + this.activate($this.parent('li'), $ul) + this.activate($target, $target.parent(), function () { + $this.trigger({ + type: 'shown.bs.tab', + relatedTarget: previous + }) + }) + } + + Tab.prototype.activate = function (element, container, callback) { + var $active = container.find('> .active') + var transition = callback + && $.support.transition + && $active.hasClass('fade') + + function next() { + $active + .removeClass('active') + .find('> .dropdown-menu > .active') + .removeClass('active') + + element.addClass('active') + + if (transition) { + element[0].offsetWidth // reflow for transition + element.addClass('in') + } else { + element.removeClass('fade') + } + + if (element.parent('.dropdown-menu')) { + element.closest('li.dropdown').addClass('active') + } + + callback && callback() + } + + transition ? + $active + .one($.support.transition.end, next) + .emulateTransitionEnd(150) : + next() + + $active.removeClass('in') + } + + + // TAB PLUGIN DEFINITION + // ===================== + + var old = $.fn.tab + + $.fn.tab = function ( option ) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.tab') + + if (!data) $this.data('bs.tab', (data = new Tab(this))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.tab.Constructor = Tab + + + // TAB NO CONFLICT + // =============== + + $.fn.tab.noConflict = function () { + $.fn.tab = old + return this + } + + + // TAB DATA-API + // ============ + + $(document).on('click.bs.tab.data-api', '[data-toggle="tab"], [data-toggle="pill"]', function (e) { + e.preventDefault() + $(this).tab('show') + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: affix.js v3.1.1 + * http://getbootstrap.com/javascript/#affix + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // AFFIX CLASS DEFINITION + // ====================== + + var Affix = function (element, options) { + this.options = $.extend({}, Affix.DEFAULTS, options) + this.$window = $(window) + .on('scroll.bs.affix.data-api', $.proxy(this.checkPosition, this)) + .on('click.bs.affix.data-api', $.proxy(this.checkPositionWithEventLoop, this)) + + this.$element = $(element) + this.affixed = + this.unpin = + this.pinnedOffset = null + + this.checkPosition() + } + + Affix.RESET = 'affix affix-top affix-bottom' + + Affix.DEFAULTS = { + offset: 0 + } + + Affix.prototype.getPinnedOffset = function () { + if (this.pinnedOffset) return this.pinnedOffset + this.$element.removeClass(Affix.RESET).addClass('affix') + var scrollTop = this.$window.scrollTop() + var position = this.$element.offset() + return (this.pinnedOffset = position.top - scrollTop) + } + + Affix.prototype.checkPositionWithEventLoop = function () { + setTimeout($.proxy(this.checkPosition, this), 1) + } + + Affix.prototype.checkPosition = function () { + if (!this.$element.is(':visible')) return + + var scrollHeight = $(document).height() + var scrollTop = this.$window.scrollTop() + var position = this.$element.offset() + var offset = this.options.offset + var offsetTop = offset.top + var offsetBottom = offset.bottom + + if (this.affixed == 'top') position.top += scrollTop + + if (typeof offset != 'object') offsetBottom = offsetTop = offset + if (typeof offsetTop == 'function') offsetTop = offset.top(this.$element) + if (typeof offsetBottom == 'function') offsetBottom = offset.bottom(this.$element) + + var affix = this.unpin != null && (scrollTop + this.unpin <= position.top) ? false : + offsetBottom != null && (position.top + this.$element.height() >= scrollHeight - offsetBottom) ? 'bottom' : + offsetTop != null && (scrollTop <= offsetTop) ? 'top' : false + + if (this.affixed === affix) return + if (this.unpin) this.$element.css('top', '') + + var affixType = 'affix' + (affix ? '-' + affix : '') + var e = $.Event(affixType + '.bs.affix') + + this.$element.trigger(e) + + if (e.isDefaultPrevented()) return + + this.affixed = affix + this.unpin = affix == 'bottom' ? this.getPinnedOffset() : null + + this.$element + .removeClass(Affix.RESET) + .addClass(affixType) + .trigger($.Event(affixType.replace('affix', 'affixed'))) + + if (affix == 'bottom') { + this.$element.offset({ top: scrollHeight - offsetBottom - this.$element.height() }) + } + } + + + // AFFIX PLUGIN DEFINITION + // ======================= + + var old = $.fn.affix + + $.fn.affix = function (option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.affix') + var options = typeof option == 'object' && option + + if (!data) $this.data('bs.affix', (data = new Affix(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.affix.Constructor = Affix + + + // AFFIX NO CONFLICT + // ================= + + $.fn.affix.noConflict = function () { + $.fn.affix = old + return this + } + + + // AFFIX DATA-API + // ============== + + $(window).on('load', function () { + $('[data-spy="affix"]').each(function () { + var $spy = $(this) + var data = $spy.data() + + data.offset = data.offset || {} + + if (data.offsetBottom) data.offset.bottom = data.offsetBottom + if (data.offsetTop) data.offset.top = data.offsetTop + + $spy.affix(data) + }) + }) + +}(jQuery); diff --git a/snf-admin-app/synnefo_admin/admin/static/js/c3.js b/snf-admin-app/synnefo_admin/admin/static/js/c3.js new file mode 100644 index 0000000000000000000000000000000000000000..b794aaf08428f0acad678950c6a22120aed028ab --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/js/c3.js @@ -0,0 +1,6227 @@ +(function (window) { + 'use strict'; + + /*global define, module, exports, require */ + + var c3 = { version: "0.3.0" }; + + var c3_chart_fn, c3_chart_internal_fn; + + function Chart(config) { + var $$ = this.internal = new ChartInternal(this); + $$.loadConfig(config); + $$.init(); + + // bind "this" to nested API + (function bindThis(fn, target, argThis) { + for (var key in fn) { + target[key] = fn[key].bind(argThis); + if (Object.keys(fn[key]).length > 0) { + bindThis(fn[key], target[key], argThis); + } + } + })(c3_chart_fn, this, this); + } + + function ChartInternal(api) { + var $$ = this; + $$.d3 = window.d3 ? window.d3 : typeof require !== 'undefined' ? require("d3") : undefined; + $$.api = api; + $$.config = $$.getDefaultConfig(); + $$.data = {}; + $$.cache = {}; + $$.axes = {}; + } + + c3.generate = function (config) { + return new Chart(config); + }; + + c3.chart = { + fn: Chart.prototype, + internal: { + fn: ChartInternal.prototype + } + }; + c3_chart_fn = c3.chart.fn; + c3_chart_internal_fn = c3.chart.internal.fn; + + + c3_chart_internal_fn.init = function () { + var $$ = this, config = $$.config; + + $$.initParams(); + + if (config.data_url) { + $$.convertUrlToData(config.data_url, config.data_mimeType, config.data_keys, $$.initWithData); + } + else if (config.data_json) { + $$.initWithData($$.convertJsonToData(config.data_json, config.data_keys)); + } + else if (config.data_rows) { + $$.initWithData($$.convertRowsToData(config.data_rows)); + } + else if (config.data_columns) { + $$.initWithData($$.convertColumnsToData(config.data_columns)); + } + else { + throw Error('url or json or rows or columns is required.'); + } + }; + + c3_chart_internal_fn.initParams = function () { + var $$ = this, d3 = $$.d3, config = $$.config; + + // MEMO: clipId needs to be unique because it conflicts when multiple charts exist + $$.clipId = "c3-" + (+new Date()) + '-clip', + $$.clipIdForXAxis = $$.clipId + '-xaxis', + $$.clipIdForYAxis = $$.clipId + '-yaxis', + $$.clipPath = $$.getClipPath($$.clipId), + $$.clipPathForXAxis = $$.getClipPath($$.clipIdForXAxis), + $$.clipPathForYAxis = $$.getClipPath($$.clipIdForYAxis); + + $$.dragStart = null; + $$.dragging = false; + $$.cancelClick = false; + $$.mouseover = false; + $$.transiting = false; + + $$.color = $$.generateColor(); + $$.levelColor = $$.generateLevelColor(); + + $$.dataTimeFormat = config.data_xLocaltime ? d3.time.format : d3.time.format.utc; + $$.axisTimeFormat = config.axis_x_localtime ? d3.time.format : d3.time.format.utc; + $$.defaultAxisTimeFormat = $$.axisTimeFormat.multi([ + [".%L", function (d) { return d.getMilliseconds(); }], + [":%S", function (d) { return d.getSeconds(); }], + ["%I:%M", function (d) { return d.getMinutes(); }], + ["%I %p", function (d) { return d.getHours(); }], + ["%-m/%-d", function (d) { return d.getDay() && d.getDate() !== 1; }], + ["%-m/%-d", function (d) { return d.getDate() !== 1; }], + ["%-m/%-d", function (d) { return d.getMonth(); }], + ["%Y/%-m/%-d", function () { return true; }] + ]); + + $$.hiddenTargetIds = []; + $$.hiddenLegendIds = []; + + $$.xOrient = config.axis_rotated ? "left" : "bottom"; + $$.yOrient = config.axis_rotated ? "bottom" : "left"; + $$.y2Orient = config.axis_rotated ? "top" : "right"; + $$.subXOrient = config.axis_rotated ? "left" : "bottom"; + + $$.isLegendRight = config.legend_position === 'right'; + $$.isLegendInset = config.legend_position === 'inset'; + $$.isLegendTop = config.legend_inset_anchor === 'top-left' || config.legend_inset_anchor === 'top-right'; + $$.isLegendLeft = config.legend_inset_anchor === 'top-left' || config.legend_inset_anchor === 'bottom-left'; + $$.legendStep = 0; + $$.legendItemWidth = 0; + $$.legendItemHeight = 0; + $$.legendOpacityForHidden = 0.15; + + $$.currentMaxTickWidth = 0; + + $$.rotated_padding_left = 30; + $$.rotated_padding_right = config.axis_rotated && !config.axis_x_show ? 0 : 30; + $$.rotated_padding_top = 5; + + $$.withoutFadeIn = {}; + + $$.axes.subx = d3.selectAll([]); // needs when excluding subchart.js + }; + + c3_chart_internal_fn.initWithData = function (data) { + var $$ = this, d3 = $$.d3, config = $$.config; + var main, binding = true; + + if ($$.initPie) { $$.initPie(); } + if ($$.initBrush) { $$.initBrush(); } + if ($$.initZoom) { $$.initZoom(); } + + $$.selectChart = d3.select(config.bindto); + if ($$.selectChart.empty()) { + $$.selectChart = d3.select(document.createElement('div')).style('opacity', 0); + $$.observeInserted($$.selectChart); + binding = false; + } + $$.selectChart.html("").classed("c3", true); + + // Init data as targets + $$.data.xs = {}; + $$.data.targets = $$.convertDataToTargets(data); + + if (config.data_filter) { + $$.data.targets = $$.data.targets.filter(config.data_filter); + } + + // Set targets to hide if needed + if (config.data_hide) { + $$.addHiddenTargetIds(config.data_hide === true ? $$.mapToIds($$.data.targets) : config.data_hide); + } + + // when gauge, hide legend // TODO: fix + if ($$.hasType('gauge')) { + config.legend_show = false; + } + + // Init sizes and scales + $$.updateSizes(); + $$.updateScales(); + + // Set domains for each scale + $$.x.domain(d3.extent($$.getXDomain($$.data.targets))); + $$.y.domain($$.getYDomain($$.data.targets, 'y')); + $$.y2.domain($$.getYDomain($$.data.targets, 'y2')); + $$.subX.domain($$.x.domain()); + $$.subY.domain($$.y.domain()); + $$.subY2.domain($$.y2.domain()); + + // Save original x domain for zoom update + $$.orgXDomain = $$.x.domain(); + + // Set initialized scales to brush and zoom + if ($$.brush) { $$.brush.scale($$.subX); } + if (config.zoom_enabled) { $$.zoom.scale($$.x); } + + /*-- Basic Elements --*/ + + // Define svgs + $$.svg = $$.selectChart.append("svg") + .style("overflow", "hidden") + .on('mouseenter', function () { return config.onmouseover.call($$); }) + .on('mouseleave', function () { return config.onmouseout.call($$); }); + + // Define defs + $$.defs = $$.svg.append("defs"); + $$.defs.append("clipPath").attr("id", $$.clipId).append("rect"); + $$.defs.append("clipPath").attr("id", $$.clipIdForXAxis).append("rect"); + $$.defs.append("clipPath").attr("id", $$.clipIdForYAxis).append("rect"); + $$.updateSvgSize(); + + // Define regions + main = $$.main = $$.svg.append("g").attr("transform", $$.getTranslate('main')); + + if ($$.initSubchart) { $$.initSubchart(); } + if ($$.initTooltip) { $$.initTooltip(); } + if ($$.initLegend) { $$.initLegend(); } + + /*-- Main Region --*/ + + // text when empty + main.append("text") + .attr("class", CLASS.text + ' ' + CLASS.empty) + .attr("text-anchor", "middle") // horizontal centering of text at x position in all browsers. + .attr("dominant-baseline", "middle"); // vertical centering of text at y position in all browsers, except IE. + + // Regions + $$.initRegion(); + + // Grids + $$.initGrid(); + + // Define g for chart area + main.append('g') + .attr("clip-path", $$.clipPath) + .attr('class', CLASS.chart); + + // Grid lines + if (config.grid_lines_front) { $$.initGridLines(); } + + // Cover whole with rects for events + $$.initEventRect(); + + // Define g for bar chart area + if ($$.initBar) { $$.initBar(); } + + // Define g for line chart area + if ($$.initLine) { $$.initLine(); } + + // Define g for arc chart area + if ($$.initArc) { $$.initArc(); } + if ($$.initGauge) { $$.initGauge(); } + + // Define g for text area + if ($$.initText) { $$.initText(); } + + // if zoom privileged, insert rect to forefront + // TODO: is this needed? + main.insert('rect', config.zoom_privileged ? null : 'g.' + CLASS.regions) + .attr('class', CLASS.zoomRect) + .attr('width', $$.width) + .attr('height', $$.height) + .style('opacity', 0) + .on("dblclick.zoom", null); + + // Set default extent if defined + if (config.axis_x_default) { + $$.brush.extent(isFunction(config.axis_x_default) ? config.axis_x_default($$.getXDomain()) : config.axis_x_default); + } + + // Add Axis + $$.initAxis(); + + // Set targets + $$.updateTargets($$.data.targets); + + // Draw with targets + if (binding) { + $$.updateDimension(); + $$.redraw({ + withTransform: true, + withUpdateXDomain: true, + withUpdateOrgXDomain: true, + withTransitionForAxis: false + }); + } + + // Bind resize event + if (window.onresize == null) { + window.onresize = $$.generateResize(); + } + if (window.onresize.add) { + window.onresize.add(function () { + config.onresize.call($$); + }); + window.onresize.add(function () { + $$.api.flush(); + }); + window.onresize.add(function () { + config.onresized.call($$); + }); + } + + // export element of the chart + $$.api.element = $$.selectChart.node(); + }; + + c3_chart_internal_fn.smoothLines = function (el, type) { + var $$ = this; + if (type === 'grid') { + el.each(function () { + var g = $$.d3.select(this), + x1 = g.attr('x1'), + x2 = g.attr('x2'), + y1 = g.attr('y1'), + y2 = g.attr('y2'); + g.attr({ + 'x1': Math.ceil(x1), + 'x2': Math.ceil(x2), + 'y1': Math.ceil(y1), + 'y2': Math.ceil(y2) + }); + }); + } + }; + + + c3_chart_internal_fn.updateSizes = function () { + var $$ = this, config = $$.config; + var legendHeight = $$.legend ? $$.getLegendHeight() : 0, + legendWidth = $$.legend ? $$.getLegendWidth() : 0, + legendHeightForBottom = $$.isLegendRight || $$.isLegendInset ? 0 : legendHeight, + hasArc = $$.hasArcType(), + xAxisHeight = config.axis_rotated || hasArc ? 0 : $$.getHorizontalAxisHeight('x'), + subchartHeight = config.subchart_show && !hasArc ? (config.subchart_size_height + xAxisHeight) : 0; + + $$.currentWidth = $$.getCurrentWidth(); + $$.currentHeight = $$.getCurrentHeight(); + + // for main + $$.margin = config.axis_rotated ? { + top: $$.getHorizontalAxisHeight('y2') + $$.getCurrentPaddingTop(), + right: hasArc ? 0 : $$.getCurrentPaddingRight(), + bottom: $$.getHorizontalAxisHeight('y') + legendHeightForBottom + $$.getCurrentPaddingBottom(), + left: subchartHeight + (hasArc ? 0 : $$.getCurrentPaddingLeft()) + } : { + top: 4 + $$.getCurrentPaddingTop(), // for top tick text + right: hasArc ? 0 : $$.getCurrentPaddingRight(), + bottom: xAxisHeight + subchartHeight + legendHeightForBottom + $$.getCurrentPaddingBottom(), + left: hasArc ? 0 : $$.getCurrentPaddingLeft() + }; + + // for subchart + $$.margin2 = config.axis_rotated ? { + top: $$.margin.top, + right: NaN, + bottom: 20 + legendHeightForBottom, + left: $$.rotated_padding_left + } : { + top: $$.currentHeight - subchartHeight - legendHeightForBottom, + right: NaN, + bottom: xAxisHeight + legendHeightForBottom, + left: $$.margin.left + }; + + // for legend + $$.margin3 = { + top: 0, + right: NaN, + bottom: 0, + left: 0 + }; + if ($$.updateSizeForLegend) { $$.updateSizeForLegend(legendHeight, legendWidth); } + + $$.width = $$.currentWidth - $$.margin.left - $$.margin.right; + $$.height = $$.currentHeight - $$.margin.top - $$.margin.bottom; + if ($$.width < 0) { $$.width = 0; } + if ($$.height < 0) { $$.height = 0; } + + $$.width2 = config.axis_rotated ? $$.margin.left - $$.rotated_padding_left - $$.rotated_padding_right : $$.width; + $$.height2 = config.axis_rotated ? $$.height : $$.currentHeight - $$.margin2.top - $$.margin2.bottom; + if ($$.width2 < 0) { $$.width2 = 0; } + if ($$.height2 < 0) { $$.height2 = 0; } + + // for arc + $$.arcWidth = $$.width - ($$.isLegendRight ? legendWidth + 10 : 0); + $$.arcHeight = $$.height - ($$.isLegendRight ? 0 : 10); + if ($$.hasType('gauge')) { + $$.arcHeight += $$.height - $$.getGaugeLabelHeight(); + } + if ($$.updateRadius) { $$.updateRadius(); } + + if ($$.isLegendRight && hasArc) { + $$.margin3.left = $$.arcWidth / 2 + $$.radiusExpanded * 1.1; + } + }; + + c3_chart_internal_fn.updateTargets = function (targets) { + var $$ = this, config = $$.config; + + /*-- Main --*/ + + //-- Text --// + $$.updateTargetsForText(targets); + + //-- Bar --// + $$.updateTargetsForBar(targets); + + //-- Line --// + $$.updateTargetsForLine(targets); + + //-- Arc --// + if ($$.updateTargetsForArc) { $$.updateTargetsForArc(targets); } + if ($$.updateTargetsForSubchart) { $$.updateTargetsForSubchart(targets); } + + /*-- Show --*/ + + // Fade-in each chart + $$.svg.selectAll('.' + CLASS.target).filter(function (d) { return $$.isTargetToShow(d.id); }) + .transition().duration(config.transition_duration) + .style("opacity", 1); + }; + + c3_chart_internal_fn.redraw = function (options, transitions) { + var $$ = this, main = $$.main, d3 = $$.d3, config = $$.config; + var areaIndices = $$.getShapeIndices($$.isAreaType), barIndices = $$.getShapeIndices($$.isBarType), lineIndices = $$.getShapeIndices($$.isLineType); + var withY, withSubchart, withTransition, withTransitionForExit, withTransitionForAxis, withTransform, withUpdateXDomain, withUpdateOrgXDomain, withLegend; + var hideAxis = $$.hasArcType(); + var drawArea, drawBar, drawLine, xForText, yForText; + var duration, durationForExit, durationForAxis; + var waitForDraw, flow; + var targetsToShow = $$.filterTargetsToShow($$.data.targets), tickValues, i, intervalForCulling; + var xv = $$.xv.bind($$), + cx = ($$.config.axis_rotated ? $$.circleY : $$.circleX).bind($$), + cy = ($$.config.axis_rotated ? $$.circleX : $$.circleY).bind($$); + + options = options || {}; + withY = getOption(options, "withY", true); + withSubchart = getOption(options, "withSubchart", true); + withTransition = getOption(options, "withTransition", true); + withTransform = getOption(options, "withTransform", false); + withUpdateXDomain = getOption(options, "withUpdateXDomain", false); + withUpdateOrgXDomain = getOption(options, "withUpdateOrgXDomain", false); + withLegend = getOption(options, "withLegend", false); + withTransitionForExit = getOption(options, "withTransitionForExit", withTransition); + withTransitionForAxis = getOption(options, "withTransitionForAxis", withTransition); + + duration = withTransition ? config.transition_duration : 0; + durationForExit = withTransitionForExit ? duration : 0; + durationForAxis = withTransitionForAxis ? duration : 0; + + transitions = transitions || $$.generateAxisTransitions(durationForAxis); + + // update legend and transform each g + if (withLegend && config.legend_show) { + $$.updateLegend($$.mapToIds($$.data.targets), options, transitions); + } + + // MEMO: needed for grids calculation + if ($$.isCategorized() && targetsToShow.length === 0) { + $$.x.domain([0, $$.axes.x.selectAll('.tick').size()]); + } + + if (targetsToShow.length) { + $$.updateXDomain(targetsToShow, withUpdateXDomain, withUpdateOrgXDomain); + // update axis tick values according to options + if (!config.axis_x_tick_values && (config.axis_x_tick_fit || config.axis_x_tick_count)) { + tickValues = $$.generateTickValues($$.mapTargetsToUniqueXs(targetsToShow), config.axis_x_tick_count); + $$.xAxis.tickValues(tickValues); + $$.subXAxis.tickValues(tickValues); + } + } else { + $$.xAxis.tickValues([]); + $$.subXAxis.tickValues([]); + } + + $$.y.domain($$.getYDomain(targetsToShow, 'y')); + $$.y2.domain($$.getYDomain(targetsToShow, 'y2')); + + // axes + $$.redrawAxis(transitions, hideAxis); + + // Update axis label + $$.updateAxisLabels(withTransition); + + // show/hide if manual culling needed + if (withUpdateXDomain && targetsToShow.length) { + if (config.axis_x_tick_culling && tickValues) { + for (i = 1; i < tickValues.length; i++) { + if (tickValues.length / i < config.axis_x_tick_culling_max) { + intervalForCulling = i; + break; + } + } + $$.svg.selectAll('.' + CLASS.axisX + ' .tick text').each(function (e) { + var index = tickValues.indexOf(e); + if (index >= 0) { + d3.select(this).style('display', index % intervalForCulling ? 'none' : 'block'); + } + }); + } else { + $$.svg.selectAll('.' + CLASS.axisX + ' .tick text').style('display', 'block'); + } + } + + // rotate tick text if needed + if (!config.axis_rotated && config.axis_x_tick_rotate) { + $$.rotateTickText($$.axes.x, transitions.axisX, config.axis_x_tick_rotate); + } + + // setup drawer - MEMO: these must be called after axis updated + drawArea = $$.generateDrawArea ? $$.generateDrawArea(areaIndices, false) : undefined; + drawBar = $$.generateDrawBar ? $$.generateDrawBar(barIndices) : undefined; + drawLine = $$.generateDrawLine ? $$.generateDrawLine(lineIndices, false) : undefined; + xForText = $$.generateXYForText(areaIndices, barIndices, lineIndices, true); + yForText = $$.generateXYForText(areaIndices, barIndices, lineIndices, false); + + // Update sub domain + $$.subY.domain($$.y.domain()); + $$.subY2.domain($$.y2.domain()); + + // tooltip + $$.tooltip.style("display", "none"); + + // xgrid focus + $$.updateXgridFocus(); + + // Data empty label positioning and text. + main.select("text." + CLASS.text + '.' + CLASS.empty) + .attr("x", $$.width / 2) + .attr("y", $$.height / 2) + .text(config.data_empty_label_text) + .transition() + .style('opacity', targetsToShow.length ? 0 : 1); + + // grid + $$.redrawGrid(duration, withY); + + // rect for regions + $$.redrawRegion(duration); + + // bars + $$.redrawBar(durationForExit); + + // lines, areas and cricles + $$.redrawLine(durationForExit); + $$.redrawArea(durationForExit); + if (config.point_show) { $$.redrawCircle(); } + + // text + if ($$.hasDataLabel()) { + $$.redrawText(durationForExit); + } + + // arc + if ($$.redrawArc) { $$.redrawArc(duration, durationForExit, withTransform); } + + // subchart + if ($$.redrawSubchart) { + $$.redrawSubchart(withSubchart, transitions, duration, durationForExit, areaIndices, barIndices, lineIndices); + } + + // circles for select + main.selectAll('.' + CLASS.selectedCircles) + .filter($$.isBarType.bind($$)) + .selectAll('circle') + .remove(); + + // event rect + if (config.interaction_enabled) { + $$.redrawEventRect(); + } + + // transition should be derived from one transition + d3.transition().duration(duration).each(function () { + var transitions = []; + + $$.addTransitionForBar(transitions, drawBar); + $$.addTransitionForLine(transitions, drawLine); + $$.addTransitionForArea(transitions, drawArea); + if (config.point_show) { $$.addTransitionForCircle(transitions, cx, cy); } + $$.addTransitionForText(transitions, xForText, yForText, options.flow); + $$.addTransitionForRegion(transitions); + $$.addTransitionForGrid(transitions); + + // Wait for end of transitions if called from flow API + if (options.flow) { + waitForDraw = $$.generateWait(); + transitions.forEach(function (t) { + waitForDraw.add(t); + }); + flow = $$.generateFlow({ + targets: targetsToShow, + flow: options.flow, + duration: duration, + drawBar: drawBar, + drawLine: drawLine, + drawArea: drawArea, + cx: cx, + cy: cy, + xv: xv, + xForText: xForText, + yForText: yForText + }); + } + }) + .call(waitForDraw || function () {}, flow || function () {}); + + // update fadein condition + $$.mapToIds($$.data.targets).forEach(function (id) { + $$.withoutFadeIn[id] = true; + }); + + if ($$.updateZoom) { $$.updateZoom(); } + }; + + c3_chart_internal_fn.updateAndRedraw = function (options) { + var $$ = this, config = $$.config, transitions; + options = options || {}; + // same with redraw + options.withTransition = getOption(options, "withTransition", true); + options.withTransform = getOption(options, "withTransform", false); + options.withLegend = getOption(options, "withLegend", false); + // NOT same with redraw + options.withUpdateXDomain = true; + options.withUpdateOrgXDomain = true; + options.withTransitionForExit = false; + options.withTransitionForTransform = getOption(options, "withTransitionForTransform", options.withTransition); + // MEMO: this needs to be called before updateLegend and it means this ALWAYS needs to be called) + $$.updateSizes(); + // MEMO: called in updateLegend in redraw if withLegend + if (!(options.withLegend && config.legend_show)) { + transitions = $$.generateAxisTransitions(options.withTransitionForAxis ? config.transition_duration : 0); + // Update scales + $$.updateScales(); + $$.updateSvgSize(); + // Update g positions + $$.transformAll(options.withTransitionForTransform, transitions); + } + // Draw with new sizes & scales + $$.redraw(options, transitions); + }; + + c3_chart_internal_fn.isTimeSeries = function () { + return this.config.axis_x_type === 'timeseries'; + }; + c3_chart_internal_fn.isCategorized = function () { + return this.config.axis_x_type.indexOf('categor') >= 0; + }; + c3_chart_internal_fn.isCustomX = function () { + var $$ = this, config = $$.config; + return !$$.isTimeSeries() && (config.data_x || notEmpty(config.data_xs)); + }; + + c3_chart_internal_fn.getTranslate = function (target) { + var $$ = this, config = $$.config, x, y; + if (target === 'main') { + x = asHalfPixel($$.margin.left); + y = asHalfPixel($$.margin.top); + } else if (target === 'context') { + x = asHalfPixel($$.margin2.left); + y = asHalfPixel($$.margin2.top); + } else if (target === 'legend') { + x = $$.margin3.left; + y = $$.margin3.top; + } else if (target === 'x') { + x = 0; + y = config.axis_rotated ? 0 : $$.height; + } else if (target === 'y') { + x = 0; + y = config.axis_rotated ? $$.height : 0; + } else if (target === 'y2') { + x = config.axis_rotated ? 0 : $$.width; + y = config.axis_rotated ? 1 : 0; + } else if (target === 'subx') { + x = 0; + y = config.axis_rotated ? 0 : $$.height2; + } else if (target === 'arc') { + x = $$.arcWidth / 2; + y = $$.arcHeight / 2; + } + return "translate(" + x + "," + y + ")"; + }; + c3_chart_internal_fn.initialOpacity = function (d) { + return d.value !== null && this.withoutFadeIn[d.id] ? 1 : 0; + }; + c3_chart_internal_fn.opacityForCircle = function (d) { + var $$ = this; + return isValue(d.value) ? $$.isScatterType(d) ? 0.5 : 1 : 0; + }; + c3_chart_internal_fn.opacityForText = function () { + return this.hasDataLabel() ? 1 : 0; + }; + c3_chart_internal_fn.xx = function (d) { + return d ? this.x(d.x) : null; + }; + c3_chart_internal_fn.xv = function (d) { + var $$ = this; + return Math.ceil($$.x($$.isTimeSeries() ? $$.parseDate(d.value) : d.value)); + }; + c3_chart_internal_fn.yv = function (d) { + var $$ = this, + yScale = d.axis && d.axis === 'y2' ? $$.y2 : $$.y; + return Math.ceil(yScale(d.value)); + }; + c3_chart_internal_fn.subxx = function (d) { + return d ? this.subX(d.x) : null; + }; + + c3_chart_internal_fn.transformMain = function (withTransition, transitions) { + var $$ = this, + xAxis, yAxis, y2Axis; + if (transitions && transitions.axisX) { + xAxis = transitions.axisX; + } else { + xAxis = $$.main.select('.' + CLASS.axisX); + if (withTransition) { xAxis = xAxis.transition(); } + } + if (transitions && transitions.axisY) { + yAxis = transitions.axisY; + } else { + yAxis = $$.main.select('.' + CLASS.axisY); + if (withTransition) { yAxis = yAxis.transition(); } + } + if (transitions && transitions.axisY2) { + y2Axis = transitions.axisY2; + } else { + y2Axis = $$.main.select('.' + CLASS.axisY2); + if (withTransition) { y2Axis = y2Axis.transition(); } + } + (withTransition ? $$.main.transition() : $$.main).attr("transform", $$.getTranslate('main')); + xAxis.attr("transform", $$.getTranslate('x')); + yAxis.attr("transform", $$.getTranslate('y')); + y2Axis.attr("transform", $$.getTranslate('y2')); + $$.main.select('.' + CLASS.chartArcs).attr("transform", $$.getTranslate('arc')); + }; + c3_chart_internal_fn.transformAll = function (withTransition, transitions) { + var $$ = this; + $$.transformMain(withTransition, transitions); + if ($$.config.subchart_show) { $$.transformContext(withTransition, transitions); } + if ($$.legend) { $$.transformLegend(withTransition); } + }; + + c3_chart_internal_fn.updateSvgSize = function () { + var $$ = this; + $$.svg.attr('width', $$.currentWidth).attr('height', $$.currentHeight); + $$.svg.select('#' + $$.clipId).select('rect') + .attr('width', $$.width) + .attr('height', $$.height); + $$.svg.select('#' + $$.clipIdForXAxis).select('rect') + .attr('x', $$.getXAxisClipX.bind($$)) + .attr('y', $$.getXAxisClipY.bind($$)) + .attr('width', $$.getXAxisClipWidth.bind($$)) + .attr('height', $$.getXAxisClipHeight.bind($$)); + $$.svg.select('#' + $$.clipIdForYAxis).select('rect') + .attr('x', $$.getYAxisClipX.bind($$)) + .attr('y', $$.getYAxisClipY.bind($$)) + .attr('width', $$.getYAxisClipWidth.bind($$)) + .attr('height', $$.getYAxisClipHeight.bind($$)); + $$.svg.select('.' + CLASS.zoomRect) + .attr('width', $$.width) + .attr('height', $$.height); + // MEMO: parent div's height will be bigger than svg when <!DOCTYPE html> + $$.selectChart.style('max-height', $$.currentHeight + "px"); + }; + + + c3_chart_internal_fn.updateDimension = function () { + var $$ = this; + if ($$.config.axis_rotated) { + $$.axes.x.call($$.xAxis); + $$.axes.subx.call($$.subXAxis); + } else { + $$.axes.y.call($$.yAxis); + $$.axes.y2.call($$.y2Axis); + } + $$.updateSizes(); + $$.updateScales(); + $$.updateSvgSize(); + $$.transformAll(false); + }; + + c3_chart_internal_fn.observeInserted = function (selection) { + var $$ = this, observer = new MutationObserver(function (mutations) { + mutations.forEach(function (mutation) { + if (mutation.type === 'childList' && mutation.previousSibling) { + observer.disconnect(); + // need to wait for completion of load because size calculation requires the actual sizes determined after that completion + var interval = window.setInterval(function () { + // parentNode will NOT be null when completed + if (selection.node().parentNode) { + window.clearInterval(interval); + $$.updateDimension(); + $$.redraw({ + withTransform: true, + withUpdateXDomain: true, + withUpdateOrgXDomain: true, + withTransition: false, + withTransitionForTransform: false, + withLegend: true + }); + selection.transition().style('opacity', 1); + } + }, 10); + } + }); + }); + observer.observe(selection.node(), {attributes: true, childList: true, characterData: true}); + }; + + + c3_chart_internal_fn.generateResize = function () { + var resizeFunctions = []; + function callResizeFunctions() { + resizeFunctions.forEach(function (f) { + f(); + }); + } + callResizeFunctions.add = function (f) { + resizeFunctions.push(f); + }; + return callResizeFunctions; + }; + + c3_chart_internal_fn.endall = function (transition, callback) { + var n = 0; + transition + .each(function () { ++n; }) + .each("end", function () { + if (!--n) { callback.apply(this, arguments); } + }); + }; + c3_chart_internal_fn.generateWait = function () { + var transitionsToWait = [], + f = function (transition, callback) { + var timer = setInterval(function () { + var done = 0; + transitionsToWait.forEach(function (t) { + if (t.empty()) { + done += 1; + return; + } + try { + t.transition(); + } catch (e) { + done += 1; + } + }); + if (done === transitionsToWait.length) { + clearInterval(timer); + if (callback) { callback(); } + } + }, 10); + }; + f.add = function (transition) { + transitionsToWait.push(transition); + }; + return f; + }; + + c3_chart_internal_fn.parseDate = function (date) { + var $$ = this, parsedDate; + if (date instanceof Date) { + parsedDate = date; + } else if (typeof date === 'number') { + parsedDate = new Date(date); + } else { + parsedDate = $$.dataTimeFormat($$.config.data_xFormat).parse(date); + } + if (!parsedDate || isNaN(+parsedDate)) { + window.console.error("Failed to parse x '" + date + "' to Date object"); + } + return parsedDate; + }; + + c3_chart_internal_fn.getDefaultConfig = function () { + var config = { + bindto: '#chart', + size_width: undefined, + size_height: undefined, + padding_left: undefined, + padding_right: undefined, + padding_top: undefined, + padding_bottom: undefined, + zoom_enabled: false, + zoom_extent: undefined, + zoom_privileged: false, + zoom_onzoom: function () {}, + interaction_enabled: true, + onmouseover: function () {}, + onmouseout: function () {}, + onresize: function () {}, + onresized: function () {}, + transition_duration: 350, + data_x: undefined, + data_xs: {}, + data_xFormat: '%Y-%m-%d', + data_xLocaltime: true, + data_idConverter: function (id) { return id; }, + data_names: {}, + data_classes: {}, + data_groups: [], + data_axes: {}, + data_type: undefined, + data_types: {}, + data_labels: {}, + data_order: 'desc', + data_regions: {}, + data_color: undefined, + data_colors: {}, + data_hide: false, + data_filter: undefined, + data_selection_enabled: false, + data_selection_grouped: false, + data_selection_isselectable: function () { return true; }, + data_selection_multiple: true, + data_onclick: function () {}, + data_onmouseover: function () {}, + data_onmouseout: function () {}, + data_onselected: function () {}, + data_onunselected: function () {}, + data_ondragstart: function () {}, + data_ondragend: function () {}, + data_url: undefined, + data_json: undefined, + data_rows: undefined, + data_columns: undefined, + data_mimeType: undefined, + data_keys: undefined, + // configuration for no plot-able data supplied. + data_empty_label_text: "", + // subchart + subchart_show: false, + subchart_size_height: 60, + subchart_onbrush: function () {}, + // color + color_pattern: [], + color_threshold: {}, + // legend + legend_show: true, + legend_position: 'bottom', + legend_inset_anchor: 'top-left', + legend_inset_x: 10, + legend_inset_y: 0, + legend_inset_step: undefined, + legend_item_onclick: undefined, + legend_item_onmouseover: undefined, + legend_item_onmouseout: undefined, + legend_equally: false, + // axis + axis_rotated: false, + axis_x_show: true, + axis_x_type: 'indexed', + axis_x_localtime: true, + axis_x_categories: [], + axis_x_tick_centered: false, + axis_x_tick_format: undefined, + axis_x_tick_culling: {}, + axis_x_tick_culling_max: 10, + axis_x_tick_count: undefined, + axis_x_tick_fit: true, + axis_x_tick_values: null, + axis_x_tick_rotate: undefined, + axis_x_tick_outer: true, + axis_x_max: undefined, + axis_x_min: undefined, + axis_x_padding: {}, + axis_x_height: undefined, + axis_x_default: undefined, + axis_x_label: {}, + axis_y_show: true, + axis_y_max: undefined, + axis_y_min: undefined, + axis_y_center: undefined, + axis_y_label: {}, + axis_y_tick_format: undefined, + axis_y_tick_outer: true, + axis_y_padding: {}, + axis_y_ticks: 10, + axis_y_default: undefined, + axis_y2_show: false, + axis_y2_max: undefined, + axis_y2_min: undefined, + axis_y2_center: undefined, + axis_y2_label: {}, + axis_y2_tick_format: undefined, + axis_y2_tick_outer: true, + axis_y2_padding: {}, + axis_y2_ticks: 10, + axis_y2_default: undefined, + // grid + grid_x_show: false, + grid_x_type: 'tick', + grid_x_lines: [], + grid_y_show: false, + // not used + // grid_y_type: 'tick', + grid_y_lines: [], + grid_y_ticks: 10, + grid_focus_show: true, + grid_lines_front: true, + // point - point of each data + point_show: true, + point_r: 2.5, + point_focus_expand_enabled: true, + point_focus_expand_r: undefined, + point_select_r: undefined, + line_connect_null: false, + // bar + bar_width: undefined, + bar_width_ratio: 0.6, + bar_width_max: undefined, + bar_zerobased: true, + // area + area_zerobased: true, + // pie + pie_label_show: true, + pie_label_format: undefined, + pie_label_threshold: 0.05, + pie_sort: true, + pie_expand: true, + // gauge + gauge_label_show: true, + gauge_label_format: undefined, + gauge_expand: true, + gauge_min: 0, + gauge_max: 100, + gauge_units: undefined, + gauge_width: undefined, + // donut + donut_label_show: true, + donut_label_format: undefined, + donut_label_threshold: 0.05, + donut_width: undefined, + donut_sort: true, + donut_expand: true, + donut_title: "", + // region - region to change style + regions: [], + // tooltip - show when mouseover on each data + tooltip_show: true, + tooltip_grouped: true, + tooltip_format_title: undefined, + tooltip_format_name: undefined, + tooltip_format_value: undefined, + tooltip_contents: function (d, defaultTitleFormat, defaultValueFormat, color) { + return this.getTooltipContent ? this.getTooltipContent(d, defaultTitleFormat, defaultValueFormat, color) : ''; + }, + tooltip_init_show: false, + tooltip_init_x: 0, + tooltip_init_position: {top: '0px', left: '50px'} + }; + + Object.keys(this.additionalConfig).forEach(function (key) { + config[key] = this.additionalConfig[key]; + }, this); + + return config; + }; + c3_chart_internal_fn.additionalConfig = {}; + + c3_chart_internal_fn.loadConfig = function (config) { + var this_config = this.config, target, keys, read; + function find() { + var key = keys.shift(); + // console.log("key =>", key, ", target =>", target); + if (key && target && typeof target === 'object' && key in target) { + target = target[key]; + return find(); + } + else if (!key) { + return target; + } + else { + return undefined; + } + } + Object.keys(this_config).forEach(function (key) { + target = config; + keys = key.split('_'); + read = find(); + // console.log("CONFIG : ", key, read); + if (isDefined(read)) { + this_config[key] = read; + } + }); + }; + + c3_chart_internal_fn.getScale = function (min, max, forTimeseries) { + return (forTimeseries ? this.d3.time.scale() : this.d3.scale.linear()).range([min, max]); + }; + c3_chart_internal_fn.getX = function (min, max, domain, offset) { + var $$ = this, + scale = $$.getScale(min, max, $$.isTimeSeries()), + _scale = domain ? scale.domain(domain) : scale, key; + // Define customized scale if categorized axis + if ($$.isCategorized()) { + offset = offset || function () { return 0; }; + scale = function (d, raw) { + var v = _scale(d) + offset(d); + return raw ? v : Math.ceil(v); + }; + } else { + scale = function (d, raw) { + var v = _scale(d); + return raw ? v : Math.ceil(v); + }; + } + // define functions + for (key in _scale) { + scale[key] = _scale[key]; + } + scale.orgDomain = function () { + return _scale.domain(); + }; + // define custom domain() for categorized axis + if ($$.isCategorized()) { + scale.domain = function (domain) { + if (!arguments.length) { + domain = this.orgDomain(); + return [domain[0], domain[1] + 1]; + } + _scale.domain(domain); + return scale; + }; + } + return scale; + }; + c3_chart_internal_fn.getY = function (min, max, domain) { + var scale = this.getScale(min, max); + if (domain) { scale.domain(domain); } + return scale; + }; + c3_chart_internal_fn.getYScale = function (id) { + return this.getAxisId(id) === 'y2' ? this.y2 : this.y; + }; + c3_chart_internal_fn.getSubYScale = function (id) { + return this.getAxisId(id) === 'y2' ? this.subY2 : this.subY; + }; + c3_chart_internal_fn.updateScales = function () { + var $$ = this, config = $$.config, + forInit = !$$.x; + // update edges + $$.xMin = config.axis_rotated ? 1 : 0; + $$.xMax = config.axis_rotated ? $$.height : $$.width; + $$.yMin = config.axis_rotated ? 0 : $$.height; + $$.yMax = config.axis_rotated ? $$.width : 1; + $$.subXMin = $$.xMin; + $$.subXMax = $$.xMax; + $$.subYMin = config.axis_rotated ? 0 : $$.height2; + $$.subYMax = config.axis_rotated ? $$.width2 : 1; + // update scales + $$.x = $$.getX($$.xMin, $$.xMax, forInit ? undefined : $$.x.orgDomain(), function () { return $$.xAxis.tickOffset(); }); + $$.y = $$.getY($$.yMin, $$.yMax, forInit ? config.axis_y_default : $$.y.domain()); + $$.y2 = $$.getY($$.yMin, $$.yMax, forInit ? config.axis_y2_default : $$.y2.domain()); + $$.subX = $$.getX($$.xMin, $$.xMax, $$.orgXDomain, function (d) { return d % 1 ? 0 : $$.subXAxis.tickOffset(); }); + $$.subY = $$.getY($$.subYMin, $$.subYMax, forInit ? config.axis_y_default : $$.subY.domain()); + $$.subY2 = $$.getY($$.subYMin, $$.subYMax, forInit ? config.axis_y2_default : $$.subY2.domain()); + // update axes + $$.xAxisTickFormat = $$.getXAxisTickFormat(); + $$.xAxisTickValues = config.axis_x_tick_values ? config.axis_x_tick_values : (forInit ? undefined : $$.xAxis.tickValues()); + $$.xAxis = $$.getXAxis($$.x, $$.xOrient, $$.xAxisTickFormat, $$.xAxisTickValues); + $$.subXAxis = $$.getXAxis($$.subX, $$.subXOrient, $$.xAxisTickFormat, $$.xAxisTickValues); + $$.yAxis = $$.getYAxis($$.y, $$.yOrient, config.axis_y_tick_format, config.axis_y_ticks); + $$.y2Axis = $$.getYAxis($$.y2, $$.y2Orient, config.axis_y2_tick_format, config.axis_y2_ticks); + // Set initialized scales to brush and zoom + if (!forInit) { + if ($$.brush) { $$.brush.scale($$.subX); } + if (config.zoom_enabled) { $$.zoom.scale($$.x); } + } + // update for arc + if ($$.updateArc) { $$.updateArc(); } + }; + + c3_chart_internal_fn.getYDomainMin = function (targets) { + var $$ = this, config = $$.config, + ids = $$.mapToIds(targets), ys = $$.getValuesAsIdKeyed(targets), + j, k, baseId, idsInGroup, id, hasNegativeValue; + if (config.data_groups.length > 0) { + hasNegativeValue = $$.hasNegativeValueInTargets(targets); + for (j = 0; j < config.data_groups.length; j++) { + // Determine baseId + idsInGroup = config.data_groups[j].filter(function (id) { return ids.indexOf(id) >= 0; }); + if (idsInGroup.length === 0) { continue; } + baseId = idsInGroup[0]; + // Consider negative values + if (hasNegativeValue && ys[baseId]) { + ys[baseId].forEach(function (v, i) { + ys[baseId][i] = v < 0 ? v : 0; + }); + } + // Compute min + for (k = 1; k < idsInGroup.length; k++) { + id = idsInGroup[k]; + if (! ys[id]) { continue; } + ys[id].forEach(function (v, i) { + if ($$.getAxisId(id) === $$.getAxisId(baseId) && ys[baseId] && !(hasNegativeValue && +v > 0)) { + ys[baseId][i] += +v; + } + }); + } + } + } + return $$.d3.min(Object.keys(ys).map(function (key) { return $$.d3.min(ys[key]); })); + }; + c3_chart_internal_fn.getYDomainMax = function (targets) { + var $$ = this, config = $$.config, + ids = $$.mapToIds(targets), ys = $$.getValuesAsIdKeyed(targets), + j, k, baseId, idsInGroup, id, hasPositiveValue; + if (config.data_groups.length > 0) { + hasPositiveValue = $$.hasPositiveValueInTargets(targets); + for (j = 0; j < config.data_groups.length; j++) { + // Determine baseId + idsInGroup = config.data_groups[j].filter(function (id) { return ids.indexOf(id) >= 0; }); + if (idsInGroup.length === 0) { continue; } + baseId = idsInGroup[0]; + // Consider positive values + if (hasPositiveValue && ys[baseId]) { + ys[baseId].forEach(function (v, i) { + ys[baseId][i] = v > 0 ? v : 0; + }); + } + // Compute max + for (k = 1; k < idsInGroup.length; k++) { + id = idsInGroup[k]; + if (! ys[id]) { continue; } + ys[id].forEach(function (v, i) { + if ($$.getAxisId(id) === $$.getAxisId(baseId) && ys[baseId] && !(hasPositiveValue && +v < 0)) { + ys[baseId][i] += +v; + } + }); + } + } + } + return $$.d3.max(Object.keys(ys).map(function (key) { return $$.d3.max(ys[key]); })); + }; + c3_chart_internal_fn.getYDomain = function (targets, axisId) { + var $$ = this, config = $$.config, + yTargets = targets.filter(function (d) { return $$.getAxisId(d.id) === axisId; }), + yMin = axisId === 'y2' ? config.axis_y2_min : config.axis_y_min, + yMax = axisId === 'y2' ? config.axis_y2_max : config.axis_y_max, + yDomainMin = isValue(yMin) ? yMin : $$.getYDomainMin(yTargets), + yDomainMax = isValue(yMax) ? yMax : $$.getYDomainMax(yTargets), + domainLength, padding, padding_top, padding_bottom, + center = axisId === 'y2' ? config.axis_y2_center : config.axis_y_center, + yDomainAbs, lengths, diff, ratio, isAllPositive, isAllNegative, + isZeroBased = ($$.hasType('bar', yTargets) && config.bar_zerobased) || ($$.hasType('area', yTargets) && config.area_zerobased), + showHorizontalDataLabel = $$.hasDataLabel() && config.axis_rotated, + showVerticalDataLabel = $$.hasDataLabel() && !config.axis_rotated; + if (yTargets.length === 0) { // use current domain if target of axisId is none + return axisId === 'y2' ? $$.y2.domain() : $$.y.domain(); + } + if (isNaN(yDomainMin)) { // set minimum to zero when not number + yDomainMin = 0; + } + if (isNaN(yDomainMax)) { // set maximum to have same value as yDomainMin + yDomainMax = yDomainMin; + } + if (yDomainMin === yDomainMax) { + yDomainMin < 0 ? yDomainMax = 0 : yDomainMin = 0; + } + isAllPositive = yDomainMin >= 0 && yDomainMax >= 0; + isAllNegative = yDomainMin <= 0 && yDomainMax <= 0; + + // Cancel zerobased if axis_*_min / axis_*_max specified + if ((isValue(yMin) && isAllPositive) || (isValue(yMax) && isAllNegative)) { + isZeroBased = false; + } + + // Bar/Area chart should be 0-based if all positive|negative + if (isZeroBased) { + if (isAllPositive) { yDomainMin = 0; } + if (isAllNegative) { yDomainMax = 0; } + } + + domainLength = Math.abs(yDomainMax - yDomainMin); + padding = padding_top = padding_bottom = domainLength * 0.1; + + if (center) { + yDomainAbs = Math.max(Math.abs(yDomainMin), Math.abs(yDomainMax)); + yDomainMax = yDomainAbs - center; + yDomainMin = center - yDomainAbs; + } + // add padding for data label + if (showHorizontalDataLabel) { + lengths = $$.getDataLabelLength(yDomainMin, yDomainMax, axisId, 'width'); + diff = diffDomain($$.y.range()); + ratio = [lengths[0] / diff, lengths[1] / diff]; + padding_top += domainLength * (ratio[1] / (1 - ratio[0] - ratio[1])); + padding_bottom += domainLength * (ratio[0] / (1 - ratio[0] - ratio[1])); + } else if (showVerticalDataLabel) { + lengths = $$.getDataLabelLength(yDomainMin, yDomainMax, axisId, 'height'); + padding_top += lengths[1]; + padding_bottom += lengths[0]; + } + if (axisId === 'y' && notEmpty(config.axis_y_padding)) { + padding_top = $$.getAxisPadding(config.axis_y_padding, 'top', padding, domainLength); + padding_bottom = $$.getAxisPadding(config.axis_y_padding, 'bottom', padding, domainLength); + } + if (axisId === 'y2' && notEmpty(config.axis_y2_padding)) { + padding_top = $$.getAxisPadding(config.axis_y2_padding, 'top', padding, domainLength); + padding_bottom = $$.getAxisPadding(config.axis_y2_padding, 'bottom', padding, domainLength); + } + // Bar/Area chart should be 0-based if all positive|negative + if (isZeroBased) { + if (isAllPositive) { padding_bottom = yDomainMin; } + if (isAllNegative) { padding_top = -yDomainMax; } + } + return [yDomainMin - padding_bottom, yDomainMax + padding_top]; + }; + c3_chart_internal_fn.getXDomainMin = function (targets) { + var $$ = this, config = $$.config; + return isDefined(config.axis_x_min) ? + ($$.isTimeSeries() ? this.parseDate(config.axis_x_min) : config.axis_x_min) : + $$.d3.min(targets, function (t) { return $$.d3.min(t.values, function (v) { return v.x; }); }); + }; + c3_chart_internal_fn.getXDomainMax = function (targets) { + var $$ = this, config = $$.config; + return isDefined(config.axis_x_max) ? + ($$.isTimeSeries() ? this.parseDate(config.axis_x_max) : config.axis_x_max) : + $$.d3.max(targets, function (t) { return $$.d3.max(t.values, function (v) { return v.x; }); }); + }; + c3_chart_internal_fn.getXDomainPadding = function (targets) { + var $$ = this, config = $$.config, + edgeX = this.getEdgeX(targets), diff = edgeX[1] - edgeX[0], + maxDataCount, padding, paddingLeft, paddingRight; + if ($$.isCategorized()) { + padding = 0; + } else if ($$.hasType('bar', targets)) { + maxDataCount = $$.getMaxDataCount(); + padding = maxDataCount > 1 ? (diff / (maxDataCount - 1)) / 2 : 0.5; + } else { + padding = diff * 0.01; + } + if (typeof config.axis_x_padding === 'object' && notEmpty(config.axis_x_padding)) { + paddingLeft = isValue(config.axis_x_padding.left) ? config.axis_x_padding.left : padding; + paddingRight = isValue(config.axis_x_padding.right) ? config.axis_x_padding.right : padding; + } else if (typeof config.axis_x_padding === 'number') { + paddingLeft = paddingRight = config.axis_x_padding; + } else { + paddingLeft = paddingRight = padding; + } + return {left: paddingLeft, right: paddingRight}; + }; + c3_chart_internal_fn.getXDomain = function (targets) { + var $$ = this, + xDomain = [$$.getXDomainMin(targets), $$.getXDomainMax(targets)], + firstX = xDomain[0], lastX = xDomain[1], + padding = $$.getXDomainPadding(targets), + min = 0, max = 0; + // show center of x domain if min and max are the same + if ((firstX - lastX) === 0 && !$$.isCategorized()) { + firstX = $$.isTimeSeries() ? new Date(firstX.getTime() * 0.5) : -0.5; + lastX = $$.isTimeSeries() ? new Date(lastX.getTime() * 1.5) : 0.5; + } + if (firstX || firstX === 0) { + min = $$.isTimeSeries() ? new Date(firstX.getTime() - padding.left) : firstX - padding.left; + } + if (lastX || lastX === 0) { + max = $$.isTimeSeries() ? new Date(lastX.getTime() + padding.right) : lastX + padding.right; + } + return [min, max]; + }; + c3_chart_internal_fn.updateXDomain = function (targets, withUpdateXDomain, withUpdateOrgXDomain, domain) { + var $$ = this, config = $$.config; + if (withUpdateOrgXDomain) { + $$.x.domain(domain ? domain : $$.d3.extent($$.getXDomain(targets))); + $$.orgXDomain = $$.x.domain(); + if (config.zoom_enabled) { $$.zoom.scale($$.x).updateScaleExtent(); } + $$.subX.domain($$.x.domain()); + if ($$.brush) { $$.brush.scale($$.subX); } + } + if (withUpdateXDomain) { + $$.x.domain(domain ? domain : (!$$.brush || $$.brush.empty()) ? $$.orgXDomain : $$.brush.extent()); + if (config.zoom_enabled) { $$.zoom.scale($$.x).updateScaleExtent(); } + } + return $$.x.domain(); + }; + + c3_chart_internal_fn.isX = function (key) { + var $$ = this, config = $$.config; + return (config.data_x && key === config.data_x) || (notEmpty(config.data_xs) && hasValue(config.data_xs, key)); + }; + c3_chart_internal_fn.isNotX = function (key) { + return !this.isX(key); + }; + c3_chart_internal_fn.getXKey = function (id) { + var $$ = this, config = $$.config; + return config.data_x ? config.data_x : notEmpty(config.data_xs) ? config.data_xs[id] : null; + }; + c3_chart_internal_fn.getXValuesOfXKey = function (key, targets) { + var $$ = this, + xValues, ids = targets && notEmpty(targets) ? $$.mapToIds(targets) : []; + ids.forEach(function (id) { + if ($$.getXKey(id) === key) { + xValues = $$.data.xs[id]; + } + }); + return xValues; + }; + c3_chart_internal_fn.getIndexByX = function (x) { + var $$ = this, + data = $$.filterByX($$.data.targets, x); + return data.length ? data[0].index : null; + }; + c3_chart_internal_fn.getXValue = function (id, i) { + var $$ = this; + return id in $$.data.xs && $$.data.xs[id] && isValue($$.data.xs[id][i]) ? $$.data.xs[id][i] : i; + }; + c3_chart_internal_fn.getOtherTargetXs = function () { + var $$ = this, + idsForX = Object.keys($$.data.xs); + return idsForX.length ? $$.data.xs[idsForX[0]] : null; + }; + c3_chart_internal_fn.getOtherTargetX = function (index) { + var xs = this.getOtherTargetXs(); + return xs && index < xs.length ? xs[index] : null; + }; + c3_chart_internal_fn.addXs = function (xs) { + var $$ = this; + Object.keys(xs).forEach(function (id) { + $$.config.data_xs[id] = xs[id]; + }); + }; + c3_chart_internal_fn.hasMultipleX = function (xs) { + return this.d3.set(Object.keys(xs).map(function (id) { return xs[id]; })).size() > 1; + }; + c3_chart_internal_fn.isMultipleX = function () { + var $$ = this, config = $$.config; + return notEmpty(config.data_xs) && $$.hasMultipleX(config.data_xs); + }; + c3_chart_internal_fn.addName = function (data) { + var $$ = this, name; + if (data) { + name = $$.config.data_names[data.id]; + data.name = name ? name : data.id; + } + return data; + }; + c3_chart_internal_fn.getValueOnIndex = function (values, index) { + var valueOnIndex = values.filter(function (v) { return v.index === index; }); + return valueOnIndex.length ? valueOnIndex[0] : null; + }; + c3_chart_internal_fn.updateTargetX = function (targets, x) { + var $$ = this; + targets.forEach(function (t) { + t.values.forEach(function (v, i) { + v.x = $$.generateTargetX(x[i], t.id, i); + }); + $$.data.xs[t.id] = x; + }); + }; + c3_chart_internal_fn.updateTargetXs = function (targets, xs) { + var $$ = this; + targets.forEach(function (t) { + if (xs[t.id]) { + $$.updateTargetX([t], xs[t.id]); + } + }); + }; + c3_chart_internal_fn.generateTargetX = function (rawX, id, index) { + var $$ = this, x; + if ($$.isTimeSeries()) { + x = rawX ? $$.parseDate(rawX) : $$.parseDate($$.getXValue(id, index)); + } + else if ($$.isCustomX() && !$$.isCategorized()) { + x = isValue(rawX) ? +rawX : $$.getXValue(id, index); + } + else { + x = index; + } + return x; + }; + c3_chart_internal_fn.cloneTarget = function (target) { + return { + id : target.id, + id_org : target.id_org, + values : target.values.map(function (d) { + return {x: d.x, value: d.value, id: d.id}; + }) + }; + }; + c3_chart_internal_fn.getPrevX = function (i) { + var $$ = this, value = $$.getValueOnIndex($$.data.targets[0].values, i - 1); + return value ? value.x : null; + }; + c3_chart_internal_fn.getNextX = function (i) { + var $$ = this, value = $$.getValueOnIndex($$.data.targets[0].values, i + 1); + return value ? value.x : null; + }; + c3_chart_internal_fn.getMaxDataCount = function () { + var $$ = this; + return $$.d3.max($$.data.targets, function (t) { return t.values.length; }); + }; + c3_chart_internal_fn.getMaxDataCountTarget = function (targets) { + var length = targets.length, max = 0, maxTarget; + if (length > 1) { + targets.forEach(function (t) { + if (t.values.length > max) { + maxTarget = t; + max = t.values.length; + } + }); + } else { + maxTarget = length ? targets[0] : null; + } + return maxTarget; + }; + c3_chart_internal_fn.getEdgeX = function (targets) { + var $$ = this; + return !targets.length ? [0, 0] : [ + $$.d3.min(targets, function (t) { return t.values[0].x; }), + $$.d3.max(targets, function (t) { return t.values[t.values.length - 1].x; }) + ]; + }; + c3_chart_internal_fn.mapToIds = function (targets) { + return targets.map(function (d) { return d.id; }); + }; + c3_chart_internal_fn.mapToTargetIds = function (ids) { + var $$ = this; + return ids ? (isString(ids) ? [ids] : ids) : $$.mapToIds($$.data.targets); + }; + c3_chart_internal_fn.hasTarget = function (targets, id) { + var ids = this.mapToIds(targets), i; + for (i = 0; i < ids.length; i++) { + if (ids[i] === id) { + return true; + } + } + return false; + }; + c3_chart_internal_fn.isTargetToShow = function (targetId) { + return this.hiddenTargetIds.indexOf(targetId) < 0; + }; + c3_chart_internal_fn.isLegendToShow = function (targetId) { + return this.hiddenLegendIds.indexOf(targetId) < 0; + }; + c3_chart_internal_fn.filterTargetsToShow = function (targets) { + var $$ = this; + return targets.filter(function (t) { return $$.isTargetToShow(t.id); }); + }; + c3_chart_internal_fn.mapTargetsToUniqueXs = function (targets) { + var $$ = this; + var xs = $$.d3.set($$.d3.merge(targets.map(function (t) { return t.values.map(function (v) { return v.x; }); }))).values(); + return $$.isTimeSeries() ? xs.map(function (x) { return new Date(x); }) : xs.map(function (x) { return +x; }); + }; + c3_chart_internal_fn.addHiddenTargetIds = function (targetIds) { + this.hiddenTargetIds = this.hiddenTargetIds.concat(targetIds); + }; + c3_chart_internal_fn.removeHiddenTargetIds = function (targetIds) { + this.hiddenTargetIds = this.hiddenTargetIds.filter(function (id) { return targetIds.indexOf(id) < 0; }); + }; + c3_chart_internal_fn.addHiddenLegendIds = function (targetIds) { + this.hiddenLegendIds = this.hiddenLegendIds.concat(targetIds); + }; + c3_chart_internal_fn.removeHiddenLegendIds = function (targetIds) { + this.hiddenLegendIds = this.hiddenLegendIds.filter(function (id) { return targetIds.indexOf(id) < 0; }); + }; + c3_chart_internal_fn.getValuesAsIdKeyed = function (targets) { + var ys = {}; + targets.forEach(function (t) { + ys[t.id] = []; + t.values.forEach(function (v) { + ys[t.id].push(v.value); + }); + }); + return ys; + }; + c3_chart_internal_fn.checkValueInTargets = function (targets, checker) { + var ids = Object.keys(targets), i, j, values; + for (i = 0; i < ids.length; i++) { + values = targets[ids[i]].values; + for (j = 0; j < values.length; j++) { + if (checker(values[j].value)) { + return true; + } + } + } + return false; + }; + c3_chart_internal_fn.hasNegativeValueInTargets = function (targets) { + return this.checkValueInTargets(targets, function (v) { return v < 0; }); + }; + c3_chart_internal_fn.hasPositiveValueInTargets = function (targets) { + return this.checkValueInTargets(targets, function (v) { return v > 0; }); + }; + c3_chart_internal_fn.isOrderDesc = function () { + var config = this.config; + return config.data_order && config.data_order.toLowerCase() === 'desc'; + }; + c3_chart_internal_fn.isOrderAsc = function () { + var config = this.config; + return config.data_order && config.data_order.toLowerCase() === 'asc'; + }; + c3_chart_internal_fn.orderTargets = function (targets) { + var $$ = this, config = $$.config, orderAsc = $$.isOrderAsc(), orderDesc = $$.isOrderDesc(); + if (orderAsc || orderDesc) { + targets.sort(function (t1, t2) { + var reducer = function (p, c) { return p + Math.abs(c.value); }; + var t1Sum = t1.values.reduce(reducer, 0), + t2Sum = t2.values.reduce(reducer, 0); + return orderAsc ? t2Sum - t1Sum : t1Sum - t2Sum; + }); + } else if (isFunction(config.data_order)) { + targets.sort(config.data_order); + } // TODO: accept name array for order + return targets; + }; + c3_chart_internal_fn.filterByX = function (targets, x) { + return this.d3.merge(targets.map(function (t) { return t.values; })).filter(function (v) { return v.x - x === 0; }); + }; + c3_chart_internal_fn.filterRemoveNull = function (data) { + return data.filter(function (d) { return isValue(d.value); }); + }; + c3_chart_internal_fn.hasDataLabel = function () { + var config = this.config; + if (typeof config.data_labels === 'boolean' && config.data_labels) { + return true; + } else if (typeof config.data_labels === 'object' && notEmpty(config.data_labels)) { + return true; + } + return false; + }; + c3_chart_internal_fn.getDataLabelLength = function (min, max, axisId, key) { + var $$ = this, + lengths = [0, 0], paddingCoef = 1.3; + $$.selectChart.select('svg').selectAll('.dummy') + .data([min, max]) + .enter().append('text') + .text(function (d) { return $$.formatByAxisId(axisId)(d); }) + .each(function (d, i) { + lengths[i] = this.getBoundingClientRect()[key] * paddingCoef; + }) + .remove(); + return lengths; + }; + c3_chart_internal_fn.isNoneArc = function (d) { + return this.hasTarget(this.data.targets, d.id); + }, + c3_chart_internal_fn.isArc = function (d) { + return 'data' in d && this.hasTarget(this.data.targets, d.data.id); + }; + c3_chart_internal_fn.findSameXOfValues = function (values, index) { + var i, targetX = values[index].x, sames = []; + for (i = index - 1; i >= 0; i--) { + if (targetX !== values[i].x) { break; } + sames.push(values[i]); + } + for (i = index; i < values.length; i++) { + if (targetX !== values[i].x) { break; } + sames.push(values[i]); + } + return sames; + }; + + c3_chart_internal_fn.findClosestOfValues = function (values, pos, _min, _max) { // MEMO: values must be sorted by x + var $$ = this, + min = _min ? _min : 0, + max = _max ? _max : values.length - 1, + med = Math.floor((max - min) / 2) + min, + value = values[med], + diff = $$.x(value.x) - pos[$$.config.axis_rotated ? 1 : 0], + candidates; + + // Update range for search + diff > 0 ? max = med : min = med; + + // if candidates are two closest min and max, stop recursive call + if ((max - min) === 1 || (min === 0 && max === 0)) { + + // Get candidates that has same min and max index + candidates = []; + if (values[min].x || values[min].x === 0) { + candidates = candidates.concat($$.findSameXOfValues(values, min)); + } + if (values[max].x || values[max].x === 0) { + candidates = candidates.concat($$.findSameXOfValues(values, max)); + } + + // Determine the closest and return + return $$.findClosest(candidates, pos); + } + + return $$.findClosestOfValues(values, pos, min, max); + }; + c3_chart_internal_fn.findClosestFromTargets = function (targets, pos) { + var $$ = this, candidates; + + // map to array of closest points of each target + candidates = targets.map(function (target) { + return $$.findClosestOfValues(target.values, pos); + }); + + // decide closest point and return + return $$.findClosest(candidates, pos); + }; + c3_chart_internal_fn.findClosest = function (values, pos) { + var $$ = this, minDist, closest; + values.forEach(function (v) { + var d = $$.dist(v, pos); + if (d < minDist || ! minDist) { + minDist = d; + closest = v; + } + }); + return closest; + }; + c3_chart_internal_fn.dist = function (data, pos) { + var $$ = this, config = $$.config, + yScale = $$.getAxisId(data.id) === 'y' ? $$.y : $$.y2, + xIndex = config.axis_rotated ? 1 : 0, + yIndex = config.axis_rotated ? 0 : 1; + return Math.pow($$.x(data.x) - pos[xIndex], 2) + Math.pow(yScale(data.value) - pos[yIndex], 2); + }; + + c3_chart_internal_fn.convertUrlToData = function (url, mimeType, keys, done) { + var $$ = this, type = mimeType ? mimeType : 'csv'; + $$.d3.xhr(url, function (error, data) { + var d; + if (type === 'json') { + d = $$.convertJsonToData(JSON.parse(data.response), keys); + } else { + d = $$.convertCsvToData(data.response); + } + done.call($$, d); + }); + }; + c3_chart_internal_fn.convertCsvToData = function (csv) { + var d3 = this.d3, rows = d3.csv.parseRows(csv), d; + if (rows.length === 1) { + d = [{}]; + rows[0].forEach(function (id) { + d[0][id] = null; + }); + } else { + d = d3.csv.parse(csv); + } + return d; + }; + c3_chart_internal_fn.convertJsonToData = function (json, keys) { + var $$ = this, + new_rows = [], targetKeys, data; + if (keys) { // when keys specified, json would be an array that includes objects + targetKeys = keys.value; + if (keys.x) { + targetKeys.push(keys.x); + $$.config.data_x = keys.x; + } + new_rows.push(targetKeys); + json.forEach(function (o) { + var new_row = []; + targetKeys.forEach(function (key) { + // convert undefined to null because undefined data will be removed in convertDataToTargets() + var v = isUndefined(o[key]) ? null : o[key]; + new_row.push(v); + }); + new_rows.push(new_row); + }); + data = $$.convertRowsToData(new_rows); + } else { + Object.keys(json).forEach(function (key) { + new_rows.push([key].concat(json[key])); + }); + data = $$.convertColumnsToData(new_rows); + } + return data; + }; + c3_chart_internal_fn.convertRowsToData = function (rows) { + var keys = rows[0], new_row = {}, new_rows = [], i, j; + for (i = 1; i < rows.length; i++) { + new_row = {}; + for (j = 0; j < rows[i].length; j++) { + if (isUndefined(rows[i][j])) { + throw new Error("Source data is missing a component at (" + i + "," + j + ")!"); + } + new_row[keys[j]] = rows[i][j]; + } + new_rows.push(new_row); + } + return new_rows; + }; + c3_chart_internal_fn.convertColumnsToData = function (columns) { + var new_rows = [], i, j, key; + for (i = 0; i < columns.length; i++) { + key = columns[i][0]; + for (j = 1; j < columns[i].length; j++) { + if (isUndefined(new_rows[j - 1])) { + new_rows[j - 1] = {}; + } + if (isUndefined(columns[i][j])) { + throw new Error("Source data is missing a component at (" + i + "," + j + ")!"); + } + new_rows[j - 1][key] = columns[i][j]; + } + } + return new_rows; + }; + c3_chart_internal_fn.convertDataToTargets = function (data, appendXs) { + var $$ = this, config = $$.config, + ids = $$.d3.keys(data[0]).filter($$.isNotX, $$), + xs = $$.d3.keys(data[0]).filter($$.isX, $$), + targets; + + // save x for update data by load when custom x and c3.x API + ids.forEach(function (id) { + var xKey = $$.getXKey(id); + + if ($$.isCustomX() || $$.isTimeSeries()) { + // if included in input data + if (xs.indexOf(xKey) >= 0) { + $$.data.xs[id] = (appendXs && $$.data.xs[id] ? $$.data.xs[id] : []).concat( + data.map(function (d) { return d[xKey]; }) + .filter(isValue) + .map(function (rawX, i) { return $$.generateTargetX(rawX, id, i); }) + ); + } + // if not included in input data, find from preloaded data of other id's x + else if (config.data_x) { + $$.data.xs[id] = $$.getOtherTargetXs(); + } + // if not included in input data, find from preloaded data + else if (notEmpty(config.data_xs)) { + $$.data.xs[id] = $$.getXValuesOfXKey(xKey, $$.data.targets); + } + // MEMO: if no x included, use same x of current will be used + } else { + $$.data.xs[id] = data.map(function (d, i) { return i; }); + } + }); + + + // check x is defined + ids.forEach(function (id) { + if (!$$.data.xs[id]) { + throw new Error('x is not defined for id = "' + id + '".'); + } + }); + + // convert to target + targets = ids.map(function (id, index) { + var convertedId = config.data_idConverter(id); + return { + id: convertedId, + id_org: id, + values: data.map(function (d, i) { + var xKey = $$.getXKey(id), rawX = d[xKey], x = $$.generateTargetX(rawX, id, i); + // use x as categories if custom x and categorized + if ($$.isCustomX() && $$.isCategorized() && index === 0 && rawX) { + if (i === 0) { config.axis_x_categories = []; } + config.axis_x_categories.push(rawX); + } + // mark as x = undefined if value is undefined and filter to remove after mapped + if (isUndefined(d[id]) || $$.data.xs[id].length <= i) { + x = undefined; + } + return {x: x, value: d[id] !== null && !isNaN(d[id]) ? +d[id] : null, id: convertedId}; + }).filter(function (v) { return isDefined(v.x); }) + }; + }); + + // finish targets + targets.forEach(function (t) { + var i; + // sort values by its x + t.values = t.values.sort(function (v1, v2) { + var x1 = v1.x || v1.x === 0 ? v1.x : Infinity, + x2 = v2.x || v2.x === 0 ? v2.x : Infinity; + return x1 - x2; + }); + // indexing each value + i = 0; + t.values.forEach(function (v) { + v.index = i++; + }); + // this needs to be sorted because its index and value.index is identical + $$.data.xs[t.id].sort(function (v1, v2) { + return v1 - v2; + }); + }); + + // set target types + if (config.data_type) { + $$.setTargetType($$.mapToIds(targets).filter(function (id) { return ! (id in config.data_types); }), config.data_type); + } + + // cache as original id keyed + targets.forEach(function (d) { + $$.addCache(d.id_org, d); + }); + + return targets; + }; + + c3_chart_internal_fn.load = function (targets, args) { + var $$ = this; + if (targets) { + // filter loading targets if needed + if (args.filter) { + targets = targets.filter(args.filter); + } + // set type if args.types || args.type specified + if (args.type || args.types) { + targets.forEach(function (t) { + $$.setTargetType(t.id, args.types ? args.types[t.id] : args.type); + }); + } + // Update/Add data + $$.data.targets.forEach(function (d) { + for (var i = 0; i < targets.length; i++) { + if (d.id === targets[i].id) { + d.values = targets[i].values; + targets.splice(i, 1); + break; + } + } + }); + $$.data.targets = $$.data.targets.concat(targets); // add remained + } + + // Set targets + $$.updateTargets($$.data.targets); + + // Redraw with new targets + $$.redraw({withUpdateOrgXDomain: true, withUpdateXDomain: true, withLegend: true}); + + if (args.done) { args.done(); } + }; + c3_chart_internal_fn.loadFromArgs = function (args) { + var $$ = this; + if (args.data) { + $$.load($$.convertDataToTargets(args.data), args); + } + else if (args.url) { + $$.convertUrlToData(args.url, args.mimeType, args.keys, function (data) { + $$.load($$.convertDataToTargets(data), args); + }); + } + else if (args.json) { + $$.load($$.convertDataToTargets($$.convertJsonToData(args.json, args.keys)), args); + } + else if (args.rows) { + $$.load($$.convertDataToTargets($$.convertRowsToData(args.rows)), args); + } + else if (args.columns) { + $$.load($$.convertDataToTargets($$.convertColumnsToData(args.columns)), args); + } + else { + $$.load(null, args); + } + }; + c3_chart_internal_fn.unload = function (targetIds, done) { + var $$ = this; + if (!done) { + done = function () {}; + } + // filter existing target + targetIds = targetIds.filter(function (id) { return $$.hasTarget($$.data.targets, id); }); + // If no target, call done and return + if (!targetIds || targetIds.length === 0) { + done(); + return; + } + $$.svg.selectAll(targetIds.map(function (id) { return $$.selectorTarget(id); })) + .transition() + .style('opacity', 0) + .remove() + .call($$.endall, done); + targetIds.forEach(function (id) { + // Reset fadein for future load + $$.withoutFadeIn[id] = false; + // Remove target's elements + if ($$.legend) { + $$.legend.selectAll('.' + CLASS.legendItem + $$.getTargetSelectorSuffix(id)).remove(); + } + // Remove target + $$.data.targets = $$.data.targets.filter(function (t) { + return t.id !== id; + }); + }); + }; + + c3_chart_internal_fn.categoryName = function (i) { + var config = this.config; + return i < config.axis_x_categories.length ? config.axis_x_categories[i] : i; + }; + + c3_chart_internal_fn.initEventRect = function () { + var $$ = this; + $$.main.select('.' + CLASS.chart).append("g") + .attr("class", CLASS.eventRects) + .style('fill-opacity', 0); + }; + c3_chart_internal_fn.redrawEventRect = function () { + var $$ = this, config = $$.config, + eventRectUpdate, maxDataCountTarget, + isMultipleX = $$.isMultipleX(); + + // rects for mouseover + var eventRects = $$.main.select('.' + CLASS.eventRects) + .style('cursor', config.zoom_enabled ? config.axis_rotated ? 'ns-resize' : 'ew-resize' : null) + .classed(CLASS.eventRectsMultiple, isMultipleX) + .classed(CLASS.eventRectsSingle, !isMultipleX); + + // clear old rects + eventRects.selectAll('.' + CLASS.eventRect).remove(); + + // open as public variable + $$.eventRect = eventRects.selectAll('.' + CLASS.eventRect); + + if (isMultipleX) { + eventRectUpdate = $$.eventRect.data([0]); + // enter : only one rect will be added + $$.generateEventRectsForMultipleXs(eventRectUpdate.enter()); + // update + $$.updateEventRect(eventRectUpdate); + // exit : not needed because always only one rect exists + } + else { + // Set data and update $$.eventRect + maxDataCountTarget = $$.getMaxDataCountTarget($$.data.targets); + eventRects.datum(maxDataCountTarget ? maxDataCountTarget.values : []); + $$.eventRect = eventRects.selectAll('.' + CLASS.eventRect); + eventRectUpdate = $$.eventRect.data(function (d) { return d; }); + // enter + $$.generateEventRectsForSingleX(eventRectUpdate.enter()); + // update + $$.updateEventRect(eventRectUpdate); + // exit + eventRectUpdate.exit().remove(); + } + }; + c3_chart_internal_fn.updateEventRect = function (eventRectUpdate) { + var $$ = this, config = $$.config, + x, y, w, h, rectW, rectX; + + // set update selection if null + eventRectUpdate = eventRectUpdate || $$.eventRect.data(function (d) { return d; }); + + if ($$.isMultipleX()) { + // TODO: rotated not supported yet + x = 0; + y = 0; + w = $$.width; + h = $$.height; + } + else { + if (($$.isCustomX() || $$.isTimeSeries()) && !$$.isCategorized()) { + rectW = function (d) { + var prevX = $$.getPrevX(d.index), nextX = $$.getNextX(d.index), dx = $$.data.xs[d.id][d.index], + w = ($$.x(nextX ? nextX : dx) - $$.x(prevX ? prevX : dx)) / 2; + return w < 0 ? 0 : w; + }; + rectX = function (d) { + var prevX = $$.getPrevX(d.index), dx = $$.data.xs[d.id][d.index]; + return ($$.x(dx) + $$.x(prevX ? prevX : dx)) / 2; + }; + } else { + rectW = $$.getEventRectWidth(); + rectX = function (d) { + return $$.x(d.x) - (rectW / 2); + }; + } + x = config.axis_rotated ? 0 : rectX; + y = config.axis_rotated ? rectX : 0; + w = config.axis_rotated ? $$.width : rectW; + h = config.axis_rotated ? rectW : $$.height; + } + + eventRectUpdate + .attr('class', $$.classEvent.bind($$)) + .attr("x", x) + .attr("y", y) + .attr("width", w) + .attr("height", h); + }; + c3_chart_internal_fn.generateEventRectsForSingleX = function (eventRectEnter) { + var $$ = this, d3 = $$.d3, config = $$.config; + eventRectEnter.append("rect") + .attr("class", $$.classEvent.bind($$)) + .style("cursor", config.data_selection_enabled && config.data_selection_grouped ? "pointer" : null) + .on('mouseover', function (d) { + var index = d.index, selectedData, newData; + + if ($$.dragging) { return; } // do nothing if dragging + if ($$.hasArcType()) { return; } + + selectedData = $$.data.targets.map(function (t) { + return $$.addName($$.getValueOnIndex(t.values, index)); + }); + + // Sort selectedData as names order + newData = []; + Object.keys(config.data_names).forEach(function (id) { + for (var j = 0; j < selectedData.length; j++) { + if (selectedData[j] && selectedData[j].id === id) { + newData.push(selectedData[j]); + selectedData.shift(j); + break; + } + } + }); + selectedData = newData.concat(selectedData); // Add remained + + // Expand shapes for selection + if (config.point_focus_expand_enabled) { $$.expandCircles(index, null, true); } + $$.expandBars(index, null, true); + + // Call event handler + $$.main.selectAll('.' + CLASS.shape + '-' + index).each(function (d) { + config.data_onmouseover.call($$, d); + }); + }) + .on('mouseout', function (d) { + var index = d.index; + if ($$.hasArcType()) { return; } + $$.hideXGridFocus(); + $$.hideTooltip(); + // Undo expanded shapes + $$.unexpandCircles(); + $$.unexpandBars(); + // Call event handler + $$.main.selectAll('.' + CLASS.shape + '-' + index).each(function (d) { + config.data_onmouseout.call($$, d); + }); + }) + .on('mousemove', function (d) { + var selectedData, index = d.index, + eventRect = $$.svg.select('.' + CLASS.eventRect + '-' + index); + + if ($$.dragging) { return; } // do nothing when dragging + if ($$.hasArcType()) { return; } + + if ($$.isStepType(d) && d3.mouse(this)[0] < $$.x($$.getXValue(d.id, index))) { + index -= 1; + } + + // Show tooltip + selectedData = $$.filterTargetsToShow($$.data.targets).map(function (t) { + return $$.addName($$.getValueOnIndex(t.values, index)); + }); + + if (config.tooltip_grouped) { + $$.showTooltip(selectedData, d3.mouse(this)); + $$.showXGridFocus(selectedData); + } + + if (config.tooltip_grouped && (!config.data_selection_enabled || config.data_selection_grouped)) { + return; + } + + $$.main.selectAll('.' + CLASS.shape + '-' + index) + .each(function () { + d3.select(this).classed(CLASS.EXPANDED, true); + if (config.data_selection_enabled) { + eventRect.style('cursor', config.data_selection_grouped ? 'pointer' : null); + } + if (!config.tooltip_grouped) { + $$.hideXGridFocus(); + $$.hideTooltip(); + if (!config.data_selection_grouped) { + $$.unexpandCircles(index); + $$.unexpandBars(index); + } + } + }) + .filter(function (d) { + if (this.nodeName === 'circle') { + return $$.isWithinCircle(this, $$.pointSelectR(d)); + } + else if (this.nodeName === 'path') { + return $$.isWithinBar(this); + } + }) + .each(function (d) { + if (config.data_selection_enabled && (config.data_selection_grouped || config.data_selection_isselectable(d))) { + eventRect.style('cursor', 'pointer'); + } + if (!config.tooltip_grouped) { + $$.showTooltip([d], d3.mouse(this)); + $$.showXGridFocus([d]); + if (config.point_focus_expand_enabled) { $$.expandCircles(index, d.id, true); } + $$.expandBars(index, d.id, true); + } + }); + }) + .on('click', function (d) { + var index = d.index; + if ($$.hasArcType() || !$$.toggleShape) { return; } + if ($$.cancelClick) { + $$.cancelClick = false; + return; + } + if ($$.isStepType(d) && d3.mouse(this)[0] < $$.x($$.getXValue(d.id, index))) { + index -= 1; + } + $$.main.selectAll('.' + CLASS.shape + '-' + index).each(function (d) { + $$.toggleShape(this, d, index); + }); + }) + .call( + d3.behavior.drag().origin(Object) + .on('drag', function () { $$.drag(d3.mouse(this)); }) + .on('dragstart', function () { $$.dragstart(d3.mouse(this)); }) + .on('dragend', function () { $$.dragend(); }) + ) + .on("dblclick.zoom", null); + }; + + c3_chart_internal_fn.generateEventRectsForMultipleXs = function (eventRectEnter) { + var $$ = this, d3 = $$.d3, config = $$.config; + eventRectEnter.append('rect') + .attr('x', 0) + .attr('y', 0) + .attr('width', $$.width) + .attr('height', $$.height) + .attr('class', CLASS.eventRect) + .on('mouseout', function () { + if ($$.hasArcType()) { return; } + $$.hideXGridFocus(); + $$.hideTooltip(); + $$.unexpandCircles(); + }) + .on('mousemove', function () { + var targetsToShow = $$.filterTargetsToShow($$.data.targets); + var mouse, closest, sameXData, selectedData; + + if ($$.dragging) { return; } // do nothing when dragging + if ($$.hasArcType(targetsToShow)) { return; } + + mouse = d3.mouse(this); + closest = $$.findClosestFromTargets(targetsToShow, mouse); + + if (! closest) { return; } + + if ($$.isScatterType(closest)) { + sameXData = [closest]; + } else { + sameXData = $$.filterByX(targetsToShow, closest.x); + } + + // show tooltip when cursor is close to some point + selectedData = sameXData.map(function (d) { + return $$.addName(d); + }); + $$.showTooltip(selectedData, mouse); + + // expand points + if (config.point_focus_expand_enabled) { + $$.expandCircles(closest.index, closest.id, true); + } + + // Show xgrid focus line + $$.showXGridFocus(selectedData); + + // Show cursor as pointer if point is close to mouse position + if ($$.dist(closest, mouse) < 100) { + $$.svg.select('.' + CLASS.eventRect).style('cursor', 'pointer'); + if (!$$.mouseover) { + config.data_onmouseover.call($$, closest); + $$.mouseover = true; + } + } else if ($$.mouseover) { + $$.svg.select('.' + CLASS.eventRect).style('cursor', null); + config.data_onmouseout.call($$, closest); + $$.mouseover = false; + } + }) + .on('click', function () { + var targetsToShow = $$.filterTargetsToShow($$.data.targets); + var mouse, closest; + + if ($$.hasArcType(targetsToShow)) { return; } + + mouse = d3.mouse(this); + closest = $$.findClosestFromTargets(targetsToShow, mouse); + + if (! closest) { return; } + + // select if selection enabled + if ($$.dist(closest, mouse) < 100 && $$.toggleShape) { + $$.main.select('.' + CLASS.circles + $$.getTargetSelectorSuffix(closest.id)).select('.' + CLASS.circle + '-' + closest.index).each(function () { + $$.toggleShape(this, closest, closest.index); + }); + } + }) + .call( + d3.behavior.drag().origin(Object) + .on('drag', function () { $$.drag(d3.mouse(this)); }) + .on('dragstart', function () { $$.dragstart(d3.mouse(this)); }) + .on('dragend', function () { $$.dragend(); }) + ) + .on("dblclick.zoom", null); + }; + c3_chart_internal_fn.dispatchEvent = function (type, index, mouse) { + var $$ = this, + selector = '.' + CLASS.eventRect + (!$$.isMultipleX() ? '-' + index : ''), + eventRect = $$.main.select(selector).node(), + box = eventRect.getBoundingClientRect(), + x = box.left + (mouse ? mouse[0] : 0), + y = box.top + (mouse ? mouse[1] : 0), + event = document.createEvent("MouseEvents"); + + event.initMouseEvent(type, true, true, window, 0, x, y, x, y, + false, false, false, false, 0, null); + eventRect.dispatchEvent(event); + }; + + c3_chart_internal_fn.getCurrentWidth = function () { + var $$ = this, config = $$.config; + return config.size_width ? config.size_width : $$.getParentWidth(); + }; + c3_chart_internal_fn.getCurrentHeight = function () { + var $$ = this, config = $$.config, + h = config.size_height ? config.size_height : $$.getParentHeight(); + return h > 0 ? h : 320 / ($$.hasType('gauge') ? 2 : 1); + }; + c3_chart_internal_fn.getCurrentPaddingTop = function () { + var config = this.config; + return isValue(config.padding_top) ? config.padding_top : 0; + }; + c3_chart_internal_fn.getCurrentPaddingBottom = function () { + var config = this.config; + return isValue(config.padding_bottom) ? config.padding_bottom : 0; + }; + c3_chart_internal_fn.getCurrentPaddingLeft = function () { + var $$ = this, config = $$.config; + if (isValue(config.padding_left)) { + return config.padding_left; + } else if (config.axis_rotated) { + return !config.axis_x_show ? 1 : Math.max(ceil10($$.getAxisWidthByAxisId('x')), 40); + } else { + return !config.axis_y_show ? 1 : ceil10($$.getAxisWidthByAxisId('y')); + } + }; + c3_chart_internal_fn.getCurrentPaddingRight = function () { + var $$ = this, config = $$.config, + defaultPadding = 10, legendWidthOnRight = $$.isLegendRight ? $$.getLegendWidth() + 20 : 0; + if (isValue(config.padding_right)) { + return config.padding_right + 1; // 1 is needed not to hide tick line + } else if (config.axis_rotated) { + return defaultPadding + legendWidthOnRight; + } else { + return (!config.axis_y2_show ? defaultPadding : ceil10($$.getAxisWidthByAxisId('y2'))) + legendWidthOnRight; + } + }; + + c3_chart_internal_fn.getParentRectValue = function (key) { + var parent = this.selectChart.node(), v; + while (parent && parent.tagName !== 'BODY') { + v = parent.getBoundingClientRect()[key]; + if (v) { + break; + } + parent = parent.parentNode; + } + return v; + }; + c3_chart_internal_fn.getParentWidth = function () { + return this.getParentRectValue('width'); + }; + c3_chart_internal_fn.getParentHeight = function () { + var h = this.selectChart.style('height'); + return h.indexOf('px') > 0 ? +h.replace('px', '') : 0; + }; + + + c3_chart_internal_fn.getSvgLeft = function () { + var $$ = this, config = $$.config, + leftAxisClass = config.axis_rotated ? CLASS.axisX : CLASS.axisY, + leftAxis = $$.main.select('.' + leftAxisClass).node(), + svgRect = leftAxis ? leftAxis.getBoundingClientRect() : {right: 0}, + chartRect = $$.selectChart.node().getBoundingClientRect(), + hasArc = $$.hasArcType(), + svgLeft = svgRect.right - chartRect.left - (hasArc ? 0 : $$.getCurrentPaddingLeft()); + return svgLeft > 0 ? svgLeft : 0; + }; + + + c3_chart_internal_fn.getAxisWidthByAxisId = function (id) { + var $$ = this, position = $$.getAxisLabelPositionById(id); + return position.isInner ? 20 + $$.getMaxTickWidth(id) : 40 + $$.getMaxTickWidth(id); + }; + c3_chart_internal_fn.getHorizontalAxisHeight = function (axisId) { + var $$ = this, config = $$.config; + if (axisId === 'x' && !config.axis_x_show) { return 0; } + if (axisId === 'x' && config.axis_x_height) { return config.axis_x_height; } + if (axisId === 'y' && !config.axis_y_show) { return config.legend_show && !$$.isLegendRight && !$$.isLegendInset ? 10 : 1; } + if (axisId === 'y2' && !config.axis_y2_show) { return $$.rotated_padding_top; } + return ($$.getAxisLabelPositionById(axisId).isInner ? 30 : 40) + (axisId === 'y2' ? -10 : 0); + }; + + c3_chart_internal_fn.getEventRectWidth = function () { + var $$ = this; + var target = $$.getMaxDataCountTarget($$.data.targets), + firstData, lastData, base, maxDataCount, ratio, w; + if (!target) { + return 0; + } + firstData = target.values[0], lastData = target.values[target.values.length - 1]; + base = $$.x(lastData.x) - $$.x(firstData.x); + if (base === 0) { + return $$.config.axis_rotated ? $$.height : $$.width; + } + maxDataCount = $$.getMaxDataCount(); + ratio = ($$.hasType('bar') ? (maxDataCount - ($$.isCategorized() ? 0.25 : 1)) / maxDataCount : 1); + w = maxDataCount > 1 ? (base * ratio) / (maxDataCount - 1) : base; + return w < 1 ? 1 : w; + }; + + c3_chart_internal_fn.getShapeIndices = function (typeFilter) { + var $$ = this, config = $$.config, + indices = {}, i = 0, j, k; + $$.filterTargetsToShow($$.data.targets.filter(typeFilter, $$)).forEach(function (d) { + for (j = 0; j < config.data_groups.length; j++) { + if (config.data_groups[j].indexOf(d.id) < 0) { continue; } + for (k = 0; k < config.data_groups[j].length; k++) { + if (config.data_groups[j][k] in indices) { + indices[d.id] = indices[config.data_groups[j][k]]; + break; + } + } + } + if (isUndefined(indices[d.id])) { indices[d.id] = i++; } + }); + indices.__max__ = i - 1; + return indices; + }; + c3_chart_internal_fn.getShapeX = function (offset, targetsNum, indices, isSub) { + var $$ = this, scale = isSub ? $$.subX : $$.x; + return function (d) { + var index = d.id in indices ? indices[d.id] : 0; + return d.x || d.x === 0 ? scale(d.x) - offset * (targetsNum / 2 - index) : 0; + }; + }; + c3_chart_internal_fn.getShapeY = function (isSub) { + var $$ = this; + return function (d) { + var scale = isSub ? $$.getSubYScale(d.id) : $$.getYScale(d.id); + return scale(d.value); + }; + }; + c3_chart_internal_fn.getShapeOffset = function (typeFilter, indices, isSub) { + var $$ = this, + targets = $$.orderTargets($$.filterTargetsToShow($$.data.targets.filter(typeFilter, $$))), + targetIds = targets.map(function (t) { return t.id; }); + return function (d, i) { + var scale = isSub ? $$.getSubYScale(d.id) : $$.getYScale(d.id), + y0 = scale(0), offset = y0; + targets.forEach(function (t) { + if (t.id === d.id || indices[t.id] !== indices[d.id]) { return; } + if (targetIds.indexOf(t.id) < targetIds.indexOf(d.id) && t.values[i].value * d.value >= 0) { + offset += scale(t.values[i].value) - y0; + } + }); + return offset; + }; + }; + + c3_chart_internal_fn.getInterpolate = function (d) { + var $$ = this; + return $$.isSplineType(d) ? "cardinal" : $$.isStepType(d) ? "step-after" : "linear"; + }; + + c3_chart_internal_fn.initLine = function () { + var $$ = this; + $$.main.select('.' + CLASS.chart).append("g") + .attr("class", CLASS.chartLines); + }; + c3_chart_internal_fn.updateTargetsForLine = function (targets) { + var $$ = this, config = $$.config, + mainLineUpdate, mainLineEnter, + classChartLine = $$.classChartLine.bind($$), + classLines = $$.classLines.bind($$), + classAreas = $$.classAreas.bind($$), + classCircles = $$.classCircles.bind($$); + mainLineUpdate = $$.main.select('.' + CLASS.chartLines).selectAll('.' + CLASS.chartLine) + .data(targets) + .attr('class', classChartLine); + mainLineEnter = mainLineUpdate.enter().append('g') + .attr('class', classChartLine) + .style('opacity', 0) + .style("pointer-events", "none"); + // Lines for each data + mainLineEnter.append('g') + .attr("class", classLines); + // Areas + mainLineEnter.append('g') + .attr('class', classAreas); + // Circles for each data point on lines + mainLineEnter.append('g') + .attr("class", function (d) { return $$.generateClass(CLASS.selectedCircles, d.id); }); + mainLineEnter.append('g') + .attr("class", classCircles) + .style("cursor", function (d) { return config.data_selection_isselectable(d) ? "pointer" : null; }); + // Update date for selected circles + targets.forEach(function (t) { + $$.main.selectAll('.' + CLASS.selectedCircles + $$.getTargetSelectorSuffix(t.id)).selectAll('.' + CLASS.selectedCircle).each(function (d) { + d.value = t.values[d.index].value; + }); + }); + // MEMO: can not keep same color... + //mainLineUpdate.exit().remove(); + }; + c3_chart_internal_fn.redrawLine = function (durationForExit) { + var $$ = this; + $$.mainLine = $$.main.selectAll('.' + CLASS.lines).selectAll('.' + CLASS.line) + .data($$.lineData.bind($$)); + $$.mainLine.enter().append('path') + .attr('class', $$.classLine.bind($$)) + .style("stroke", $$.color); + $$.mainLine + .style("opacity", $$.initialOpacity.bind($$)) + .attr('transform', null); + $$.mainLine.exit().transition().duration(durationForExit) + .style('opacity', 0) + .remove(); + }; + c3_chart_internal_fn.addTransitionForLine = function (transitions, drawLine) { + var $$ = this; + transitions.push($$.mainLine.transition() + .attr("d", drawLine) + .style("stroke", $$.color) + .style("opacity", 1)); + }; + c3_chart_internal_fn.generateDrawLine = function (lineIndices, isSub) { + var $$ = this, config = $$.config, + line = $$.d3.svg.line(), + getPoints = $$.generateGetLinePoints(lineIndices, isSub), + yScaleGetter = isSub ? $$.getSubYScale : $$.getYScale, + xValue = function (d) { return (isSub ? $$.subxx : $$.xx).call($$, d); }, + yValue = function (d, i) { + return config.data_groups.length > 0 ? getPoints(d, i)[0][1] : yScaleGetter.call($$, d.id)(d.value); + }; + + line = config.axis_rotated ? line.x(yValue).y(xValue) : line.x(xValue).y(yValue); + if (!config.line_connect_null) { line = line.defined(function (d) { return d.value != null; }); } + return function (d) { + var data = config.line_connect_null ? $$.filterRemoveNull(d.values) : d.values, + x = isSub ? $$.x : $$.subX, y = yScaleGetter.call($$, d.id), x0 = 0, y0 = 0, path; + if ($$.isLineType(d)) { + if (config.data_regions[d.id]) { + path = $$.lineWithRegions(data, x, y, config.data_regions[d.id]); + } else { + path = line.interpolate($$.getInterpolate(d))(data); + } + } else { + if (data[0]) { + x0 = x(data[0].x); + y0 = y(data[0].value); + } + path = config.axis_rotated ? "M " + y0 + " " + x0 : "M " + x0 + " " + y0; + } + return path ? path : "M 0 0"; + }; + }; + c3_chart_internal_fn.generateGetLinePoints = function (lineIndices, isSub) { // partial duplication of generateGetBarPoints + var $$ = this, config = $$.config, + lineTargetsNum = lineIndices.__max__ + 1, + x = $$.getShapeX(0, lineTargetsNum, lineIndices, !!isSub), + y = $$.getShapeY(!!isSub), + lineOffset = $$.getShapeOffset($$.isLineType, lineIndices, !!isSub), + yScale = isSub ? $$.getSubYScale : $$.getYScale; + return function (d, i) { + var y0 = yScale.call($$, d.id)(0), + offset = lineOffset(d, i) || y0, // offset is for stacked area chart + posX = x(d), posY = y(d); + // fix posY not to overflow opposite quadrant + if (config.axis_rotated) { + if ((0 < d.value && posY < y0) || (d.value < 0 && y0 < posY)) { posY = y0; } + } + // 1 point that marks the line position + return [ + [posX, posY - (y0 - offset)], + [posX, posY - (y0 - offset)], // needed for compatibility + [posX, posY - (y0 - offset)], // needed for compatibility + [posX, posY - (y0 - offset)] // needed for compatibility + ]; + }; + }; + + + c3_chart_internal_fn.lineWithRegions = function (d, x, y, _regions) { + var $$ = this, config = $$.config, + prev = -1, i, j, + s = "M", sWithRegion, + xp, yp, dx, dy, dd, diff, diffx2, + xValue, yValue, + regions = []; + + function isWithinRegions(x, regions) { + var i; + for (i = 0; i < regions.length; i++) { + if (regions[i].start < x && x <= regions[i].end) { return true; } + } + return false; + } + + // Check start/end of regions + if (isDefined(_regions)) { + for (i = 0; i < _regions.length; i++) { + regions[i] = {}; + if (isUndefined(_regions[i].start)) { + regions[i].start = d[0].x; + } else { + regions[i].start = $$.isTimeSeries() ? $$.parseDate(_regions[i].start) : _regions[i].start; + } + if (isUndefined(_regions[i].end)) { + regions[i].end = d[d.length - 1].x; + } else { + regions[i].end = $$.isTimeSeries() ? $$.parseDate(_regions[i].end) : _regions[i].end; + } + } + } + + // Set scales + xValue = config.axis_rotated ? function (d) { return y(d.value); } : function (d) { return x(d.x); }; + yValue = config.axis_rotated ? function (d) { return x(d.x); } : function (d) { return y(d.value); }; + + // Define svg generator function for region + if ($$.isTimeSeries()) { + sWithRegion = function (d0, d1, j, diff) { + var x0 = d0.x.getTime(), x_diff = d1.x - d0.x, + xv0 = new Date(x0 + x_diff * j), + xv1 = new Date(x0 + x_diff * (j + diff)); + return "M" + x(xv0) + " " + y(yp(j)) + " " + x(xv1) + " " + y(yp(j + diff)); + }; + } else { + sWithRegion = function (d0, d1, j, diff) { + return "M" + x(xp(j), true) + " " + y(yp(j)) + " " + x(xp(j + diff), true) + " " + y(yp(j + diff)); + }; + } + + // Generate + for (i = 0; i < d.length; i++) { + + // Draw as normal + if (isUndefined(regions) || ! isWithinRegions(d[i].x, regions)) { + s += " " + xValue(d[i]) + " " + yValue(d[i]); + } + // Draw with region // TODO: Fix for horizotal charts + else { + xp = $$.getScale(d[i - 1].x, d[i].x, $$.isTimeSeries()); + yp = $$.getScale(d[i - 1].value, d[i].value); + + dx = x(d[i].x) - x(d[i - 1].x); + dy = y(d[i].value) - y(d[i - 1].value); + dd = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)); + diff = 2 / dd; + diffx2 = diff * 2; + + for (j = diff; j <= 1; j += diffx2) { + s += sWithRegion(d[i - 1], d[i], j, diff); + } + } + prev = d[i].x; + } + + return s; + }; + + + c3_chart_internal_fn.redrawArea = function (durationForExit) { + var $$ = this, d3 = $$.d3; + $$.mainArea = $$.main.selectAll('.' + CLASS.areas).selectAll('.' + CLASS.area) + .data($$.lineData.bind($$)); + $$.mainArea.enter().append('path') + .attr("class", $$.classArea.bind($$)) + .style("fill", $$.color) + .style("opacity", function () { $$.orgAreaOpacity = +d3.select(this).style('opacity'); return 0; }); + $$.mainArea + .style("opacity", $$.orgAreaOpacity); + $$.mainArea.exit().transition().duration(durationForExit) + .style('opacity', 0) + .remove(); + }; + c3_chart_internal_fn.addTransitionForArea = function (transitions, drawArea) { + var $$ = this; + transitions.push($$.mainArea.transition() + .attr("d", drawArea) + .style("fill", $$.color) + .style("opacity", $$.orgAreaOpacity)); + }; + c3_chart_internal_fn.generateDrawArea = function (areaIndices, isSub) { + var $$ = this, config = $$.config, area = $$.d3.svg.area(), + getPoints = $$.generateGetAreaPoints(areaIndices, isSub), + yScaleGetter = isSub ? $$.getSubYScale : $$.getYScale, + xValue = function (d) { return (isSub ? $$.subxx : $$.xx).call($$, d); }, + value0 = function (d, i) { + return config.data_groups.length > 0 ? getPoints(d, i)[0][1] : yScaleGetter.call($$, d.id)(0); + }, + value1 = function (d, i) { + return config.data_groups.length > 0 ? getPoints(d, i)[1][1] : yScaleGetter.call($$, d.id)(d.value); + }; + + area = config.axis_rotated ? area.x0(value0).x1(value1).y(xValue) : area.x(xValue).y0(value0).y1(value1); + if (!config.line_connect_null) { + area = area.defined(function (d) { return d.value !== null; }); + } + + return function (d) { + var data = config.line_connect_null ? $$.filterRemoveNull(d.values) : d.values, x0 = 0, y0 = 0, path; + if ($$.isAreaType(d)) { + path = area.interpolate($$.getInterpolate(d))(data); + } else { + if (data[0]) { + x0 = $$.x(data[0].x); + y0 = $$.getYScale(d.id)(data[0].value); + } + path = config.axis_rotated ? "M " + y0 + " " + x0 : "M " + x0 + " " + y0; + } + return path ? path : "M 0 0"; + }; + }; + + c3_chart_internal_fn.generateGetAreaPoints = function (areaIndices, isSub) { // partial duplication of generateGetBarPoints + var $$ = this, config = $$.config, + areaTargetsNum = areaIndices.__max__ + 1, + x = $$.getShapeX(0, areaTargetsNum, areaIndices, !!isSub), + y = $$.getShapeY(!!isSub), + areaOffset = $$.getShapeOffset($$.isAreaType, areaIndices, !!isSub), + yScale = isSub ? $$.getSubYScale : $$.getYScale; + return function (d, i) { + var y0 = yScale.call($$, d.id)(0), + offset = areaOffset(d, i) || y0, // offset is for stacked area chart + posX = x(d), posY = y(d); + // fix posY not to overflow opposite quadrant + if (config.axis_rotated) { + if ((0 < d.value && posY < y0) || (d.value < 0 && y0 < posY)) { posY = y0; } + } + // 1 point that marks the area position + return [ + [posX, offset], + [posX, posY - (y0 - offset)], + [posX, posY - (y0 - offset)], // needed for compatibility + [posX, offset] // needed for compatibility + ]; + }; + }; + + + c3_chart_internal_fn.redrawCircle = function () { + var $$ = this; + $$.mainCircle = $$.main.selectAll('.' + CLASS.circles).selectAll('.' + CLASS.circle) + .data($$.lineOrScatterData.bind($$)); + $$.mainCircle.enter().append("circle") + .attr("class", $$.classCircle.bind($$)) + .attr("r", $$.pointR.bind($$)) + .style("fill", $$.color); + $$.mainCircle + .style("opacity", $$.initialOpacity.bind($$)); + $$.mainCircle.exit().remove(); + }; + c3_chart_internal_fn.addTransitionForCircle = function (transitions, cx, cy) { + var $$ = this; + transitions.push($$.mainCircle.transition() + .style('opacity', $$.opacityForCircle.bind($$)) + .style("fill", $$.color) + .attr("cx", cx) + .attr("cy", cy)); + transitions.push($$.main.selectAll('.' + CLASS.selectedCircle).transition() + .attr("cx", cx) + .attr("cy", cy)); + }; + c3_chart_internal_fn.circleX = function (d) { + return d.x || d.x === 0 ? this.x(d.x) : null; + }; + c3_chart_internal_fn.circleY = function (d, i) { + var $$ = this, + lineIndices = $$.getShapeIndices($$.isLineType), getPoints = $$.generateGetLinePoints(lineIndices); + return $$.config.data_groups.length > 0 ? getPoints(d, i)[0][1] : $$.getYScale(d.id)(d.value); + }; + c3_chart_internal_fn.getCircles = function (i, id) { + var $$ = this; + return (id ? $$.main.selectAll('.' + CLASS.circles + $$.getTargetSelectorSuffix(id)) : $$.main).selectAll('.' + CLASS.circle + (isValue(i) ? '-' + i : '')); + }; + c3_chart_internal_fn.expandCircles = function (i, id, reset) { + var $$ = this, + r = $$.pointExpandedR.bind($$); + if (reset) { $$.unexpandCircles(); } + $$.getCircles(i, id) + .classed(CLASS.EXPANDED, true) + .attr('r', r); + }; + c3_chart_internal_fn.unexpandCircles = function (i) { + var $$ = this, + r = $$.pointR.bind($$); + $$.getCircles(i) + .filter(function () { return $$.d3.select(this).classed(CLASS.EXPANDED); }) + .classed(CLASS.EXPANDED, false) + .attr('r', r); + }; + c3_chart_internal_fn.pointR = function (d) { + var $$ = this, config = $$.config; + return config.point_show && !$$.isStepType(d) ? (isFunction(config.point_r) ? config.point_r(d) : config.point_r) : 0; + }; + c3_chart_internal_fn.pointExpandedR = function (d) { + var $$ = this, config = $$.config; + return config.point_focus_expand_enabled ? (config.point_focus_expand_r ? config.point_focus_expand_r : $$.pointR(d) * 1.75) : $$.pointR(d); + }; + c3_chart_internal_fn.pointSelectR = function (d) { + var $$ = this, config = $$.config; + return config.point_select_r ? config.point_select_r : $$.pointR(d) * 4; + }; + c3_chart_internal_fn.isWithinCircle = function (_this, _r) { + var d3 = this.d3, + mouse = d3.mouse(_this), d3_this = d3.select(_this), + cx = d3_this.attr("cx") * 1, cy = d3_this.attr("cy") * 1; + return Math.sqrt(Math.pow(cx - mouse[0], 2) + Math.pow(cy - mouse[1], 2)) < _r; + }; + + c3_chart_internal_fn.initBar = function () { + var $$ = this; + $$.main.select('.' + CLASS.chart).append("g") + .attr("class", CLASS.chartBars); + }; + c3_chart_internal_fn.updateTargetsForBar = function (targets) { + var $$ = this, config = $$.config, + mainBarUpdate, mainBarEnter, + classChartBar = $$.classChartBar.bind($$), + classBars = $$.classBars.bind($$); + mainBarUpdate = $$.main.select('.' + CLASS.chartBars).selectAll('.' + CLASS.chartBar) + .data(targets) + .attr('class', classChartBar); + mainBarEnter = mainBarUpdate.enter().append('g') + .attr('class', classChartBar) + .style('opacity', 0) + .style("pointer-events", "none"); + // Bars for each data + mainBarEnter.append('g') + .attr("class", classBars) + .style("cursor", function (d) { return config.data_selection_isselectable(d) ? "pointer" : null; }); + + }; + c3_chart_internal_fn.redrawBar = function (durationForExit) { + var $$ = this, + barData = $$.barData.bind($$), + classBar = $$.classBar.bind($$), + initialOpacity = $$.initialOpacity.bind($$), + color = function (d) { return $$.color(d.id); }; + $$.mainBar = $$.main.selectAll('.' + CLASS.bars).selectAll('.' + CLASS.bar) + .data(barData); + $$.mainBar.enter().append('path') + .attr("class", classBar) + .style("stroke", color) + .style("fill", color); + $$.mainBar + .style("opacity", initialOpacity); + $$.mainBar.exit().transition().duration(durationForExit) + .style('opacity', 0) + .remove(); + }; + c3_chart_internal_fn.addTransitionForBar = function (transitions, drawBar) { + var $$ = this; + transitions.push($$.mainBar.transition() + .attr('d', drawBar) + .style("fill", $$.color) + .style("opacity", 1)); + }; + c3_chart_internal_fn.getBarW = function (axis, barTargetsNum) { + var $$ = this, config = $$.config, + w = typeof config.bar_width === 'number' ? config.bar_width : barTargetsNum ? (axis.tickOffset() * 2 * config.bar_width_ratio) / barTargetsNum : 0; + return config.bar_width_max && w > config.bar_width_max ? config.bar_width_max : w; + }; + c3_chart_internal_fn.getBars = function (i) { + var $$ = this; + return $$.main.selectAll('.' + CLASS.bar + (isValue(i) ? '-' + i : '')); + }; + c3_chart_internal_fn.expandBars = function (i, id, reset) { + var $$ = this; + if (reset) { $$.unexpandBars(); } + $$.getBars(i).classed(CLASS.EXPANDED, true); + }; + c3_chart_internal_fn.unexpandBars = function (i) { + var $$ = this; + $$.getBars(i).classed(CLASS.EXPANDED, false); + }; + c3_chart_internal_fn.generateDrawBar = function (barIndices, isSub) { + var $$ = this, config = $$.config, + getPoints = $$.generateGetBarPoints(barIndices, isSub); + return function (d, i) { + // 4 points that make a bar + var points = getPoints(d, i); + + // switch points if axis is rotated, not applicable for sub chart + var indexX = config.axis_rotated ? 1 : 0; + var indexY = config.axis_rotated ? 0 : 1; + + var path = 'M ' + points[0][indexX] + ',' + points[0][indexY] + ' ' + + 'L' + points[1][indexX] + ',' + points[1][indexY] + ' ' + + 'L' + points[2][indexX] + ',' + points[2][indexY] + ' ' + + 'L' + points[3][indexX] + ',' + points[3][indexY] + ' ' + + 'z'; + + return path; + }; + }; + c3_chart_internal_fn.generateGetBarPoints = function (barIndices, isSub) { + var $$ = this, + barTargetsNum = barIndices.__max__ + 1, + barW = $$.getBarW($$.xAxis, barTargetsNum), + barX = $$.getShapeX(barW, barTargetsNum, barIndices, !!isSub), + barY = $$.getShapeY(!!isSub), + barOffset = $$.getShapeOffset($$.isBarType, barIndices, !!isSub), + yScale = isSub ? $$.getSubYScale : $$.getYScale; + return function (d, i) { + var y0 = yScale.call($$, d.id)(0), + offset = barOffset(d, i) || y0, // offset is for stacked bar chart + posX = barX(d), posY = barY(d); + // fix posY not to overflow opposite quadrant + if ($$.config.axis_rotated) { + if ((0 < d.value && posY < y0) || (d.value < 0 && y0 < posY)) { posY = y0; } + } + // 4 points that make a bar + return [ + [posX, offset], + [posX, posY - (y0 - offset)], + [posX + barW, posY - (y0 - offset)], + [posX + barW, offset] + ]; + }; + }; + c3_chart_internal_fn.isWithinBar = function (_this) { + var d3 = this.d3, + mouse = d3.mouse(_this), box = _this.getBoundingClientRect(), + seg0 = _this.pathSegList.getItem(0), seg1 = _this.pathSegList.getItem(1), + x = seg0.x, y = Math.min(seg0.y, seg1.y), w = box.width, h = box.height, offset = 2, + sx = x - offset, ex = x + w + offset, sy = y + h + offset, ey = y - offset; + return sx < mouse[0] && mouse[0] < ex && ey < mouse[1] && mouse[1] < sy; + }; + + c3_chart_internal_fn.initText = function () { + var $$ = this; + $$.main.select('.' + CLASS.chart).append("g") + .attr("class", CLASS.chartTexts); + $$.mainText = $$.d3.selectAll([]); + }; + c3_chart_internal_fn.updateTargetsForText = function (targets) { + var $$ = this, mainTextUpdate, mainTextEnter, + classChartText = $$.classChartText.bind($$), + classTexts = $$.classTexts.bind($$); + mainTextUpdate = $$.main.select('.' + CLASS.chartTexts).selectAll('.' + CLASS.chartText) + .data(targets) + .attr('class', classChartText); + mainTextEnter = mainTextUpdate.enter().append('g') + .attr('class', classChartText) + .style('opacity', 0) + .style("pointer-events", "none"); + mainTextEnter.append('g') + .attr('class', classTexts); + }; + c3_chart_internal_fn.redrawText = function (durationForExit) { + var $$ = this, config = $$.config, + barOrLineData = $$.barOrLineData.bind($$), + classText = $$.classText.bind($$); + $$.mainText = $$.main.selectAll('.' + CLASS.texts).selectAll('.' + CLASS.text) + .data(barOrLineData); + $$.mainText.enter().append('text') + .attr("class", classText) + .attr('text-anchor', function (d) { return config.axis_rotated ? (d.value < 0 ? 'end' : 'start') : 'middle'; }) + .style("stroke", 'none') + .style("fill", function (d) { return $$.color(d); }) + .style("fill-opacity", 0); + $$.mainText + .text(function (d) { return $$.formatByAxisId($$.getAxisId(d.id))(d.value, d.id); }); + $$.mainText.exit() + .transition().duration(durationForExit) + .style('fill-opacity', 0) + .remove(); + }; + c3_chart_internal_fn.addTransitionForText = function (transitions, xForText, yForText, forFlow) { + var $$ = this, + opacityForText = forFlow ? 0 : $$.opacityForText.bind($$); + transitions.push($$.mainText.transition() + .attr('x', xForText) + .attr('y', yForText) + .style("fill", $$.color) + .style("fill-opacity", opacityForText)); + }; + c3_chart_internal_fn.getTextRect = function (text, cls) { + var rect; + this.d3.select('body').selectAll('.dummy') + .data([text]) + .enter().append('text') + .classed(cls ? cls : "", true) + .text(text) + .each(function () { rect = this.getBoundingClientRect(); }) + .remove(); + return rect; + }; + c3_chart_internal_fn.generateXYForText = function (areaIndices, barIndices, lineIndices, forX) { + var $$ = this, + getAreaPoints = $$.generateGetAreaPoints(barIndices, false), + getBarPoints = $$.generateGetBarPoints(barIndices, false), + getLinePoints = $$.generateGetLinePoints(lineIndices, false), + getter = forX ? $$.getXForText : $$.getYForText; + return function (d, i) { + var getPoints = $$.isAreaType(d) ? getAreaPoints : $$.isBarType(d) ? getBarPoints : getLinePoints; + return getter.call($$, getPoints(d, i), d, this); + }; + }; + c3_chart_internal_fn.getXForText = function (points, d, textElement) { + var $$ = this, + box = textElement.getBoundingClientRect(), xPos, padding; + if ($$.config.axis_rotated) { + padding = $$.isBarType(d) ? 4 : 6; + xPos = points[2][1] + padding * (d.value < 0 ? -1 : 1); + } else { + xPos = $$.hasType('bar') ? (points[2][0] + points[0][0]) / 2 : points[0][0]; + } + return d.value !== null ? xPos : xPos > $$.width ? $$.width - box.width : xPos; + }; + c3_chart_internal_fn.getYForText = function (points, d, textElement) { + var $$ = this, + box = textElement.getBoundingClientRect(), yPos; + if ($$.config.axis_rotated) { + yPos = (points[0][0] + points[2][0] + box.height * 0.6) / 2; + } else { + yPos = points[2][1] + (d.value < 0 ? box.height : $$.isBarType(d) ? -3 : -6); + } + return d.value !== null ? yPos : yPos < box.height ? box.height : yPos; + }; + + c3_chart_internal_fn.setTargetType = function (targetIds, type) { + var $$ = this, config = $$.config; + $$.mapToTargetIds(targetIds).forEach(function (id) { + $$.withoutFadeIn[id] = (type === config.data_types[id]); + config.data_types[id] = type; + }); + if (!targetIds) { + config.data_type = type; + } + }; + c3_chart_internal_fn.hasType = function (type, targets) { + var $$ = this, types = $$.config.data_types, has = false; + (targets || $$.data.targets).forEach(function (t) { + if ((types[t.id] && types[t.id].indexOf(type) >= 0) || (!(t.id in types) && type === 'line')) { + has = true; + } + }); + return has; + }; + c3_chart_internal_fn.hasArcType = function (targets) { + return this.hasType('pie', targets) || this.hasType('donut', targets) || this.hasType('gauge', targets); + }; + c3_chart_internal_fn.isLineType = function (d) { + var config = this.config, id = isString(d) ? d : d.id; + return !config.data_types[id] || ['line', 'spline', 'area', 'area-spline', 'step', 'area-step'].indexOf(config.data_types[id]) >= 0; + }; + c3_chart_internal_fn.isStepType = function (d) { + var id = isString(d) ? d : d.id; + return ['step', 'area-step'].indexOf(this.config.data_types[id]) >= 0; + }; + c3_chart_internal_fn.isSplineType = function (d) { + var id = isString(d) ? d : d.id; + return ['spline', 'area-spline'].indexOf(this.config.data_types[id]) >= 0; + }; + c3_chart_internal_fn.isAreaType = function (d) { + var id = isString(d) ? d : d.id; + return ['area', 'area-spline', 'area-step'].indexOf(this.config.data_types[id]) >= 0; + }; + c3_chart_internal_fn.isBarType = function (d) { + var id = isString(d) ? d : d.id; + return this.config.data_types[id] === 'bar'; + }; + c3_chart_internal_fn.isScatterType = function (d) { + var id = isString(d) ? d : d.id; + return this.config.data_types[id] === 'scatter'; + }; + c3_chart_internal_fn.isPieType = function (d) { + var id = isString(d) ? d : d.id; + return this.config.data_types[id] === 'pie'; + }; + c3_chart_internal_fn.isGaugeType = function (d) { + var id = isString(d) ? d : d.id; + return this.config.data_types[id] === 'gauge'; + }; + c3_chart_internal_fn.isDonutType = function (d) { + var id = isString(d) ? d : d.id; + return this.config.data_types[id] === 'donut'; + }; + c3_chart_internal_fn.isArcType = function (d) { + return this.isPieType(d) || this.isDonutType(d) || this.isGaugeType(d); + }; + c3_chart_internal_fn.lineData = function (d) { + return this.isLineType(d) ? [d] : []; + }; + c3_chart_internal_fn.arcData = function (d) { + return this.isArcType(d.data) ? [d] : []; + }; + /* not used + function scatterData(d) { + return isScatterType(d) ? d.values : []; + } + */ + c3_chart_internal_fn.barData = function (d) { + return this.isBarType(d) ? d.values : []; + }; + c3_chart_internal_fn.lineOrScatterData = function (d) { + return this.isLineType(d) || this.isScatterType(d) ? d.values : []; + }; + c3_chart_internal_fn.barOrLineData = function (d) { + return this.isBarType(d) || this.isLineType(d) ? d.values : []; + }; + + c3_chart_internal_fn.initGrid = function () { + var $$ = this, config = $$.config, d3 = $$.d3; + $$.grid = $$.main.append('g') + .attr("clip-path", $$.clipPath) + .attr('class', CLASS.grid); + if (config.grid_x_show) { + $$.grid.append("g").attr("class", CLASS.xgrids); + } + if (config.grid_y_show) { + $$.grid.append('g').attr('class', CLASS.ygrids); + } + if (config.grid_focus_show) { + $$.grid.append('g') + .attr("class", CLASS.xgridFocus) + .append('line') + .attr('class', CLASS.xgridFocus); + } + $$.xgrid = d3.selectAll([]); + if (!config.grid_lines_front) { $$.initGridLines(); } + }; + c3_chart_internal_fn.initGridLines = function () { + var $$ = this, d3 = $$.d3; + $$.gridLines = $$.main.append('g') + .attr("clip-path", $$.clipPath) + .attr('class', CLASS.grid + ' ' + CLASS.gridLines); + $$.gridLines.append('g').attr("class", CLASS.xgridLines); + $$.gridLines.append('g').attr('class', CLASS.ygridLines); + $$.xgridLines = d3.selectAll([]); + }; + c3_chart_internal_fn.updateXGrid = function (withoutUpdate) { + var $$ = this, config = $$.config, d3 = $$.d3, + xgridData = $$.generateGridData(config.grid_x_type, $$.x), + tickOffset = $$.isCategorized() ? $$.xAxis.tickOffset() : 0; + + $$.xgridAttr = config.axis_rotated ? { + 'x1': 0, + 'x2': $$.width, + 'y1': function (d) { return $$.x(d) - tickOffset; }, + 'y2': function (d) { return $$.x(d) - tickOffset; } + } : { + 'x1': function (d) { return $$.x(d) + tickOffset; }, + 'x2': function (d) { return $$.x(d) + tickOffset; }, + 'y1': 0, + 'y2': $$.height + }; + + $$.xgrid = $$.main.select('.' + CLASS.xgrids).selectAll('.' + CLASS.xgrid) + .data(xgridData); + $$.xgrid.enter().append('line').attr("class", CLASS.xgrid); + if (!withoutUpdate) { + $$.xgrid.attr($$.xgridAttr) + .style("opacity", function () { return +d3.select(this).attr(config.axis_rotated ? 'y1' : 'x1') === (config.axis_rotated ? $$.height : 0) ? 0 : 1; }); + } + $$.xgrid.exit().remove(); + }; + + c3_chart_internal_fn.updateYGrid = function () { + var $$ = this, config = $$.config; + $$.ygrid = $$.main.select('.' + CLASS.ygrids).selectAll('.' + CLASS.ygrid) + .data($$.y.ticks(config.grid_y_ticks)); + $$.ygrid.enter().append('line') + .attr('class', CLASS.ygrid); + $$.ygrid.attr("x1", config.axis_rotated ? $$.y : 0) + .attr("x2", config.axis_rotated ? $$.y : $$.width) + .attr("y1", config.axis_rotated ? 0 : $$.y) + .attr("y2", config.axis_rotated ? $$.height : $$.y); + $$.ygrid.exit().remove(); + $$.smoothLines($$.ygrid, 'grid'); + }; + + + c3_chart_internal_fn.redrawGrid = function (duration, withY) { + var $$ = this, main = $$.main, config = $$.config, + xgridLine, ygridLine, yv; + main.select('line.' + CLASS.xgridFocus).style("visibility", "hidden"); + if (config.grid_x_show) { + $$.updateXGrid(); + } + $$.xgridLines = main.select('.' + CLASS.xgridLines).selectAll('.' + CLASS.xgridLine) + .data(config.grid_x_lines); + // enter + xgridLine = $$.xgridLines.enter().append('g') + .attr("class", function (d) { return CLASS.xgridLine + (d.class ? ' ' + d.class : ''); }); + xgridLine.append('line') + .style("opacity", 0); + xgridLine.append('text') + .attr("text-anchor", "end") + .attr("transform", config.axis_rotated ? "" : "rotate(-90)") + .attr('dx', config.axis_rotated ? 0 : -$$.margin.top) + .attr('dy', -5) + .style("opacity", 0); + // udpate + // done in d3.transition() of the end of this function + // exit + $$.xgridLines.exit().transition().duration(duration) + .style("opacity", 0) + .remove(); + + // Y-Grid + if (withY && config.grid_y_show) { + $$.updateYGrid(); + } + if (withY) { + $$.ygridLines = main.select('.' + CLASS.ygridLines).selectAll('.' + CLASS.ygridLine) + .data(config.grid_y_lines); + // enter + ygridLine = $$.ygridLines.enter().append('g') + .attr("class", function (d) { return CLASS.ygridLine + (d.class ? ' ' + d.class : ''); }); + ygridLine.append('line') + .style("opacity", 0); + ygridLine.append('text') + .attr("text-anchor", "end") + .attr("transform", config.axis_rotated ? "rotate(-90)" : "") + .attr('dx', config.axis_rotated ? 0 : -$$.margin.top) + .attr('dy', -5) + .style("opacity", 0); + // update + yv = $$.yv.bind($$); + $$.ygridLines.select('line') + .transition().duration(duration) + .attr("x1", config.axis_rotated ? yv : 0) + .attr("x2", config.axis_rotated ? yv : $$.width) + .attr("y1", config.axis_rotated ? 0 : yv) + .attr("y2", config.axis_rotated ? $$.height : yv) + .style("opacity", 1); + $$.ygridLines.select('text') + .transition().duration(duration) + .attr("x", config.axis_rotated ? 0 : $$.width) + .attr("y", yv) + .text(function (d) { return d.text; }) + .style("opacity", 1); + // exit + $$.ygridLines.exit().transition().duration(duration) + .style("opacity", 0) + .remove(); + } + }; + c3_chart_internal_fn.addTransitionForGrid = function (transitions) { + var $$ = this, config = $$.config, xv = $$.xv.bind($$); + transitions.push($$.xgridLines.select('line').transition() + .attr("x1", config.axis_rotated ? 0 : xv) + .attr("x2", config.axis_rotated ? $$.width : xv) + .attr("y1", config.axis_rotated ? xv : $$.margin.top) + .attr("y2", config.axis_rotated ? xv : $$.height) + .style("opacity", 1)); + transitions.push($$.xgridLines.select('text').transition() + .attr("x", config.axis_rotated ? $$.width : 0) + .attr("y", xv) + .text(function (d) { return d.text; }) + .style("opacity", 1)); + }; + c3_chart_internal_fn.showXGridFocus = function (selectedData) { + var $$ = this, config = $$.config, + dataToShow = selectedData.filter(function (d) { return d && isValue(d.value); }), + focusEl = $$.main.selectAll('line.' + CLASS.xgridFocus), + xx = $$.xx.bind($$); + if (! config.tooltip_show) { return; } + // Hide when scatter plot exists + if ($$.hasType('scatter') || $$.hasArcType()) { return; } + focusEl + .style("visibility", "visible") + .data([dataToShow[0]]) + .attr(config.axis_rotated ? 'y1' : 'x1', xx) + .attr(config.axis_rotated ? 'y2' : 'x2', xx); + $$.smoothLines(focusEl, 'grid'); + }; + c3_chart_internal_fn.hideXGridFocus = function () { + this.main.select('line.' + CLASS.xgridFocus).style("visibility", "hidden"); + }; + c3_chart_internal_fn.updateXgridFocus = function () { + var $$ = this, config = $$.config; + $$.main.select('line.' + CLASS.xgridFocus) + .attr("x1", config.axis_rotated ? 0 : -10) + .attr("x2", config.axis_rotated ? $$.width : -10) + .attr("y1", config.axis_rotated ? -10 : 0) + .attr("y2", config.axis_rotated ? -10 : $$.height); + }; + c3_chart_internal_fn.generateGridData = function (type, scale) { + var $$ = this, + gridData = [], xDomain, firstYear, lastYear, i, + tickNum = $$.main.select("." + CLASS.axisX).selectAll('.tick').size(); + if (type === 'year') { + xDomain = $$.getXDomain(); + firstYear = xDomain[0].getFullYear(); + lastYear = xDomain[1].getFullYear(); + for (i = firstYear; i <= lastYear; i++) { + gridData.push(new Date(i + '-01-01 00:00:00')); + } + } else { + gridData = scale.ticks(10); + if (gridData.length > tickNum) { // use only int + gridData = gridData.filter(function (d) { return ("" + d).indexOf('.') < 0; }); + } + } + return gridData; + }; + c3_chart_internal_fn.getGridFilterToRemove = function (params) { + return params ? function (line) { + var found = false; + [].concat(params).forEach(function (param) { + if ((('value' in param && line.value === params.value) || ('class' in param && line.class === params.class))) { + found = true; + } + }); + return found; + } : function () { return true; }; + }; + c3_chart_internal_fn.removeGridLines = function (params, forX) { + var $$ = this, config = $$.config, + toRemove = $$.getGridFilterToRemove(params), + toShow = function (line) { return !toRemove(line); }, + classLines = forX ? CLASS.xgridLines : CLASS.ygridLines, + classLine = forX ? CLASS.xgridLine : CLASS.ygridLine; + $$.main.select('.' + classLines).selectAll('.' + classLine).filter(toRemove) + .transition().duration(config.transition_duration) + .style('opacity', 0).remove(); + if (forX) { + config.grid_x_lines = config.grid_x_lines.filter(toShow); + } else { + config.grid_y_lines = config.grid_y_lines.filter(toShow); + } + }; + + c3_chart_internal_fn.initTooltip = function () { + var $$ = this, config = $$.config, i; + $$.tooltip = $$.selectChart + .style("position", "relative") + .append("div") + .style("position", "absolute") + .style("pointer-events", "none") + .style("z-index", "10") + .style("display", "none"); + // Show tooltip if needed + if (config.tooltip_init_show) { + if ($$.isTimeSeries() && isString(config.tooltip_init_x)) { + config.tooltip_init_x = $$.parseDate(config.tooltip_init_x); + for (i = 0; i < $$.data.targets[0].values.length; i++) { + if (($$.data.targets[0].values[i].x - config.tooltip_init_x) === 0) { break; } + } + config.tooltip_init_x = i; + } + $$.tooltip.html(config.tooltip_contents.call($$, $$.data.targets.map(function (d) { + return $$.addName(d.values[config.tooltip_init_x]); + }), $$.getXAxisTickFormat(), $$.getYFormat($$.hasArcType()), $$.color)); + $$.tooltip.style("top", config.tooltip_init_position.top) + .style("left", config.tooltip_init_position.left) + .style("display", "block"); + } + }; + c3_chart_internal_fn.getTooltipContent = function (d, defaultTitleFormat, defaultValueFormat, color) { + var $$ = this, config = $$.config, + titleFormat = config.tooltip_format_title || defaultTitleFormat, + nameFormat = config.tooltip_format_name || function (name) { return name; }, + valueFormat = config.tooltip_format_value || defaultValueFormat, + text, i, title, value, name, bgcolor; + for (i = 0; i < d.length; i++) { + if (! (d[i] && (d[i].value || d[i].value === 0))) { continue; } + + if (! text) { + title = titleFormat ? titleFormat(d[i].x) : d[i].x; + text = "<table class='" + CLASS.tooltip + "'>" + (title || title === 0 ? "<tr><th colspan='2'>" + title + "</th></tr>" : ""); + } + + name = nameFormat(d[i].name); + value = valueFormat(d[i].value, d[i].ratio, d[i].id, d[i].index); + bgcolor = $$.levelColor ? $$.levelColor(d[i].value) : color(d[i].id); + + text += "<tr class='" + CLASS.tooltipName + "-" + d[i].id + "'>"; + text += "<td class='name'><span style='background-color:" + bgcolor + "'></span>" + name + "</td>"; + text += "<td class='value'>" + value + "</td>"; + text += "</tr>"; + } + return text + "</table>"; + }; + c3_chart_internal_fn.showTooltip = function (selectedData, mouse) { + var $$ = this, config = $$.config; + var tWidth, tHeight, svgLeft, tooltipLeft, tooltipRight, tooltipTop, chartRight; + var forArc = $$.hasArcType(), + dataToShow = selectedData.filter(function (d) { return d && isValue(d.value); }); + if (dataToShow.length === 0 || !config.tooltip_show) { + return; + } + $$.tooltip.html(config.tooltip_contents.call($$, selectedData, $$.getXAxisTickFormat(), $$.getYFormat(forArc), $$.color)).style("display", "block"); + + // Get tooltip dimensions + tWidth = $$.tooltip.property('offsetWidth'); + tHeight = $$.tooltip.property('offsetHeight'); + // Determin tooltip position + if (forArc) { + tooltipLeft = ($$.width / 2) + mouse[0]; + tooltipTop = ($$.height / 2) + mouse[1] + 20; + } else { + if (config.axis_rotated) { + svgLeft = $$.getSvgLeft(); + tooltipLeft = svgLeft + mouse[0] + 100; + tooltipRight = tooltipLeft + tWidth; + chartRight = $$.getCurrentWidth() - $$.getCurrentPaddingRight(); + tooltipTop = $$.x(dataToShow[0].x) + 20; + } else { + svgLeft = $$.getSvgLeft(); + tooltipLeft = svgLeft + $$.getCurrentPaddingLeft() + $$.x(dataToShow[0].x) + 20; + tooltipRight = tooltipLeft + tWidth; + chartRight = svgLeft + $$.getCurrentWidth() - $$.getCurrentPaddingRight(); + tooltipTop = mouse[1] + 15; + } + + if (tooltipRight > chartRight) { + tooltipLeft -= tooltipRight - chartRight; + } + if (tooltipTop + tHeight > $$.getCurrentHeight() && tooltipTop > tHeight + 30) { + tooltipTop -= tHeight + 30; + } + } + // Set tooltip + $$.tooltip + .style("top", tooltipTop + "px") + .style("left", tooltipLeft + 'px'); + }; + c3_chart_internal_fn.hideTooltip = function () { + this.tooltip.style("display", "none"); + }; + + c3_chart_internal_fn.initLegend = function () { + var $$ = this; + $$.legend = $$.svg.append("g").attr("transform", $$.getTranslate('legend')); + if (!$$.config.legend_show) { + $$.legend.style('visibility', 'hidden'); + $$.hiddenLegendIds = $$.mapToIds($$.data.targets); + } + // MEMO: call here to update legend box and tranlate for all + // MEMO: translate will be upated by this, so transform not needed in updateLegend() + $$.updateLegend($$.mapToIds($$.data.targets), {withTransform: false, withTransitionForTransform: false, withTransition: false}); + }; + c3_chart_internal_fn.updateSizeForLegend = function (legendHeight, legendWidth) { + var $$ = this, config = $$.config, insetLegendPosition = { + top: $$.isLegendTop ? $$.getCurrentPaddingTop() + config.legend_inset_y + 5.5 : $$.currentHeight - legendHeight - $$.getCurrentPaddingBottom() - config.legend_inset_y, + left: $$.isLegendLeft ? $$.getCurrentPaddingLeft() + config.legend_inset_x + 0.5 : $$.currentWidth - legendWidth - $$.getCurrentPaddingRight() - config.legend_inset_x + 0.5 + }; + $$.margin3 = { + top: $$.isLegendRight ? 0 : $$.isLegendInset ? insetLegendPosition.top : $$.currentHeight - legendHeight, + right: NaN, + bottom: 0, + left: $$.isLegendRight ? $$.currentWidth - legendWidth : $$.isLegendInset ? insetLegendPosition.left : 0 + }; + }; + c3_chart_internal_fn.transformLegend = function (withTransition) { + var $$ = this; + (withTransition ? $$.legend.transition() : $$.legend).attr("transform", $$.getTranslate('legend')); + }; + c3_chart_internal_fn.updateLegendStep = function (step) { + this.legendStep = step; + }; + c3_chart_internal_fn.updateLegendItemWidth = function (w) { + this.legendItemWidth = w; + }; + c3_chart_internal_fn.updateLegendItemHeight = function (h) { + this.legendItemHeight = h; + }; + c3_chart_internal_fn.getLegendWidth = function () { + var $$ = this; + return $$.config.legend_show ? $$.isLegendRight || $$.isLegendInset ? $$.legendItemWidth * ($$.legendStep + 1) : $$.currentWidth : 0; + }; + c3_chart_internal_fn.getLegendHeight = function () { + var $$ = this, config = $$.config, h = 0; + if (config.legend_show) { + if ($$.isLegendRight) { + h = $$.currentHeight; + } else if ($$.isLegendInset) { + h = config.legend_inset_step ? Math.max(20, $$.legendItemHeight) * (config.legend_inset_step + 1) : $$.height; + } else { + h = Math.max(20, $$.legendItemHeight) * ($$.legendStep + 1); + } + } + return h; + }; + c3_chart_internal_fn.opacityForLegend = function (legendItem) { + var $$ = this; + return legendItem.classed(CLASS.legendItemHidden) ? $$.legendOpacityForHidden : 1; + }; + c3_chart_internal_fn.opacityForUnfocusedLegend = function (legendItem) { + var $$ = this; + return legendItem.classed(CLASS.legendItemHidden) ? $$.legendOpacityForHidden : 0.3; + }; + c3_chart_internal_fn.toggleFocusLegend = function (id, focus) { + var $$ = this; + $$.legend.selectAll('.' + CLASS.legendItem) + .transition().duration(100) + .style('opacity', function (_id) { + var This = $$.d3.select(this); + if (id && _id !== id) { + return focus ? $$.opacityForUnfocusedLegend(This) : $$.opacityForLegend(This); + } else { + return focus ? $$.opacityForLegend(This) : $$.opacityForUnfocusedLegend(This); + } + }); + }; + c3_chart_internal_fn.revertLegend = function () { + var $$ = this, d3 = $$.d3; + $$.legend.selectAll('.' + CLASS.legendItem) + .transition().duration(100) + .style('opacity', function () { return $$.opacityForLegend(d3.select(this)); }); + }; + c3_chart_internal_fn.showLegend = function (targetIds) { + var $$ = this, config = $$.config; + if (!config.legend_show) { + config.legend_show = true; + $$.legend.style('visibility', 'visible'); + } + $$.removeHiddenLegendIds(targetIds); + $$.legend.selectAll($$.selectorLegends(targetIds)) + .style('visibility', 'visible') + .transition() + .style('opacity', function () { return $$.opacityForLegend($$.d3.select(this)); }); + }; + c3_chart_internal_fn.hideLegend = function (targetIds) { + var $$ = this, config = $$.config; + if (config.legend_show && isEmpty(targetIds)) { + config.legend_show = false; + $$.legend.style('visibility', 'hidden'); + } + $$.addHiddenLegendIds(targetIds); + $$.legend.selectAll($$.selectorLegends(targetIds)) + .style('opacity', 0) + .style('visibility', 'hidden'); + }; + c3_chart_internal_fn.updateLegend = function (targetIds, options, transitions) { + var $$ = this, config = $$.config; + var xForLegend, xForLegendText, xForLegendRect, yForLegend, yForLegendText, yForLegendRect; + var paddingTop = 4, paddingRight = 36, maxWidth = 0, maxHeight = 0, posMin = 10; + var l, totalLength = 0, offsets = {}, widths = {}, heights = {}, margins = [0], steps = {}, step = 0; + var withTransition, withTransitionForTransform; + var hasFocused = $$.legend.selectAll('.' + CLASS.legendItemFocused).size(); + var texts, rects, tiles; + + options = options || {}; + withTransition = getOption(options, "withTransition", true); + withTransitionForTransform = getOption(options, "withTransitionForTransform", true); + + function updatePositions(textElement, id, reset) { + var box = $$.getTextRect(textElement.textContent, CLASS.legendItem), + itemWidth = Math.ceil((box.width + paddingRight) / 10) * 10, + itemHeight = Math.ceil((box.height + paddingTop) / 10) * 10, + itemLength = $$.isLegendRight || $$.isLegendInset ? itemHeight : itemWidth, + areaLength = $$.isLegendRight || $$.isLegendInset ? $$.getLegendHeight() : $$.getLegendWidth(), + margin, maxLength; + + // MEMO: care about condifion of step, totalLength + function updateValues(id, withoutStep) { + if (!withoutStep) { + margin = (areaLength - totalLength - itemLength) / 2; + if (margin < posMin) { + margin = (areaLength - itemLength) / 2; + totalLength = 0; + step++; + } + } + steps[id] = step; + margins[step] = $$.isLegendInset ? 10 : margin; + offsets[id] = totalLength; + totalLength += itemLength; + } + + if (reset) { + totalLength = 0; + step = 0; + maxWidth = 0; + maxHeight = 0; + } + + if (config.legend_show && !$$.isLegendToShow(id)) { + widths[id] = heights[id] = steps[id] = offsets[id] = 0; + return; + } + + widths[id] = itemWidth; + heights[id] = itemHeight; + + if (!maxWidth || itemWidth >= maxWidth) { maxWidth = itemWidth; } + if (!maxHeight || itemHeight >= maxHeight) { maxHeight = itemHeight; } + maxLength = $$.isLegendRight || $$.isLegendInset ? maxHeight : maxWidth; + + if (config.legend_equally) { + Object.keys(widths).forEach(function (id) { widths[id] = maxWidth; }); + Object.keys(heights).forEach(function (id) { heights[id] = maxHeight; }); + margin = (areaLength - maxLength * targetIds.length) / 2; + if (margin < posMin) { + totalLength = 0; + step = 0; + targetIds.forEach(function (id) { updateValues(id); }); + } + else { + updateValues(id, true); + } + } else { + updateValues(id); + } + } + + if ($$.isLegendRight) { + xForLegend = function (id) { return maxWidth * steps[id]; }; + yForLegend = function (id) { return margins[steps[id]] + offsets[id]; }; + } else if ($$.isLegendInset) { + xForLegend = function (id) { return maxWidth * steps[id] + 10; }; + yForLegend = function (id) { return margins[steps[id]] + offsets[id]; }; + } else { + xForLegend = function (id) { return margins[steps[id]] + offsets[id]; }; + yForLegend = function (id) { return maxHeight * steps[id]; }; + } + xForLegendText = function (id, i) { return xForLegend(id, i) + 14; }; + yForLegendText = function (id, i) { return yForLegend(id, i) + 9; }; + xForLegendRect = function (id, i) { return xForLegend(id, i) - 4; }; + yForLegendRect = function (id, i) { return yForLegend(id, i) - 7; }; + + // Define g for legend area + l = $$.legend.selectAll('.' + CLASS.legendItem) + .data(targetIds) + .enter().append('g') + .attr('class', function (id) { return $$.generateClass(CLASS.legendItem, id); }) + .style('visibility', function (id) { return $$.isLegendToShow(id) ? 'visible' : 'hidden'; }) + .style('cursor', 'pointer') + .on('click', function (id) { + config.legend_item_onclick ? config.legend_item_onclick.call($$, id) : $$.api.toggle(id); + }) + .on('mouseover', function (id) { + $$.d3.select(this).classed(CLASS.legendItemFocused, true); + if (!$$.transiting) { + $$.api.focus(id); + } + if (config.legend_item_onmouseover) { + config.legend_item_onmouseover.call($$, id); + } + }) + .on('mouseout', function (id) { + $$.d3.select(this).classed(CLASS.legendItemFocused, false); + if (!$$.transiting) { + $$.api.revert(); + } + if (config.legend_item_onmouseout) { + config.legend_item_onmouseout.call($$, id); + } + }); + l.append('text') + .text(function (id) { return isDefined(config.data_names[id]) ? config.data_names[id] : id; }) + .each(function (id, i) { updatePositions(this, id, i === 0); }) + .style("pointer-events", "none") + .attr('x', $$.isLegendRight || $$.isLegendInset ? xForLegendText : -200) + .attr('y', $$.isLegendRight || $$.isLegendInset ? -200 : yForLegendText); + l.append('rect') + .attr("class", CLASS.legendItemEvent) + .style('fill-opacity', 0) + .attr('x', $$.isLegendRight || $$.isLegendInset ? xForLegendRect : -200) + .attr('y', $$.isLegendRight || $$.isLegendInset ? -200 : yForLegendRect); + l.append('rect') + .attr("class", CLASS.legendItemTile) + .style("pointer-events", "none") + .style('fill', $$.color) + .attr('x', $$.isLegendRight || $$.isLegendInset ? xForLegendText : -200) + .attr('y', $$.isLegendRight || $$.isLegendInset ? -200 : yForLegend) + .attr('width', 10) + .attr('height', 10); + // Set background for inset legend + if ($$.isLegendInset && maxWidth !== 0) { + $$.legend.insert('g', '.' + CLASS.legendItem) + .attr("class", CLASS.legendBackground) + .append('rect') + .attr('height', $$.getLegendHeight() - 10) + .attr('width', maxWidth * (step + 1) + 10); + } + + texts = $$.legend.selectAll('text') + .data(targetIds) + .text(function (id) { return isDefined(config.data_names[id]) ? config.data_names[id] : id; }) // MEMO: needed for update + .each(function (id, i) { updatePositions(this, id, i === 0); }); + (withTransition ? texts.transition() : texts) + .attr('x', xForLegendText) + .attr('y', yForLegendText); + + rects = $$.legend.selectAll('rect.' + CLASS.legendItemEvent) + .data(targetIds); + (withTransition ? rects.transition() : rects) + .attr('width', function (id) { return widths[id]; }) + .attr('height', function (id) { return heights[id]; }) + .attr('x', xForLegendRect) + .attr('y', yForLegendRect); + + tiles = $$.legend.selectAll('rect.' + CLASS.legendItemTile) + .data(targetIds); + (withTransition ? tiles.transition() : tiles) + .style('fill', $$.color) + .attr('x', xForLegend) + .attr('y', yForLegend); + + // toggle legend state + $$.legend.selectAll('.' + CLASS.legendItem) + .classed(CLASS.legendItemHidden, function (id) { return !$$.isTargetToShow(id); }) + .transition() + .style('opacity', function (id) { + var This = $$.d3.select(this); + if ($$.isTargetToShow(id)) { + return !hasFocused || This.classed(CLASS.legendItemFocused) ? $$.opacityForLegend(This) : $$.opacityForUnfocusedLegend(This); + } else { + return $$.legendOpacityForHidden; + } + }); + + // Update all to reflect change of legend + $$.updateLegendItemWidth(maxWidth); + $$.updateLegendItemHeight(maxHeight); + $$.updateLegendStep(step); + // Update size and scale + $$.updateSizes(); + $$.updateScales(); + $$.updateSvgSize(); + // Update g positions + $$.transformAll(withTransitionForTransform, transitions); + }; + + c3_chart_internal_fn.initAxis = function () { + var $$ = this, config = $$.config, main = $$.main; + $$.axes.x = main.append("g") + .attr("class", CLASS.axis + ' ' + CLASS.axisX) + .attr("clip-path", $$.clipPathForXAxis) + .attr("transform", $$.getTranslate('x')) + .style("visibility", config.axis_x_show ? 'visible' : 'hidden'); + $$.axes.x.append("text") + .attr("class", CLASS.axisXLabel) + .attr("transform", config.axis_rotated ? "rotate(-90)" : "") + .style("text-anchor", $$.textAnchorForXAxisLabel.bind($$)); + + $$.axes.y = main.append("g") + .attr("class", CLASS.axis + ' ' + CLASS.axisY) + .attr("clip-path", $$.clipPathForYAxis) + .attr("transform", $$.getTranslate('y')) + .style("visibility", config.axis_y_show ? 'visible' : 'hidden'); + $$.axes.y.append("text") + .attr("class", CLASS.axisYLabel) + .attr("transform", config.axis_rotated ? "" : "rotate(-90)") + .style("text-anchor", $$.textAnchorForYAxisLabel.bind($$)); + + $$.axes.y2 = main.append("g") + .attr("class", CLASS.axis + ' ' + CLASS.axisY2) + // clip-path? + .attr("transform", $$.getTranslate('y2')) + .style("visibility", config.axis_y2_show ? 'visible' : 'hidden'); + $$.axes.y2.append("text") + .attr("class", CLASS.axisY2Label) + .attr("transform", config.axis_rotated ? "" : "rotate(-90)") + .style("text-anchor", $$.textAnchorForY2AxisLabel.bind($$)); + }; + c3_chart_internal_fn.getXAxis = function (scale, orient, tickFormat, tickValues) { + var $$ = this, config = $$.config, + axis = c3_axis($$.d3, $$.isCategorized()).scale(scale).orient(orient); + + if ($$.isTimeSeries() && tickValues) { + tickValues = tickValues.map(function (v) { return $$.parseDate(v); }); + } + + // Set tick + axis.tickFormat(tickFormat).tickValues(tickValues); + if ($$.isCategorized()) { + axis.tickCentered(config.axis_x_tick_centered); + if (isEmpty(config.axis_x_tick_culling)) { + config.axis_x_tick_culling = false; + } + } else { + // TODO: move this to c3_axis + axis.tickOffset = function () { + var edgeX = $$.getEdgeX($$.data.targets), diff = $$.x(edgeX[1]) - $$.x(edgeX[0]), + base = diff ? diff : (config.axis_rotated ? $$.height : $$.width); + return (base / $$.getMaxDataCount()) / 2; + }; + } + + return axis; + }; + c3_chart_internal_fn.getYAxis = function (scale, orient, tickFormat, ticks) { + return c3_axis(this.d3).scale(scale).orient(orient).tickFormat(tickFormat).ticks(ticks); + }; + c3_chart_internal_fn.getAxisId = function (id) { + var config = this.config; + return id in config.data_axes ? config.data_axes[id] : 'y'; + }; + c3_chart_internal_fn.getXAxisTickFormat = function () { + var $$ = this, config = $$.config, + format = $$.isTimeSeries() ? $$.defaultAxisTimeFormat : $$.isCategorized() ? $$.categoryName : function (v) { return v < 0 ? v.toFixed(0) : v; }; + if (config.axis_x_tick_format) { + if (isFunction(config.axis_x_tick_format)) { + format = config.axis_x_tick_format; + } else if ($$.isTimeSeries()) { + format = function (date) { + return date ? $$.axisTimeFormat(config.axis_x_tick_format)(date) : ""; + }; + } + } + return isFunction(format) ? function (v) { return format.call($$, v); } : format; + }; + c3_chart_internal_fn.getAxisLabelOptionByAxisId = function (axisId) { + var $$ = this, config = $$.config, option; + if (axisId === 'y') { + option = config.axis_y_label; + } else if (axisId === 'y2') { + option = config.axis_y2_label; + } else if (axisId === 'x') { + option = config.axis_x_label; + } + return option; + }; + c3_chart_internal_fn.getAxisLabelText = function (axisId) { + var option = this.getAxisLabelOptionByAxisId(axisId); + return isString(option) ? option : option ? option.text : null; + }; + c3_chart_internal_fn.setAxisLabelText = function (axisId, text) { + var $$ = this, config = $$.config, + option = $$.getAxisLabelOptionByAxisId(axisId); + if (isString(option)) { + if (axisId === 'y') { + config.axis_y_label = text; + } else if (axisId === 'y2') { + config.axis_y2_label = text; + } else if (axisId === 'x') { + config.axis_x_label = text; + } + } else if (option) { + option.text = text; + } + }; + c3_chart_internal_fn.getAxisLabelPosition = function (axisId, defaultPosition) { + var option = this.getAxisLabelOptionByAxisId(axisId), + position = (option && typeof option === 'object' && option.position) ? option.position : defaultPosition; + return { + isInner: position.indexOf('inner') >= 0, + isOuter: position.indexOf('outer') >= 0, + isLeft: position.indexOf('left') >= 0, + isCenter: position.indexOf('center') >= 0, + isRight: position.indexOf('right') >= 0, + isTop: position.indexOf('top') >= 0, + isMiddle: position.indexOf('middle') >= 0, + isBottom: position.indexOf('bottom') >= 0 + }; + }; + c3_chart_internal_fn.getXAxisLabelPosition = function () { + return this.getAxisLabelPosition('x', this.config.axis_rotated ? 'inner-top' : 'inner-right'); + }; + c3_chart_internal_fn.getYAxisLabelPosition = function () { + return this.getAxisLabelPosition('y', this.config.axis_rotated ? 'inner-right' : 'inner-top'); + }; + c3_chart_internal_fn.getY2AxisLabelPosition = function () { + return this.getAxisLabelPosition('y2', this.config.axis_rotated ? 'inner-right' : 'inner-top'); + }; + c3_chart_internal_fn.getAxisLabelPositionById = function (id) { + return id === 'y2' ? this.getY2AxisLabelPosition() : id === 'y' ? this.getYAxisLabelPosition() : this.getXAxisLabelPosition(); + }; + c3_chart_internal_fn.textForXAxisLabel = function () { + return this.getAxisLabelText('x'); + }; + c3_chart_internal_fn.textForYAxisLabel = function () { + return this.getAxisLabelText('y'); + }; + c3_chart_internal_fn.textForY2AxisLabel = function () { + return this.getAxisLabelText('y2'); + }; + c3_chart_internal_fn.xForAxisLabel = function (forHorizontal, position) { + var $$ = this; + if (forHorizontal) { + return position.isLeft ? 0 : position.isCenter ? $$.width / 2 : $$.width; + } else { + return position.isBottom ? -$$.height : position.isMiddle ? -$$.height / 2 : 0; + } + }; + c3_chart_internal_fn.dxForAxisLabel = function (forHorizontal, position) { + if (forHorizontal) { + return position.isLeft ? "0.5em" : position.isRight ? "-0.5em" : "0"; + } else { + return position.isTop ? "-0.5em" : position.isBottom ? "0.5em" : "0"; + } + }; + c3_chart_internal_fn.textAnchorForAxisLabel = function (forHorizontal, position) { + if (forHorizontal) { + return position.isLeft ? 'start' : position.isCenter ? 'middle' : 'end'; + } else { + return position.isBottom ? 'start' : position.isMiddle ? 'middle' : 'end'; + } + }; + c3_chart_internal_fn.xForXAxisLabel = function () { + return this.xForAxisLabel(!this.config.axis_rotated, this.getXAxisLabelPosition()); + }; + c3_chart_internal_fn.xForYAxisLabel = function () { + return this.xForAxisLabel(this.config.axis_rotated, this.getYAxisLabelPosition()); + }; + c3_chart_internal_fn.xForY2AxisLabel = function () { + return this.xForAxisLabel(this.config.axis_rotated, this.getY2AxisLabelPosition()); + }; + c3_chart_internal_fn.dxForXAxisLabel = function () { + return this.dxForAxisLabel(!this.config.axis_rotated, this.getXAxisLabelPosition()); + }; + c3_chart_internal_fn.dxForYAxisLabel = function () { + return this.dxForAxisLabel(this.config.axis_rotated, this.getYAxisLabelPosition()); + }; + c3_chart_internal_fn.dxForY2AxisLabel = function () { + return this.dxForAxisLabel(this.config.axis_rotated, this.getY2AxisLabelPosition()); + }; + c3_chart_internal_fn.dyForXAxisLabel = function () { + var $$ = this, config = $$.config, + position = $$.getXAxisLabelPosition(); + if (config.axis_rotated) { + return position.isInner ? "1.2em" : -25 - $$.getMaxTickWidth('x'); + } else { + return position.isInner ? "-0.5em" : config.axis_x_height ? config.axis_x_height - 10 : "3em"; + } + }; + c3_chart_internal_fn.dyForYAxisLabel = function () { + var $$ = this, + position = $$.getYAxisLabelPosition(); + if ($$.config.axis_rotated) { + return position.isInner ? "-0.5em" : "3em"; + } else { + return position.isInner ? "1.2em" : -20 - $$.getMaxTickWidth('y'); + } + }; + c3_chart_internal_fn.dyForY2AxisLabel = function () { + var $$ = this, + position = $$.getY2AxisLabelPosition(); + if ($$.config.axis_rotated) { + return position.isInner ? "1.2em" : "-2.2em"; + } else { + return position.isInner ? "-0.5em" : 30 + this.getMaxTickWidth('y2'); + } + }; + c3_chart_internal_fn.textAnchorForXAxisLabel = function () { + var $$ = this; + return $$.textAnchorForAxisLabel(!$$.config.axis_rotated, $$.getXAxisLabelPosition()); + }; + c3_chart_internal_fn.textAnchorForYAxisLabel = function () { + var $$ = this; + return $$.textAnchorForAxisLabel($$.config.axis_rotated, $$.getYAxisLabelPosition()); + }; + c3_chart_internal_fn.textAnchorForY2AxisLabel = function () { + var $$ = this; + return $$.textAnchorForAxisLabel($$.config.axis_rotated, $$.getY2AxisLabelPosition()); + }; + + c3_chart_internal_fn.xForRotatedTickText = function (r) { + return 10 * Math.sin(Math.PI * (r / 180)); + }; + c3_chart_internal_fn.yForRotatedTickText = function (r) { + return 11.5 - 2.5 * (r / 15); + }; + c3_chart_internal_fn.rotateTickText = function (axis, transition, rotate) { + axis.selectAll('.tick text') + .style("text-anchor", "start"); + transition.selectAll('.tick text') + .attr("y", this.yForRotatedTickText(rotate)) + .attr("x", this.xForRotatedTickText(rotate)) + .attr("transform", "rotate(" + rotate + ")"); + }; + + c3_chart_internal_fn.getMaxTickWidth = function (id) { + var $$ = this, config = $$.config, + maxWidth = 0, targetsToShow, scale, axis; + if ($$.svg) { + targetsToShow = $$.filterTargetsToShow($$.data.targets); + if (id === 'y') { + scale = $$.y.copy().domain($$.getYDomain(targetsToShow, 'y')); + axis = $$.getYAxis(scale, $$.yOrient, config.axis_y_tick_format, config.axis_y_ticks); + } else if (id === 'y2') { + scale = $$.y2.copy().domain($$.getYDomain(targetsToShow, 'y2')); + axis = $$.getYAxis(scale, $$.y2Orient, config.axis_y2_tick_format, config.axis_y2_ticks); + } else { + scale = $$.x.copy().domain($$.getXDomain(targetsToShow)); + axis = $$.getXAxis(scale, $$.xOrient, $$.getXAxisTickFormat(), config.axis_x_tick_values ? config.axis_x_tick_values : $$.xAxis.tickValues()); + } + $$.d3.select('body').append("g").style('visibility', 'hidden').call(axis).each(function () { + $$.d3.select(this).selectAll('text').each(function () { + var box = this.getBoundingClientRect(); + if (maxWidth < box.width) { maxWidth = box.width; } + }); + }).remove(); + } + $$.currentMaxTickWidth = maxWidth <= 0 ? $$.currentMaxTickWidth : maxWidth; + return $$.currentMaxTickWidth; + }; + + c3_chart_internal_fn.updateAxisLabels = function (withTransition) { + var $$ = this; + var axisXLabel = $$.main.select('.' + CLASS.axisX + ' .' + CLASS.axisXLabel), + axisYLabel = $$.main.select('.' + CLASS.axisY + ' .' + CLASS.axisYLabel), + axisY2Label = $$.main.select('.' + CLASS.axisY2 + ' .' + CLASS.axisY2Label); + (withTransition ? axisXLabel.transition() : axisXLabel) + .attr("x", $$.xForXAxisLabel.bind($$)) + .attr("dx", $$.dxForXAxisLabel.bind($$)) + .attr("dy", $$.dyForXAxisLabel.bind($$)) + .text($$.textForXAxisLabel.bind($$)); + (withTransition ? axisYLabel.transition() : axisYLabel) + .attr("x", $$.xForYAxisLabel.bind($$)) + .attr("dx", $$.dxForYAxisLabel.bind($$)) + .attr("dy", $$.dyForYAxisLabel.bind($$)) + .text($$.textForYAxisLabel.bind($$)); + (withTransition ? axisY2Label.transition() : axisY2Label) + .attr("x", $$.xForY2AxisLabel.bind($$)) + .attr("dx", $$.dxForY2AxisLabel.bind($$)) + .attr("dy", $$.dyForY2AxisLabel.bind($$)) + .text($$.textForY2AxisLabel.bind($$)); + }; + + c3_chart_internal_fn.getAxisPadding = function (padding, key, defaultValue, all) { + var ratio = padding.unit === 'ratio' ? all : 1; + return isValue(padding[key]) ? padding[key] * ratio : defaultValue; + }; + + c3_chart_internal_fn.generateTickValues = function (xs, tickCount) { + var $$ = this; + var tickValues = xs, targetCount, start, end, count, interval, i, tickValue; + if (tickCount) { + targetCount = isFunction(tickCount) ? tickCount() : tickCount; + // compute ticks according to $$.config.axis_x_tick_count + if (targetCount === 1) { + tickValues = [xs[0]]; + } else if (targetCount === 2) { + tickValues = [xs[0], xs[xs.length - 1]]; + } else if (targetCount > 2) { + count = targetCount - 2; + start = xs[0]; + end = xs[xs.length - 1]; + interval = (end - start) / (count + 1); + // re-construct uniqueXs + tickValues = [start]; + for (i = 0; i < count; i++) { + tickValue = +start + interval * (i + 1); + tickValues.push($$.isTimeSeries() ? new Date(tickValue) : tickValue); + } + tickValues.push(end); + } + } + if (!$$.isTimeSeries()) { tickValues = tickValues.sort(function (a, b) { return a - b; }); } + return tickValues; + }; + c3_chart_internal_fn.generateAxisTransitions = function (duration) { + var $$ = this, axes = $$.axes; + return { + axisX: duration ? axes.x.transition().duration(duration) : axes.x, + axisY: duration ? axes.y.transition().duration(duration) : axes.y, + axisY2: duration ? axes.y2.transition().duration(duration) : axes.y2, + axisSubX: duration ? axes.subx.transition().duration(duration) : axes.subx + }; + }; + c3_chart_internal_fn.redrawAxis = function (transitions, isHidden) { + var $$ = this; + $$.axes.x.style("opacity", isHidden ? 0 : 1); + $$.axes.y.style("opacity", isHidden ? 0 : 1); + $$.axes.y2.style("opacity", isHidden ? 0 : 1); + $$.axes.subx.style("opacity", isHidden ? 0 : 1); + transitions.axisX.call($$.xAxis); + transitions.axisY.call($$.yAxis); + transitions.axisY2.call($$.y2Axis); + transitions.axisSubX.call($$.subXAxis); + }; + + c3_chart_internal_fn.getClipPath = function (id) { + var isIE9 = window.navigator.appVersion.toLowerCase().indexOf("msie 9.") >= 0; + return "url(" + (isIE9 ? "" : document.URL.split('#')[0]) + "#" + id + ")"; + }; + c3_chart_internal_fn.getAxisClipX = function (forHorizontal) { + // axis line width + padding for left + return forHorizontal ? -(1 + 30) : -(this.margin.left - 1); + }; + c3_chart_internal_fn.getAxisClipY = function (forHorizontal) { + return forHorizontal ? -20 : -4; + }; + c3_chart_internal_fn.getXAxisClipX = function () { + var $$ = this; + return $$.getAxisClipX(!$$.config.axis_rotated); + }; + c3_chart_internal_fn.getXAxisClipY = function () { + var $$ = this; + return $$.getAxisClipY(!$$.config.axis_rotated); + }; + c3_chart_internal_fn.getYAxisClipX = function () { + var $$ = this; + return $$.getAxisClipX($$.config.axis_rotated); + }; + c3_chart_internal_fn.getYAxisClipY = function () { + var $$ = this; + return $$.getAxisClipY($$.config.axis_rotated); + }; + c3_chart_internal_fn.getAxisClipWidth = function (forHorizontal) { + var $$ = this; + // width + axis line width + padding for left/right + return forHorizontal ? $$.width + 2 + 30 + 30 : $$.margin.left + 20; + }; + c3_chart_internal_fn.getAxisClipHeight = function (forHorizontal) { + var $$ = this, config = $$.config; + return forHorizontal ? (config.axis_x_height ? config.axis_x_height : 0) + 80 : $$.height + 8; + }; + c3_chart_internal_fn.getXAxisClipWidth = function () { + var $$ = this; + return $$.getAxisClipWidth(!$$.config.axis_rotated); + }; + c3_chart_internal_fn.getXAxisClipHeight = function () { + var $$ = this; + return $$.getAxisClipHeight(!$$.config.axis_rotated); + }; + c3_chart_internal_fn.getYAxisClipWidth = function () { + var $$ = this; + return $$.getAxisClipWidth($$.config.axis_rotated); + }; + c3_chart_internal_fn.getYAxisClipHeight = function () { + var $$ = this; + return $$.getAxisClipHeight($$.config.axis_rotated); + }; + + c3_chart_internal_fn.initPie = function () { + var $$ = this, d3 = $$.d3, config = $$.config; + $$.pie = d3.layout.pie().value(function (d) { + return d.values.reduce(function (a, b) { return a + b.value; }, 0); + }); + if (!config.data_order || !config.pie_sort || !config.donut_sort) { + $$.pie.sort(null); + } + }; + + c3_chart_internal_fn.updateRadius = function () { + var $$ = this, config = $$.config, + w = config.gauge_width || config.donut_width; + $$.radiusExpanded = Math.min($$.arcWidth, $$.arcHeight) / 2; + $$.radius = $$.radiusExpanded * 0.95; + $$.innerRadiusRatio = w ? ($$.radius - w) / $$.radius : 0.6; + $$.innerRadius = $$.hasType('donut') || $$.hasType('gauge') ? $$.radius * $$.innerRadiusRatio : 0; + }; + + c3_chart_internal_fn.updateArc = function () { + var $$ = this; + $$.svgArc = $$.getSvgArc(); + $$.svgArcExpanded = $$.getSvgArcExpanded(); + $$.svgArcExpandedSub = $$.getSvgArcExpanded(0.98); + }; + + c3_chart_internal_fn.updateAngle = function (d) { + var $$ = this, config = $$.config, + found = false, index = 0; + $$.pie($$.filterTargetsToShow($$.data.targets)).sort($$.descByStartAngle).forEach(function (t) { + if (! found && t.data.id === d.data.id) { + found = true; + d = t; + d.index = index; + } + index++; + }); + if (isNaN(d.endAngle)) { + d.endAngle = d.startAngle; + } + if ($$.isGaugeType(d.data)) { + var gMin = config.gauge_min, gMax = config.gauge_max, + gF = Math.abs(gMin) + gMax, + aTic = (Math.PI) / gF; + d.startAngle = (-1 * (Math.PI / 2)) + (aTic * Math.abs(gMin)); + d.endAngle = d.startAngle + (aTic * ((d.value > gMax) ? gMax : d.value)); + } + return found ? d : null; + }; + + c3_chart_internal_fn.getSvgArc = function () { + var $$ = this, + arc = $$.d3.svg.arc().outerRadius($$.radius).innerRadius($$.innerRadius), + newArc = function (d, withoutUpdate) { + var updated; + if (withoutUpdate) { return arc(d); } // for interpolate + updated = $$.updateAngle(d); + return updated ? arc(updated) : "M 0 0"; + }; + // TODO: extends all function + newArc.centroid = arc.centroid; + return newArc; + }; + + c3_chart_internal_fn.getSvgArcExpanded = function (rate) { + var $$ = this, + arc = $$.d3.svg.arc().outerRadius($$.radiusExpanded * (rate ? rate : 1)).innerRadius($$.innerRadius); + return function (d) { + var updated = $$.updateAngle(d); + return updated ? arc(updated) : "M 0 0"; + }; + }; + + c3_chart_internal_fn.getArc = function (d, withoutUpdate, force) { + return force || this.isArcType(d.data) ? this.svgArc(d, withoutUpdate) : "M 0 0"; + }; + + + c3_chart_internal_fn.transformForArcLabel = function (d) { + var $$ = this, + updated = $$.updateAngle(d), c, x, y, h, ratio, translate = ""; + if (updated && !$$.hasType('gauge')) { + c = this.svgArc.centroid(updated); + x = isNaN(c[0]) ? 0 : c[0]; + y = isNaN(c[1]) ? 0 : c[1]; + h = Math.sqrt(x * x + y * y); + // TODO: ratio should be an option? + ratio = $$.radius && h ? (36 / $$.radius > 0.375 ? 1.175 - 36 / $$.radius : 0.8) * $$.radius / h : 0; + translate = "translate(" + (x * ratio) + ',' + (y * ratio) + ")"; + } + return translate; + }; + + c3_chart_internal_fn.getArcRatio = function (d) { + var $$ = this, + whole = $$.hasType('gauge') ? Math.PI : (Math.PI * 2); + return d ? (d.endAngle - d.startAngle) / whole : null; + }; + + c3_chart_internal_fn.convertToArcData = function (d) { + return this.addName({ + id: d.data.id, + value: d.value, + ratio: this.getArcRatio(d), + index: d.index + }); + }; + + c3_chart_internal_fn.textForArcLabel = function (d) { + var $$ = this, + updated, value, ratio, format; + if (! $$.shouldShowArcLabel()) { return ""; } + updated = $$.updateAngle(d); + value = updated ? updated.value : null; + ratio = $$.getArcRatio(updated); + if (! $$.hasType('gauge') && ! $$.meetsArcLabelThreshold(ratio)) { return ""; } + format = $$.getArcLabelFormat(); + return format ? format(value, ratio) : $$.defaultArcValueFormat(value, ratio); + }; + + c3_chart_internal_fn.expandArc = function (id, withoutFadeOut) { + var $$ = this, + target = $$.svg.selectAll('.' + CLASS.chartArc + $$.selectorTarget(id)), + noneTargets = $$.svg.selectAll('.' + CLASS.arc).filter(function (data) { return data.data.id !== id; }); + + if ($$.shouldExpand(id)) { + target.selectAll('path') + .transition().duration(50) + .attr("d", $$.svgArcExpanded) + .transition().duration(100) + .attr("d", $$.svgArcExpandedSub) + .each(function (d) { + if ($$.isDonutType(d.data)) { + // callback here + } + }); + } + if (!withoutFadeOut) { + noneTargets.style("opacity", 0.3); + } + }; + + c3_chart_internal_fn.unexpandArc = function (id) { + var $$ = this, + target = $$.svg.selectAll('.' + CLASS.chartArc + $$.selectorTarget(id)); + target.selectAll('path.' + CLASS.arc) + .transition().duration(50) + .attr("d", $$.svgArc); + $$.svg.selectAll('.' + CLASS.arc) + .style("opacity", 1); + }; + + c3_chart_internal_fn.shouldExpand = function (id) { + var $$ = this, config = $$.config; + return ($$.isDonutType(id) && config.donut_expand) || ($$.isGaugeType(id) && config.gauge_expand) || ($$.isPieType(id) && config.pie_expand); + }; + + c3_chart_internal_fn.shouldShowArcLabel = function () { + var $$ = this, config = $$.config, shouldShow = true; + if ($$.hasType('donut')) { + shouldShow = config.donut_label_show; + } else if ($$.hasType('pie')) { + shouldShow = config.pie_label_show; + } + // when gauge, always true + return shouldShow; + }; + + c3_chart_internal_fn.meetsArcLabelThreshold = function (ratio) { + var $$ = this, config = $$.config, + threshold = $$.hasType('donut') ? config.donut_label_threshold : config.pie_label_threshold; + return ratio >= threshold; + }; + + c3_chart_internal_fn.getArcLabelFormat = function () { + var $$ = this, config = $$.config, + format = config.pie_label_format; + if ($$.hasType('gauge')) { + format = config.gauge_label_format; + } else if ($$.hasType('donut')) { + format = config.donut_label_format; + } + return format; + }; + + c3_chart_internal_fn.getArcTitle = function () { + var $$ = this; + return $$.hasType('donut') ? $$.config.donut_title : ""; + }; + + c3_chart_internal_fn.descByStartAngle = function (a, b) { + return a.startAngle - b.startAngle; + }; + + c3_chart_internal_fn.updateTargetsForArc = function (targets) { + var $$ = this, main = $$.main, + mainPieUpdate, mainPieEnter, + classChartArc = $$.classChartArc.bind($$), + classArcs = $$.classArcs.bind($$); + mainPieUpdate = main.select('.' + CLASS.chartArcs).selectAll('.' + CLASS.chartArc) + .data($$.pie(targets)) + .attr("class", classChartArc); + mainPieEnter = mainPieUpdate.enter().append("g") + .attr("class", classChartArc); + mainPieEnter.append('g') + .attr('class', classArcs); + mainPieEnter.append("text") + .attr("dy", $$.hasType('gauge') ? "-0.35em" : ".35em") + .style("opacity", 0) + .style("text-anchor", "middle") + .style("pointer-events", "none"); + // MEMO: can not keep same color..., but not bad to update color in redraw + //mainPieUpdate.exit().remove(); + }; + + c3_chart_internal_fn.initArc = function () { + var $$ = this; + $$.arcs = $$.main.select('.' + CLASS.chart).append("g") + .attr("class", CLASS.chartArcs) + .attr("transform", $$.getTranslate('arc')); + $$.arcs.append('text') + .attr('class', CLASS.chartArcsTitle) + .style("text-anchor", "middle") + .text($$.getArcTitle()); + }; + + c3_chart_internal_fn.redrawArc = function (duration, durationForExit, withTransform) { + var $$ = this, d3 = $$.d3, config = $$.config, main = $$.main, + mainArc; + mainArc = main.selectAll('.' + CLASS.arcs).selectAll('.' + CLASS.arc) + .data($$.arcData.bind($$)); + mainArc.enter().append('path') + .attr("class", $$.classArc.bind($$)) + .style("fill", function (d) { return $$.color(d.data); }) + .style("cursor", function (d) { return config.data_selection_isselectable(d) ? "pointer" : null; }) + .style("opacity", 0) + .each(function (d) { + if ($$.isGaugeType(d.data)) { + d.startAngle = d.endAngle = -1 * (Math.PI / 2); + } + this._current = d; + }) + .on('mouseover', function (d) { + var updated, arcData; + if ($$.transiting) { // skip while transiting + return; + } + updated = $$.updateAngle(d); + arcData = $$.convertToArcData(updated); + // transitions + $$.expandArc(updated.data.id); + $$.toggleFocusLegend(updated.data.id, true); + $$.config.data_onmouseover(arcData, this); + }) + .on('mousemove', function (d) { + var updated = $$.updateAngle(d), + arcData = $$.convertToArcData(updated), + selectedData = [arcData]; + $$.showTooltip(selectedData, d3.mouse(this)); + }) + .on('mouseout', function (d) { + var updated, arcData; + if ($$.transiting) { // skip while transiting + return; + } + updated = $$.updateAngle(d); + arcData = $$.convertToArcData(updated); + // transitions + $$.unexpandArc(updated.data.id); + $$.revertLegend(); + $$.hideTooltip(); + $$.config.data_onmouseout(arcData, this); + }) + .on('click', function (d, i) { + var updated, arcData; + if (!$$.toggleShape) { + return; + } + updated = $$.updateAngle(d); + arcData = $$.convertToArcData(updated); + $$.toggleShape(this, arcData, i); // onclick called in toogleShape() + }); + mainArc + .attr("transform", function (d) { return !$$.isGaugeType(d.data) && withTransform ? "scale(0)" : ""; }) + .style("opacity", function (d) { return d === this._current ? 0 : 1; }) + .each(function () { $$.transiting = true; }) + .transition().duration(duration) + .attrTween("d", function (d) { + var updated = $$.updateAngle(d), interpolate; + if (! updated) { + return function () { return "M 0 0"; }; + } + // if (this._current === d) { + // this._current = { + // startAngle: Math.PI*2, + // endAngle: Math.PI*2, + // }; + // } + if (isNaN(this._current.endAngle)) { + this._current.endAngle = this._current.startAngle; + } + interpolate = d3.interpolate(this._current, updated); + this._current = interpolate(0); + return function (t) { return $$.getArc(interpolate(t), true); }; + }) + .attr("transform", withTransform ? "scale(1)" : "") + .style("fill", function (d) { + return $$.levelColor ? $$.levelColor(d.data.values[0].value) : $$.color(d.data.id); + }) // Where gauge reading color would receive customization. + .style("opacity", 1) + .call($$.endall, function () { + $$.transiting = false; + }); + mainArc.exit().transition().duration(durationForExit) + .style('opacity', 0) + .remove(); + main.selectAll('.' + CLASS.chartArc).select('text') + .style("opacity", 0) + .attr('class', function (d) { return $$.isGaugeType(d.data) ? CLASS.gaugeValue : ''; }) + .text($$.textForArcLabel.bind($$)) + .attr("transform", $$.transformForArcLabel.bind($$)) + .transition().duration(duration) + .style("opacity", function (d) { return $$.isTargetToShow(d.data.id) && $$.isArcType(d.data) ? 1 : 0; }); + main.select('.' + CLASS.chartArcsTitle) + .style("opacity", $$.hasType('donut') || $$.hasType('gauge') ? 1 : 0); + + }; + c3_chart_internal_fn.initGauge = function () { + var $$ = this, config = $$.config, arcs = $$.arcs; + if ($$.hasType('gauge')) { + arcs.append('path') + .attr("class", CLASS.chartArcsBackground) + .attr("d", function () { + var d = { + data: [{value: config.gauge_max}], + startAngle: -1 * (Math.PI / 2), + endAngle: Math.PI / 2 + }; + return $$.getArc(d, true, true); + }); + arcs.append("text") + .attr("dy", ".75em") + .attr("class", CLASS.chartArcsGaugeUnit) + .style("text-anchor", "middle") + .style("pointer-events", "none") + .text(config.gauge_label_show ? config.gauge_units : ''); + arcs.append("text") + .attr("dx", -1 * ($$.innerRadius + (($$.radius - $$.innerRadius) / 2)) + "px") + .attr("dy", "1.2em") + .attr("class", CLASS.chartArcsGaugeMin) + .style("text-anchor", "middle") + .style("pointer-events", "none") + .text(config.gauge_label_show ? config.gauge_min : ''); + arcs.append("text") + .attr("dx", $$.innerRadius + (($$.radius - $$.innerRadius) / 2) + "px") + .attr("dy", "1.2em") + .attr("class", CLASS.chartArcsGaugeMax) + .style("text-anchor", "middle") + .style("pointer-events", "none") + .text(config.gauge_label_show ? config.gauge_max : ''); + } + }; + c3_chart_internal_fn.getGaugeLabelHeight = function () { + return this.config.gauge_label_show ? 20 : 0; + }; + + c3_chart_internal_fn.initRegion = function () { + var $$ = this; + $$.main.append('g') + .attr("clip-path", $$.clipPath) + .attr("class", CLASS.regions); + }; + c3_chart_internal_fn.redrawRegion = function (duration) { + var $$ = this, config = $$.config; + $$.mainRegion = $$.main.select('.' + CLASS.regions).selectAll('.' + CLASS.region) + .data(config.regions); + $$.mainRegion.enter().append('g') + .attr('class', $$.classRegion.bind($$)) + .append('rect') + .style("fill-opacity", 0); + $$.mainRegion.exit().transition().duration(duration) + .style("opacity", 0) + .remove(); + }; + c3_chart_internal_fn.addTransitionForRegion = function (transitions) { + var $$ = this, + x = $$.regionX.bind($$), + y = $$.regionY.bind($$), + w = $$.regionWidth.bind($$), + h = $$.regionHeight.bind($$); + transitions.push($$.mainRegion.selectAll('rect').transition() + .attr("x", x) + .attr("y", y) + .attr("width", w) + .attr("height", h) + .style("fill-opacity", function (d) { return isValue(d.opacity) ? d.opacity : 0.1; })); + }; + c3_chart_internal_fn.regionX = function (d) { + var $$ = this, config = $$.config, + xPos, yScale = d.axis === 'y' ? $$.y : $$.y2; + if (d.axis === 'y' || d.axis === 'y2') { + xPos = config.axis_rotated ? ('start' in d ? yScale(d.start) : 0) : 0; + } else { + xPos = config.axis_rotated ? 0 : ('start' in d ? $$.x($$.isTimeSeries() ? $$.parseDate(d.start) : d.start) : 0); + } + return xPos; + }; + c3_chart_internal_fn.regionY = function (d) { + var $$ = this, config = $$.config, + yPos, yScale = d.axis === 'y' ? $$.y : $$.y2; + if (d.axis === 'y' || d.axis === 'y2') { + yPos = config.axis_rotated ? 0 : ('end' in d ? yScale(d.end) : 0); + } else { + yPos = config.axis_rotated ? ('start' in d ? $$.x($$.isTimeSeries() ? $$.parseDate(d.start) : d.start) : 0) : 0; + } + return yPos; + }; + c3_chart_internal_fn.regionWidth = function (d) { + var $$ = this, config = $$.config, + start = $$.regionX(d), end, yScale = d.axis === 'y' ? $$.y : $$.y2; + if (d.axis === 'y' || d.axis === 'y2') { + end = config.axis_rotated ? ('end' in d ? yScale(d.end) : $$.width) : $$.width; + } else { + end = config.axis_rotated ? $$.width : ('end' in d ? $$.x($$.isTimeSeries() ? $$.parseDate(d.end) : d.end) : $$.width); + } + return end < start ? 0 : end - start; + }; + c3_chart_internal_fn.regionHeight = function (d) { + var $$ = this, config = $$.config, + start = this.regionY(d), end, yScale = d.axis === 'y' ? $$.y : $$.y2; + if (d.axis === 'y' || d.axis === 'y2') { + end = config.axis_rotated ? $$.height : ('start' in d ? yScale(d.start) : $$.height); + } else { + end = config.axis_rotated ? ('end' in d ? $$.x($$.isTimeSeries() ? $$.parseDate(d.end) : d.end) : $$.height) : $$.height; + } + return end < start ? 0 : end - start; + }; + c3_chart_internal_fn.isRegionOnX = function (d) { + return !d.axis || d.axis === 'x'; + }; + + c3_chart_internal_fn.drag = function (mouse) { + var $$ = this, config = $$.config, main = $$.main, d3 = $$.d3; + var sx, sy, mx, my, minX, maxX, minY, maxY; + + if ($$.hasArcType()) { return; } + if (! config.data_selection_enabled) { return; } // do nothing if not selectable + if (config.zoom_enabled && ! $$.zoom.altDomain) { return; } // skip if zoomable because of conflict drag dehavior + if (!config.data_selection_multiple) { return; } // skip when single selection because drag is used for multiple selection + + sx = $$.dragStart[0]; + sy = $$.dragStart[1]; + mx = mouse[0]; + my = mouse[1]; + minX = Math.min(sx, mx); + maxX = Math.max(sx, mx); + minY = (config.data_selection_grouped) ? $$.margin.top : Math.min(sy, my); + maxY = (config.data_selection_grouped) ? $$.height : Math.max(sy, my); + + main.select('.' + CLASS.dragarea) + .attr('x', minX) + .attr('y', minY) + .attr('width', maxX - minX) + .attr('height', maxY - minY); + // TODO: binary search when multiple xs + main.selectAll('.' + CLASS.shapes).selectAll('.' + CLASS.shape) + .filter(function (d) { return config.data_selection_isselectable(d); }) + .each(function (d, i) { + var shape = d3.select(this), + isSelected = shape.classed(CLASS.SELECTED), + isIncluded = shape.classed(CLASS.INCLUDED), + _x, _y, _w, _h, toggle, isWithin = false, box; + if (shape.classed(CLASS.circle)) { + _x = shape.attr("cx") * 1; + _y = shape.attr("cy") * 1; + toggle = $$.togglePoint; + isWithin = minX < _x && _x < maxX && minY < _y && _y < maxY; + } + else if (shape.classed(CLASS.bar)) { + box = getPathBox(this); + _x = box.x; + _y = box.y; + _w = box.width; + _h = box.height; + toggle = $$.toggleBar; + isWithin = !(maxX < _x || _x + _w < minX) && !(maxY < _y || _y + _h < minY); + } else { + // line/area selection not supported yet + return; + } + if (isWithin ^ isIncluded) { + shape.classed(CLASS.INCLUDED, !isIncluded); + // TODO: included/unincluded callback here + shape.classed(CLASS.SELECTED, !isSelected); + toggle.call($$, !isSelected, shape, d, i); + } + }); + }; + + c3_chart_internal_fn.dragstart = function (mouse) { + var $$ = this, config = $$.config; + if ($$.hasArcType()) { return; } + if (! config.data_selection_enabled) { return; } // do nothing if not selectable + $$.dragStart = mouse; + $$.main.select('.' + CLASS.chart).append('rect') + .attr('class', CLASS.dragarea) + .style('opacity', 0.1); + $$.dragging = true; + $$.config.data_ondragstart(); + }; + + c3_chart_internal_fn.dragend = function () { + var $$ = this, config = $$.config; + if ($$.hasArcType()) { return; } + if (! config.data_selection_enabled) { return; } // do nothing if not selectable + $$.main.select('.' + CLASS.dragarea) + .transition().duration(100) + .style('opacity', 0) + .remove(); + $$.main.selectAll('.' + CLASS.shape) + .classed(CLASS.INCLUDED, false); + $$.dragging = false; + $$.config.data_ondragend(); + }; + + + c3_chart_internal_fn.selectPoint = function (target, d, i) { + var $$ = this, config = $$.config, + cx = (config.axis_rotated ? $$.circleY : $$.circleX).bind($$), + cy = (config.axis_rotated ? $$.circleX : $$.circleY).bind($$), + r = $$.pointSelectR.bind($$); + config.data_onselected.call($$.api, d, target.node()); + // add selected-circle on low layer g + $$.main.select('.' + CLASS.selectedCircles + $$.getTargetSelectorSuffix(d.id)).selectAll('.' + CLASS.selectedCircle + '-' + i) + .data([d]) + .enter().append('circle') + .attr("class", function () { return $$.generateClass(CLASS.selectedCircle, i); }) + .attr("cx", cx) + .attr("cy", cy) + .attr("stroke", function () { return $$.color(d); }) + .attr("r", function (d) { return $$.pointSelectR(d) * 1.4; }) + .transition().duration(100) + .attr("r", r); + }; + c3_chart_internal_fn.unselectPoint = function (target, d, i) { + var $$ = this; + $$.config.data_onunselected(d, target.node()); + // remove selected-circle from low layer g + $$.main.select('.' + CLASS.selectedCircles + $$.getTargetSelectorSuffix(d.id)).selectAll('.' + CLASS.selectedCircle + '-' + i) + .transition().duration(100).attr('r', 0) + .remove(); + }; + c3_chart_internal_fn.togglePoint = function (selected, target, d, i) { + selected ? this.selectPoint(target, d, i) : this.unselectPoint(target, d, i); + }; + c3_chart_internal_fn.selectBar = function (target, d) { + var $$ = this; + $$.config.data_onselected.call($$, d, target.node()); + target.transition().duration(100) + .style("fill", function () { return $$.d3.rgb($$.color(d)).brighter(0.75); }); + }; + c3_chart_internal_fn.unselectBar = function (target, d) { + var $$ = this; + $$.config.data_onunselected.call($$, d, target.node()); + target.transition().duration(100) + .style("fill", function () { return $$.color(d); }); + }; + c3_chart_internal_fn.toggleBar = function (selected, target, d, i) { + selected ? this.selectBar(target, d, i) : this.unselectBar(target, d, i); + }; + c3_chart_internal_fn.toggleArc = function (selected, target, d, i) { + this.toggleBar(selected, target, d.data, i); + }; + c3_chart_internal_fn.getToggle = function (that) { + var $$ = this; + // path selection not supported yet + return that.nodeName === 'circle' ? $$.togglePoint : ($$.d3.select(that).classed(CLASS.bar) ? $$.toggleBar : $$.toggleArc); + }; + c3_chart_internal_fn.toggleShape = function (that, d, i) { + var $$ = this, d3 = $$.d3, config = $$.config, + shape = d3.select(that), isSelected = shape.classed(CLASS.SELECTED), isWithin, toggle; + if (that.nodeName === 'circle') { + if ($$.isStepType(d)) { + // circle is hidden in step chart, so treat as within the click area + isWithin = true; + toggle = function () {}; // TODO: how to select step chart? + } else { + isWithin = $$.isWithinCircle(that, $$.pointSelectR(d) * 1.5); + toggle = $$.togglePoint; + } + } + else if (that.nodeName === 'path') { + if (shape.classed(CLASS.bar)) { + isWithin = $$.isWithinBar(that); + toggle = $$.toggleBar; + } else { // would be arc + isWithin = true; + toggle = $$.toggleArc; + } + } + if (config.data_selection_grouped || isWithin) { + if (config.data_selection_enabled && config.data_selection_isselectable(d)) { + if (!config.data_selection_multiple) { + $$.main.selectAll('.' + CLASS.shapes + (config.data_selection_grouped ? $$.getTargetSelectorSuffix(d.id) : "")).selectAll('.' + CLASS.shape).each(function (d, i) { + var shape = d3.select(this); + if (shape.classed(CLASS.SELECTED)) { toggle.call($$, false, shape.classed(CLASS.SELECTED, false), d, i); } + }); + } + shape.classed(CLASS.SELECTED, !isSelected); + toggle.call($$, !isSelected, shape, d, i); + } + $$.config.data_onclick.call($$.api, d, that); + } + }; + + c3_chart_internal_fn.initBrush = function () { + var $$ = this, d3 = $$.d3; + $$.brush = d3.svg.brush().on("brush", function () { $$.redrawForBrush(); }); + $$.brush.update = function () { + if ($$.context) { $$.context.select('.' + CLASS.brush).call(this); } + return this; + }; + $$.brush.scale = function (scale) { + return $$.config.axis_rotated ? this.y(scale) : this.x(scale); + }; + }; + c3_chart_internal_fn.initSubchart = function () { + var $$ = this, config = $$.config, + context = $$.context = $$.svg.append("g").attr("transform", $$.getTranslate('context')); + + if (!config.subchart_show) { + context.style('visibility', 'hidden'); + } + + // Define g for chart area + context.append('g') + .attr("clip-path", $$.clipPath) + .attr('class', CLASS.chart); + + // Define g for bar chart area + context.select('.' + CLASS.chart).append("g") + .attr("class", CLASS.chartBars); + + // Define g for line chart area + context.select('.' + CLASS.chart).append("g") + .attr("class", CLASS.chartLines); + + // Add extent rect for Brush + context.append("g") + .attr("clip-path", $$.clipPath) + .attr("class", CLASS.brush) + .call($$.brush) + .selectAll("rect") + .attr(config.axis_rotated ? "width" : "height", config.axis_rotated ? $$.width2 : $$.height2); + + // ATTENTION: This must be called AFTER chart added + // Add Axis + $$.axes.subx = context.append("g") + .attr("class", CLASS.axisX) + .attr("transform", $$.getTranslate('subx')) + .attr("clip-path", config.axis_rotated ? "" : $$.clipPathForXAxis); + }; + c3_chart_internal_fn.updateTargetsForSubchart = function (targets) { + var $$ = this, context = $$.context, config = $$.config, + contextLineEnter, contextLineUpdate, contextBarEnter, contextBarUpdate, + classChartBar = $$.classChartBar.bind($$), + classBars = $$.classBars.bind($$), + classChartLine = $$.classChartLine.bind($$), + classLines = $$.classLines.bind($$), + classAreas = $$.classAreas.bind($$); + + if (config.subchart_show) { + contextBarUpdate = context.select('.' + CLASS.chartBars).selectAll('.' + CLASS.chartBar) + .data(targets) + .attr('class', classChartBar); + contextBarEnter = contextBarUpdate.enter().append('g') + .style('opacity', 0) + .attr('class', classChartBar); + // Bars for each data + contextBarEnter.append('g') + .attr("class", classBars); + + //-- Line --// + contextLineUpdate = context.select('.' + CLASS.chartLines).selectAll('.' + CLASS.chartLine) + .data(targets) + .attr('class', classChartLine); + contextLineEnter = contextLineUpdate.enter().append('g') + .style('opacity', 0) + .attr('class', classChartLine); + // Lines for each data + contextLineEnter.append("g") + .attr("class", classLines); + // Area + contextLineEnter.append("g") + .attr("class", classAreas); + } + }; + c3_chart_internal_fn.redrawSubchart = function (withSubchart, transitions, duration, durationForExit, areaIndices, barIndices, lineIndices) { + var $$ = this, d3 = $$.d3, context = $$.context, config = $$.config, + contextLine, contextArea, contextBar, drawAreaOnSub, drawBarOnSub, drawLineOnSub, + barData = $$.barData.bind($$), + lineData = $$.lineData.bind($$), + classBar = $$.classBar.bind($$), + classLine = $$.classLine.bind($$), + classArea = $$.classArea.bind($$), + initialOpacity = $$.initialOpacity.bind($$); + + // subchart + if (config.subchart_show) { + // reflect main chart to extent on subchart if zoomed + if (d3.event && d3.event.type === 'zoom') { + $$.brush.extent($$.x.orgDomain()).update(); + } + // update subchart elements if needed + if (withSubchart) { + + // rotate tick text if needed + if (!config.axis_rotated && config.axis_x_tick_rotate) { + $$.rotateTickText($$.axes.subx, transitions.axisSubX, config.axis_x_tick_rotate); + } + + // extent rect + if (!$$.brush.empty()) { + $$.brush.extent($$.x.orgDomain()).update(); + } + // setup drawer - MEMO: this must be called after axis updated + drawAreaOnSub = $$.generateDrawArea(areaIndices, true); + drawBarOnSub = $$.generateDrawBar(barIndices, true); + drawLineOnSub = $$.generateDrawLine(lineIndices, true); + // bars + contextBar = context.selectAll('.' + CLASS.bars).selectAll('.' + CLASS.bar) + .data(barData); + contextBar.enter().append('path') + .attr("class", classBar) + .style("stroke", 'none') + .style("fill", $$.color); + contextBar + .style("opacity", initialOpacity) + .transition().duration(duration) + .attr('d', drawBarOnSub) + .style('opacity', 1); + contextBar.exit().transition().duration(duration) + .style('opacity', 0) + .remove(); + // lines + contextLine = context.selectAll('.' + CLASS.lines).selectAll('.' + CLASS.line) + .data(lineData); + contextLine.enter().append('path') + .attr('class', classLine) + .style('stroke', $$.color); + contextLine + .style("opacity", initialOpacity) + .transition().duration(duration) + .attr("d", drawLineOnSub) + .style('opacity', 1); + contextLine.exit().transition().duration(duration) + .style('opacity', 0) + .remove(); + // area + contextArea = context.selectAll('.' + CLASS.areas).selectAll('.' + CLASS.area) + .data(lineData); + contextArea.enter().append('path') + .attr("class", classArea) + .style("fill", $$.color) + .style("opacity", function () { $$.orgAreaOpacity = +d3.select(this).style('opacity'); return 0; }); + contextArea + .style("opacity", 0) + .transition().duration(duration) + .attr("d", drawAreaOnSub) + .style("fill", $$.color) + .style("opacity", $$.orgAreaOpacity); + contextArea.exit().transition().duration(durationForExit) + .style('opacity', 0) + .remove(); + } + } + }; + c3_chart_internal_fn.redrawForBrush = function () { + var $$ = this, x = $$.x; + $$.redraw({ + withTransition: false, + withY: false, + withSubchart: false, + withUpdateXDomain: true + }); + $$.config.subchart_onbrush.call($$.api, x.orgDomain()); + }; + c3_chart_internal_fn.transformContext = function (withTransition, transitions) { + var $$ = this, subXAxis; + if (transitions && transitions.axisSubX) { + subXAxis = transitions.axisSubX; + } else { + subXAxis = $$.context.select('.' + CLASS.axisX); + if (withTransition) { subXAxis = subXAxis.transition(); } + } + $$.context.attr("transform", $$.getTranslate('context')); + subXAxis.attr("transform", $$.getTranslate('subx')); + }; + + c3_chart_internal_fn.initZoom = function () { + var $$ = this, d3 = $$.d3, config = $$.config; + $$.zoom = d3.behavior.zoom() + .on("zoomstart", function () { + $$.zoom.altDomain = d3.event.sourceEvent.altKey ? $$.x.orgDomain() : null; + }) + .on("zoom", function () { $$.redrawForZoom.call($$); }); + $$.zoom.scale = function (scale) { + return config.axis_rotated ? this.y(scale) : this.x(scale); + }; + $$.zoom.orgScaleExtent = function () { + var extent = config.zoom_extent ? config.zoom_extent : [1, 10]; + return [extent[0], Math.max($$.getMaxDataCount() / extent[1], extent[1])]; + }; + $$.zoom.updateScaleExtent = function () { + var ratio = diffDomain($$.x.orgDomain()) / diffDomain($$.orgXDomain), + extent = this.orgScaleExtent(); + this.scaleExtent([extent[0] * ratio, extent[1] * ratio]); + return this; + }; + }; + c3_chart_internal_fn.updateZoom = function () { + var $$ = this, z = $$.config.zoom_enabled ? $$.zoom : function () {}; + $$.main.select('.' + CLASS.zoomRect).call(z); + $$.main.selectAll('.' + CLASS.eventRect).call(z); + }; + c3_chart_internal_fn.redrawForZoom = function () { + var $$ = this, d3 = $$.d3, config = $$.config, zoom = $$.zoom, x = $$.x, orgXDomain = $$.orgXDomain; + if (!config.zoom_enabled) { + return; + } + if ($$.filterTargetsToShow($$.data.targets).length === 0) { + return; + } + if (d3.event.sourceEvent.type === 'mousemove' && zoom.altDomain) { + x.domain(zoom.altDomain); + zoom.scale(x).updateScaleExtent(); + return; + } + if ($$.isCategorized() && x.orgDomain()[0] === orgXDomain[0]) { + x.domain([orgXDomain[0] - 1e-10, x.orgDomain()[1]]); + } + $$.redraw({ + withTransition: false, + withY: false, + withSubchart: false + }); + if (d3.event.sourceEvent.type === 'mousemove') { + $$.cancelClick = true; + } + config.zoom_onzoom.call($$.api, x.orgDomain()); + }; + + c3_chart_internal_fn.generateColor = function () { + var $$ = this, config = $$.config, d3 = $$.d3, + colors = config.data_colors, + pattern = notEmpty(config.color_pattern) ? config.color_pattern : d3.scale.category10().range(), + callback = config.data_color, + ids = []; + + return function (d) { + var id = d.id || d, color; + + // if callback function is provided + if (colors[id] instanceof Function) { + color = colors[id](d); + } + // if specified, choose that color + else if (colors[id]) { + color = colors[id]; + } + // if not specified, choose from pattern + else { + if (ids.indexOf(id) < 0) { ids.push(id); } + color = pattern[ids.indexOf(id) % pattern.length]; + colors[id] = color; + } + return callback instanceof Function ? callback(color, d) : color; + }; + }; + c3_chart_internal_fn.generateLevelColor = function () { + var $$ = this, config = $$.config, + colors = config.color_pattern, + threshold = config.color_threshold, + asValue = threshold.unit === 'value', + values = threshold.values && threshold.values.length ? threshold.values : [], + max = threshold.max || 100; + return notEmpty(config.color_threshold) ? function (value) { + var i, v, color = colors[colors.length - 1]; + for (i = 0; i < values.length; i++) { + v = asValue ? value : (value * 100 / max); + if (v < values[i]) { + color = colors[i]; + break; + } + } + return color; + } : null; + }; + + c3_chart_internal_fn.getYFormat = function (forArc) { + var $$ = this, + formatForY = forArc && !$$.hasType('gauge') ? $$.defaultArcValueFormat : $$.yFormat, + formatForY2 = forArc && !$$.hasType('gauge') ? $$.defaultArcValueFormat : $$.y2Format; + return function (v, ratio, id) { + var format = $$.getAxisId(id) === 'y2' ? formatForY2 : formatForY; + return format.call($$, v, ratio); + }; + }; + c3_chart_internal_fn.yFormat = function (v) { + var $$ = this, config = $$.config, + format = config.axis_y_tick_format ? config.axis_y_tick_format : $$.defaultValueFormat; + return format(v); + }; + c3_chart_internal_fn.y2Format = function (v) { + var $$ = this, config = $$.config, + format = config.axis_y2_tick_format ? config.axis_y2_tick_format : $$.defaultValueFormat; + return format(v); + }; + c3_chart_internal_fn.defaultValueFormat = function (v) { + return isValue(v) ? +v : ""; + }; + c3_chart_internal_fn.defaultArcValueFormat = function (v, ratio) { + return (ratio * 100).toFixed(1) + '%'; + }; + c3_chart_internal_fn.formatByAxisId = function (axisId) { + var $$ = this, data_labels = $$.config.data_labels, + format = function (v) { return isValue(v) ? +v : ""; }; + // find format according to axis id + if (typeof data_labels.format === 'function') { + format = data_labels.format; + } else if (typeof data_labels.format === 'object') { + if (data_labels.format[axisId]) { + format = data_labels.format[axisId]; + } + } + return format; + }; + + c3_chart_internal_fn.hasCaches = function (ids) { + for (var i = 0; i < ids.length; i++) { + if (! (ids[i] in this.cache)) { return false; } + } + return true; + }; + c3_chart_internal_fn.addCache = function (id, target) { + this.cache[id] = this.cloneTarget(target); + }; + c3_chart_internal_fn.getCaches = function (ids) { + var targets = [], i; + for (i = 0; i < ids.length; i++) { + if (ids[i] in this.cache) { targets.push(this.cloneTarget(this.cache[ids[i]])); } + } + return targets; + }; + + var CLASS = c3_chart_internal_fn.CLASS = { + target: 'c3-target', + chart: 'c3-chart', + chartLine: 'c3-chart-line', + chartLines: 'c3-chart-lines', + chartBar: 'c3-chart-bar', + chartBars: 'c3-chart-bars', + chartText: 'c3-chart-text', + chartTexts: 'c3-chart-texts', + chartArc: 'c3-chart-arc', + chartArcs: 'c3-chart-arcs', + chartArcsTitle: 'c3-chart-arcs-title', + chartArcsBackground: 'c3-chart-arcs-background', + chartArcsGaugeUnit: 'c3-chart-arcs-gauge-unit', + chartArcsGaugeMax: 'c3-chart-arcs-gauge-max', + chartArcsGaugeMin: 'c3-chart-arcs-gauge-min', + selectedCircle: 'c3-selected-circle', + selectedCircles: 'c3-selected-circles', + eventRect: 'c3-event-rect', + eventRects: 'c3-event-rects', + eventRectsSingle: 'c3-event-rects-single', + eventRectsMultiple: 'c3-event-rects-multiple', + zoomRect: 'c3-zoom-rect', + brush: 'c3-brush', + focused: 'c3-focused', + region: 'c3-region', + regions: 'c3-regions', + tooltip: 'c3-tooltip', + tooltipName: 'c3-tooltip-name', + shape: 'c3-shape', + shapes: 'c3-shapes', + line: 'c3-line', + lines: 'c3-lines', + bar: 'c3-bar', + bars: 'c3-bars', + circle: 'c3-circle', + circles: 'c3-circles', + arc: 'c3-arc', + arcs: 'c3-arcs', + area: 'c3-area', + areas: 'c3-areas', + empty: 'c3-empty', + text: 'c3-text', + texts: 'c3-texts', + gaugeValue: 'c3-gauge-value', + grid: 'c3-grid', + gridLines: 'c3-grid-lines', + xgrid: 'c3-xgrid', + xgrids: 'c3-xgrids', + xgridLine: 'c3-xgrid-line', + xgridLines: 'c3-xgrid-lines', + xgridFocus: 'c3-xgrid-focus', + ygrid: 'c3-ygrid', + ygrids: 'c3-ygrids', + ygridLine: 'c3-ygrid-line', + ygridLines: 'c3-ygrid-lines', + axis: 'c3-axis', + axisX: 'c3-axis-x', + axisXLabel: 'c3-axis-x-label', + axisY: 'c3-axis-y', + axisYLabel: 'c3-axis-y-label', + axisY2: 'c3-axis-y2', + axisY2Label: 'c3-axis-y2-label', + legendBackground: 'c3-legend-background', + legendItem: 'c3-legend-item', + legendItemEvent: 'c3-legend-item-event', + legendItemTile: 'c3-legend-item-tile', + legendItemHidden: 'c3-legend-item-hidden', + legendItemFocused: 'c3-legend-item-focused', + dragarea: 'c3-dragarea', + EXPANDED: '_expanded_', + SELECTED: '_selected_', + INCLUDED: '_included_' + }; + c3_chart_internal_fn.generateClass = function (prefix, targetId) { + return " " + prefix + " " + prefix + this.getTargetSelectorSuffix(targetId); + }; + c3_chart_internal_fn.classText = function (d) { + return this.generateClass(CLASS.text, d.index); + }; + c3_chart_internal_fn.classTexts = function (d) { + return this.generateClass(CLASS.texts, d.id); + }; + c3_chart_internal_fn.classShape = function (d) { + return this.generateClass(CLASS.shape, d.index); + }; + c3_chart_internal_fn.classShapes = function (d) { + return this.generateClass(CLASS.shapes, d.id); + }; + c3_chart_internal_fn.classLine = function (d) { + return this.classShape(d) + this.generateClass(CLASS.line, d.id); + }; + c3_chart_internal_fn.classLines = function (d) { + return this.classShapes(d) + this.generateClass(CLASS.lines, d.id); + }; + c3_chart_internal_fn.classCircle = function (d) { + return this.classShape(d) + this.generateClass(CLASS.circle, d.index); + }; + c3_chart_internal_fn.classCircles = function (d) { + return this.classShapes(d) + this.generateClass(CLASS.circles, d.id); + }; + c3_chart_internal_fn.classBar = function (d) { + return this.classShape(d) + this.generateClass(CLASS.bar, d.index); + }; + c3_chart_internal_fn.classBars = function (d) { + return this.classShapes(d) + this.generateClass(CLASS.bars, d.id); + }; + c3_chart_internal_fn.classArc = function (d) { + return this.classShape(d.data) + this.generateClass(CLASS.arc, d.data.id); + }; + c3_chart_internal_fn.classArcs = function (d) { + return this.classShapes(d.data) + this.generateClass(CLASS.arcs, d.data.id); + }; + c3_chart_internal_fn.classArea = function (d) { + return this.classShape(d) + this.generateClass(CLASS.area, d.id); + }; + c3_chart_internal_fn.classAreas = function (d) { + return this.classShapes(d) + this.generateClass(CLASS.areas, d.id); + }; + c3_chart_internal_fn.classRegion = function (d, i) { + return this.generateClass(CLASS.region, i) + ' ' + ('class' in d ? d.class : ''); + }; + c3_chart_internal_fn.classEvent = function (d) { + return this.generateClass(CLASS.eventRect, d.index); + }; + c3_chart_internal_fn.classTarget = function (id) { + var $$ = this; + var additionalClassSuffix = $$.config.data_classes[id], additionalClass = ''; + if (additionalClassSuffix) { + additionalClass = ' ' + CLASS.target + '-' + additionalClassSuffix; + } + return $$.generateClass(CLASS.target, id) + additionalClass; + }; + c3_chart_internal_fn.classChartText = function (d) { + return CLASS.chartText + this.classTarget(d.id); + }; + c3_chart_internal_fn.classChartLine = function (d) { + return CLASS.chartLine + this.classTarget(d.id); + }; + c3_chart_internal_fn.classChartBar = function (d) { + return CLASS.chartBar + this.classTarget(d.id); + }; + c3_chart_internal_fn.classChartArc = function (d) { + return CLASS.chartArc + this.classTarget(d.data.id); + }; + c3_chart_internal_fn.getTargetSelectorSuffix = function (targetId) { + return targetId || targetId === 0 ? '-' + (targetId.replace ? targetId.replace(/([^a-zA-Z0-9-_])/g, '-') : targetId) : ''; + }; + c3_chart_internal_fn.selectorTarget = function (id) { + return '.' + CLASS.target + this.getTargetSelectorSuffix(id); + }; + c3_chart_internal_fn.selectorTargets = function (ids) { + var $$ = this; + return ids.length ? ids.map(function (id) { return $$.selectorTarget(id); }) : null; + }; + c3_chart_internal_fn.selectorLegend = function (id) { + return '.' + CLASS.legendItem + this.getTargetSelectorSuffix(id); + }; + c3_chart_internal_fn.selectorLegends = function (ids) { + var $$ = this; + return ids.length ? ids.map(function (id) { return $$.selectorLegend(id); }) : null; + }; + + var isValue = c3_chart_internal_fn.isValue = function (v) { + return v || v === 0; + }, + isFunction = c3_chart_internal_fn.isFunction = function (o) { + return typeof o === 'function'; + }, + isString = c3_chart_internal_fn.isString = function (o) { + return typeof o === 'string'; + }, + isUndefined = c3_chart_internal_fn.isUndefined = function (v) { + return typeof v === 'undefined'; + }, + isDefined = c3_chart_internal_fn.isDefined = function (v) { + return typeof v !== 'undefined'; + }, + ceil10 = c3_chart_internal_fn.ceil10 = function (v) { + return Math.ceil(v / 10) * 10; + }, + asHalfPixel = c3_chart_internal_fn.asHalfPixel = function (n) { + return Math.ceil(n) + 0.5; + }, + diffDomain = c3_chart_internal_fn.diffDomain = function (d) { + return d[1] - d[0]; + }, + isEmpty = c3_chart_internal_fn.isEmpty = function (o) { + return !o || (isString(o) && o.length === 0) || (typeof o === 'object' && Object.keys(o).length === 0); + }, + notEmpty = c3_chart_internal_fn.notEmpty = function (o) { + return Object.keys(o).length > 0; + }, + getOption = c3_chart_internal_fn.getOption = function (options, key, defaultValue) { + return isDefined(options[key]) ? options[key] : defaultValue; + }, + hasValue = c3_chart_internal_fn.hasValue = function (dict, value) { + var found = false; + Object.keys(dict).forEach(function (key) { + if (dict[key] === value) { found = true; } + }); + return found; + }, + getPathBox = c3_chart_internal_fn.getPathBox = function (path) { + var box = path.getBoundingClientRect(), + items = [path.pathSegList.getItem(0), path.pathSegList.getItem(1)], + minX = items[0].x, minY = Math.min(items[0].y, items[1].y); + return {x: minX, y: minY, width: box.width, height: box.height}; + }; + + c3_chart_fn.focus = function (targetId) { + var $$ = this.internal, + candidates = $$.svg.selectAll($$.selectorTarget(targetId)), + candidatesForNoneArc = candidates.filter($$.isNoneArc.bind($$)), + candidatesForArc = candidates.filter($$.isArc.bind($$)); + function focus(targets) { + $$.filterTargetsToShow(targets).transition().duration(100).style('opacity', 1); + } + this.revert(); + this.defocus(); + focus(candidatesForNoneArc.classed(CLASS.focused, true)); + focus(candidatesForArc); + if ($$.hasArcType()) { + $$.expandArc(targetId, true); + } + $$.toggleFocusLegend(targetId, true); + }; + + c3_chart_fn.defocus = function (targetId) { + var $$ = this.internal, + candidates = $$.svg.selectAll($$.selectorTarget(targetId)), + candidatesForNoneArc = candidates.filter($$.isNoneArc.bind($$)), + candidatesForArc = candidates.filter($$.isArc.bind($$)); + function defocus(targets) { + $$.filterTargetsToShow(targets).transition().duration(100).style('opacity', 0.3); + } + this.revert(); + defocus(candidatesForNoneArc.classed(CLASS.focused, false)); + defocus(candidatesForArc); + if ($$.hasArcType()) { + $$.unexpandArc(targetId); + } + $$.toggleFocusLegend(targetId, false); + }; + + c3_chart_fn.revert = function (targetId) { + var $$ = this.internal, + candidates = $$.svg.selectAll($$.selectorTarget(targetId)), + candidatesForNoneArc = candidates.filter($$.isNoneArc.bind($$)), + candidatesForArc = candidates.filter($$.isArc.bind($$)); + function revert(targets) { + $$.filterTargetsToShow(targets).transition().duration(100).style('opacity', 1); + } + revert(candidatesForNoneArc.classed(CLASS.focused, false)); + revert(candidatesForArc); + if ($$.hasArcType()) { + $$.unexpandArc(targetId); + } + $$.revertLegend(); + }; + + c3_chart_fn.show = function (targetIds, options) { + var $$ = this.internal; + + targetIds = $$.mapToTargetIds(targetIds); + options = options || {}; + + $$.removeHiddenTargetIds(targetIds); + $$.svg.selectAll($$.selectorTargets(targetIds)) + .transition() + .style('opacity', 1); + + if (options.withLegend) { + $$.showLegend(targetIds); + } + + $$.redraw({withUpdateOrgXDomain: true, withUpdateXDomain: true, withLegend: true}); + }; + + c3_chart_fn.hide = function (targetIds, options) { + var $$ = this.internal; + + targetIds = $$.mapToTargetIds(targetIds); + options = options || {}; + + $$.addHiddenTargetIds(targetIds); + $$.svg.selectAll($$.selectorTargets(targetIds)) + .transition() + .style('opacity', 0); + + if (options.withLegend) { + $$.hideLegend(targetIds); + } + + $$.redraw({withUpdateOrgXDomain: true, withUpdateXDomain: true, withLegend: true}); + }; + + c3_chart_fn.toggle = function (targetId) { + var $$ = this.internal; + $$.isTargetToShow(targetId) ? this.hide(targetId) : this.show(targetId); + }; + + c3_chart_fn.zoom = function () { + }; + c3_chart_fn.zoom.enable = function (enabled) { + var $$ = this.internal; + $$.config.zoom_enabled = enabled; + $$.updateAndRedraw(); + }; + c3_chart_fn.unzoom = function () { + var $$ = this.internal; + $$.brush.clear().update(); + $$.redraw({withUpdateXDomain: true}); + }; + + c3_chart_fn.load = function (args) { + var $$ = this.internal, config = $$.config; + // update xs if specified + if (args.xs) { + $$.addXs(args.xs); + } + // update classes if exists + if ('classes' in args) { + Object.keys(args.classes).forEach(function (id) { + config.data_classes[id] = args.classes[id]; + }); + } + // update categories if exists + if ('categories' in args && $$.isCategorized()) { + config.axis_x_categories = args.categories; + } + // use cache if exists + if ('cacheIds' in args && $$.hasCaches(args.cacheIds)) { + $$.load($$.getCaches(args.cacheIds), args.done); + return; + } + // unload if needed + if ('unload' in args) { + // TODO: do not unload if target will load (included in url/rows/columns) + $$.unload($$.mapToTargetIds((typeof args.unload === 'boolean' && args.unload) ? null : args.unload), function () { + $$.loadFromArgs(args); + }); + } else { + $$.loadFromArgs(args); + } + }; + + c3_chart_fn.unload = function (args) { + var $$ = this.internal; + args = args || {}; + $$.unload($$.mapToTargetIds(args.ids), function () { + $$.redraw({withUpdateOrgXDomain: true, withUpdateXDomain: true, withLegend: true}); + if (args.done) { args.done(); } + }); + }; + + c3_chart_fn.flow = function (args) { + var $$ = this.internal, + targets, data, notfoundIds = [], orgDataCount = $$.getMaxDataCount(), + dataCount, domain, baseTarget, baseValue, length = 0, tail = 0, diff, to; + + if (args.json) { + data = $$.convertJsonToData(args.json, args.keys); + } + else if (args.rows) { + data = $$.convertRowsToData(args.rows); + } + else if (args.columns) { + data = $$.convertColumnsToData(args.columns); + } + else { + return; + } + targets = $$.convertDataToTargets(data, true); + + // Update/Add data + $$.data.targets.forEach(function (t) { + var found = false, i, j; + for (i = 0; i < targets.length; i++) { + if (t.id === targets[i].id) { + found = true; + + if (t.values[t.values.length - 1]) { + tail = t.values[t.values.length - 1].index + 1; + } + length = targets[i].values.length; + + for (j = 0; j < length; j++) { + targets[i].values[j].index = tail + j; + if (!$$.isTimeSeries()) { + targets[i].values[j].x = tail + j; + } + } + t.values = t.values.concat(targets[i].values); + + targets.splice(i, 1); + break; + } + } + if (!found) { notfoundIds.push(t.id); } + }); + + // Append null for not found targets + $$.data.targets.forEach(function (t) { + var i, j; + for (i = 0; i < notfoundIds.length; i++) { + if (t.id === notfoundIds[i]) { + tail = t.values[t.values.length - 1].index + 1; + for (j = 0; j < length; j++) { + t.values.push({ + id: t.id, + index: tail + j, + x: $$.isTimeSeries() ? $$.getOtherTargetX(tail + j) : tail + j, + value: null + }); + } + } + } + }); + + // Generate null values for new target + if ($$.data.targets.length) { + targets.forEach(function (t) { + var i, missing = []; + for (i = $$.data.targets[0].values[0].index; i < tail; i++) { + missing.push({ + id: t.id, + index: i, + x: $$.isTimeSeries() ? $$.getOtherTargetX(i) : i, + value: null + }); + } + t.values.forEach(function (v) { + v.index += tail; + if (!$$.isTimeSeries()) { + v.x += tail; + } + }); + t.values = missing.concat(t.values); + }); + } + $$.data.targets = $$.data.targets.concat(targets); // add remained + + // check data count because behavior needs to change when it's only one + dataCount = $$.getMaxDataCount(); + baseTarget = $$.data.targets[0]; + baseValue = baseTarget.values[0]; + + // Update length to flow if needed + if (isDefined(args.to)) { + length = 0; + to = $$.isTimeSeries() ? $$.parseDate(args.to) : args.to; + baseTarget.values.forEach(function (v) { + if (v.x < to) { length++; } + }); + } else if (isDefined(args.length)) { + length = args.length; + } + + // If only one data, update the domain to flow from left edge of the chart + if (!orgDataCount) { + if ($$.isTimeSeries()) { + if (baseTarget.values.length > 1) { + diff = baseTarget.values[baseTarget.values.length - 1].x - baseValue.x; + } else { + diff = baseValue.x - $$.getXDomain($$.data.targets)[0]; + } + } else { + diff = 1; + } + domain = [baseValue.x - diff, baseValue.x]; + $$.updateXDomain(null, true, true, domain); + } else if (orgDataCount === 1) { + if ($$.isTimeSeries()) { + diff = (baseTarget.values[baseTarget.values.length - 1].x - baseValue.x) / 2; + domain = [new Date(+baseValue.x - diff), new Date(+baseValue.x + diff)]; + $$.updateXDomain(null, true, true, domain); + } + } + + // Set targets + $$.updateTargets($$.data.targets); + + // Redraw with new targets + $$.redraw({ + flow: { + index: baseValue.index, + length: length, + duration: isValue(args.duration) ? args.duration : $$.config.transition_duration, + done: args.done, + orgDataCount: orgDataCount, + }, + withLegend: true, + withTransition: orgDataCount > 1, + }); + }; + + c3_chart_internal_fn.generateFlow = function (args) { + var $$ = this, config = $$.config, d3 = $$.d3; + + return function () { + var targets = args.targets, + flow = args.flow, + drawBar = args.drawBar, + drawLine = args.drawLine, + drawArea = args.drawArea, + cx = args.cx, + cy = args.cy, + xv = args.xv, + xForText = args.xForText, + yForText = args.yForText, + duration = args.duration; + + var translateX, scaleX = 1, transform, + flowIndex = flow.index, + flowLength = flow.length, + flowStart = $$.getValueOnIndex($$.data.targets[0].values, flowIndex), + flowEnd = $$.getValueOnIndex($$.data.targets[0].values, flowIndex + flowLength), + orgDomain = $$.x.domain(), domain, + durationForFlow = flow.duration || duration, + done = flow.done || function () {}, + wait = $$.generateWait(); + + var xgrid = $$.xgrid || d3.selectAll([]), + xgridLines = $$.xgridLines || d3.selectAll([]), + mainRegion = $$.mainRegion || d3.selectAll([]), + mainText = $$.mainText || d3.selectAll([]), + mainBar = $$.mainBar || d3.selectAll([]), + mainLine = $$.mainLine || d3.selectAll([]), + mainArea = $$.mainArea || d3.selectAll([]), + mainCircle = $$.mainCircle || d3.selectAll([]); + + // remove head data after rendered + $$.data.targets.forEach(function (d) { + d.values.splice(0, flowLength); + }); + + // update x domain to generate axis elements for flow + domain = $$.updateXDomain(targets, true, true); + // update elements related to x scale + if ($$.updateXGrid) { $$.updateXGrid(true); } + + // generate transform to flow + if (!flow.orgDataCount) { // if empty + if ($$.data.targets[0].values.length !== 1) { + translateX = $$.x(orgDomain[0]) - $$.x(domain[0]); + } else { + if ($$.isTimeSeries()) { + flowStart = $$.getValueOnIndex($$.data.targets[0].values, 0); + flowEnd = $$.getValueOnIndex($$.data.targets[0].values, $$.data.targets[0].values.length - 1); + translateX = $$.x(flowStart.x) - $$.x(flowEnd.x); + } else { + translateX = diffDomain(domain) / 2; + } + } + } else if (flow.orgDataCount === 1 || flowStart.x === flowEnd.x) { + translateX = $$.x(orgDomain[0]) - $$.x(domain[0]); + } else { + if ($$.isTimeSeries()) { + translateX = ($$.x(orgDomain[0]) - $$.x(domain[0])); + } else { + translateX = ($$.x(flowStart.x) - $$.x(flowEnd.x)); + } + } + scaleX = (diffDomain(orgDomain) / diffDomain(domain)); + transform = 'translate(' + translateX + ',0) scale(' + scaleX + ',1)'; + + d3.transition().ease('linear').duration(durationForFlow).each(function () { + wait.add($$.axes.x.transition().call($$.xAxis)); + wait.add(mainBar.transition().attr('transform', transform)); + wait.add(mainLine.transition().attr('transform', transform)); + wait.add(mainArea.transition().attr('transform', transform)); + wait.add(mainCircle.transition().attr('transform', transform)); + wait.add(mainText.transition().attr('transform', transform)); + wait.add(mainRegion.filter($$.isRegionOnX).transition().attr('transform', transform)); + wait.add(xgrid.transition().attr('transform', transform)); + wait.add(xgridLines.transition().attr('transform', transform)); + }) + .call(wait, function () { + var i, shapes = [], texts = [], eventRects = []; + + // remove flowed elements + if (flowLength) { + for (i = 0; i < flowLength; i++) { + shapes.push('.' + CLASS.shape + '-' + (flowIndex + i)); + texts.push('.' + CLASS.text + '-' + (flowIndex + i)); + eventRects.push('.' + CLASS.eventRect + '-' + (flowIndex + i)); + } + $$.svg.selectAll('.' + CLASS.shapes).selectAll(shapes).remove(); + $$.svg.selectAll('.' + CLASS.texts).selectAll(texts).remove(); + $$.svg.selectAll('.' + CLASS.eventRects).selectAll(eventRects).remove(); + $$.svg.select('.' + CLASS.xgrid).remove(); + } + + // draw again for removing flowed elements and reverting attr + xgrid + .attr('transform', null) + .attr($$.xgridAttr); + xgridLines + .attr('transform', null); + xgridLines.select('line') + .attr("x1", config.axis_rotated ? 0 : xv) + .attr("x2", config.axis_rotated ? $$.width : xv); + xgridLines.select('text') + .attr("x", config.axis_rotated ? $$.width : 0) + .attr("y", xv); + mainBar + .attr('transform', null) + .attr("d", drawBar); + mainLine + .attr('transform', null) + .attr("d", drawLine); + mainArea + .attr('transform', null) + .attr("d", drawArea); + mainCircle + .attr('transform', null) + .attr("cx", cx) + .attr("cy", cy); + mainText + .attr('transform', null) + .attr('x', xForText) + .attr('y', yForText) + .style('fill-opacity', $$.opacityForText.bind($$)); + mainRegion + .attr('transform', null); + mainRegion.select('rect').filter($$.isRegionOnX) + .attr("x", $$.regionX.bind($$)) + .attr("width", $$.regionWidth.bind($$)); + $$.updateEventRect(); + + // callback for end of flow + done(); + }); + }; + }; + + c3_chart_fn.selected = function (targetId) { + var $$ = this.internal, d3 = $$.d3; + return d3.merge( + $$.main.selectAll('.' + CLASS.shapes + $$.getTargetSelectorSuffix(targetId)).selectAll('.' + CLASS.shape) + .filter(function () { return d3.select(this).classed(CLASS.SELECTED); }) + .map(function (d) { return d.map(function (d) { var data = d.__data__; return data.data ? data.data : data; }); }) + ); + }; + c3_chart_fn.select = function (ids, indices, resetOther) { + var $$ = this.internal, d3 = $$.d3, config = $$.config; + if (! config.data_selection_enabled) { return; } + $$.main.selectAll('.' + CLASS.shapes).selectAll('.' + CLASS.shape).each(function (d, i) { + var shape = d3.select(this), id = d.data ? d.data.id : d.id, + toggle = $$.getToggle(this).bind($$), + isTargetId = config.data_selection_grouped || !ids || ids.indexOf(id) >= 0, + isTargetIndex = !indices || indices.indexOf(i) >= 0, + isSelected = shape.classed(CLASS.SELECTED); + // line/area selection not supported yet + if (shape.classed(CLASS.line) || shape.classed(CLASS.area)) { + return; + } + if (isTargetId && isTargetIndex) { + if (config.data_selection_isselectable(d) && !isSelected) { + toggle(true, shape.classed(CLASS.SELECTED, true), d, i); + } + } else if (isDefined(resetOther) && resetOther) { + if (isSelected) { + toggle(false, shape.classed(CLASS.SELECTED, false), d, i); + } + } + }); + }; + c3_chart_fn.unselect = function (ids, indices) { + var $$ = this.internal, d3 = $$.d3, config = $$.config; + if (! config.data_selection_enabled) { return; } + $$.main.selectAll('.' + CLASS.shapes).selectAll('.' + CLASS.shape).each(function (d, i) { + var shape = d3.select(this), id = d.data ? d.data.id : d.id, + toggle = $$.getToggle(this).bind($$), + isTargetId = config.data_selection_grouped || !ids || ids.indexOf(id) >= 0, + isTargetIndex = !indices || indices.indexOf(i) >= 0, + isSelected = shape.classed(CLASS.SELECTED); + // line/area selection not supported yet + if (shape.classed(CLASS.line) || shape.classed(CLASS.area)) { + return; + } + if (isTargetId && isTargetIndex) { + if (config.data_selection_isselectable(d)) { + if (isSelected) { + toggle(false, shape.classed(CLASS.SELECTED, false), d, i); + } + } + } + }); + }; + + c3_chart_fn.transform = function (type, targetIds) { + var $$ = this.internal, + options = ['pie', 'donut'].indexOf(type) >= 0 ? {withTransform: true} : null; + $$.transformTo(targetIds, type, options); + }; + + c3_chart_internal_fn.transformTo = function (targetIds, type, optionsForRedraw) { + var $$ = this, + withTransitionForAxis = !$$.hasArcType(), + options = optionsForRedraw || {withTransitionForAxis: withTransitionForAxis}; + options.withTransitionForTransform = false; + $$.transiting = false; + $$.setTargetType(targetIds, type); + $$.updateAndRedraw(options); + }; + + c3_chart_fn.groups = function (groups) { + var $$ = this.internal, config = $$.config; + if (isUndefined(groups)) { return config.data_groups; } + config.data_groups = groups; + $$.redraw(); + return config.data_groups; + }; + + c3_chart_fn.xgrids = function (grids) { + var $$ = this.internal, config = $$.config; + if (! grids) { return config.grid_x_lines; } + config.grid_x_lines = grids; + $$.redraw(); + return config.grid_x_lines; + }; + c3_chart_fn.xgrids.add = function (grids) { + var $$ = this.internal; + return this.xgrids($$.config.grid_x_lines.concat(grids ? grids : [])); + }; + c3_chart_fn.xgrids.remove = function (params) { // TODO: multiple + var $$ = this.internal; + $$.removeGridLines(params, true); + }; + + c3_chart_fn.ygrids = function (grids) { + var $$ = this.internal, config = $$.config; + if (! grids) { return config.grid_y_lines; } + config.grid_y_lines = grids; + $$.redraw(); + return config.grid_y_lines; + }; + c3_chart_fn.ygrids.add = function (grids) { + var $$ = this.internal; + return this.ygrids($$.config.grid_y_lines.concat(grids ? grids : [])); + }; + c3_chart_fn.ygrids.remove = function (params) { // TODO: multiple + var $$ = this.internal; + $$.removeGridLines(params, false); + }; + + c3_chart_fn.regions = function (regions) { + var $$ = this.internal, config = $$.config; + if (!regions) { return config.regions; } + config.regions = regions; + $$.redraw(); + return config.regions; + }; + c3_chart_fn.regions.add = function (regions) { + var $$ = this.internal, config = $$.config; + if (!regions) { return config.regions; } + config.regions = config.regions.concat(regions); + $$.redraw(); + return config.regions; + }; + c3_chart_fn.regions.remove = function (options) { + var $$ = this.internal, config = $$.config, + duration, classes, regions; + + options = options || {}; + duration = $$.getOption(options, "duration", config.transition_duration); + classes = $$.getOption(options, "classes", [CLASS.region]); + + regions = $$.main.select('.' + CLASS.regions).selectAll(classes.map(function (c) { return '.' + c; })); + (duration ? regions.transition().duration(duration) : regions) + .style('opacity', 0) + .remove(); + + config.regions = config.regions.filter(function (region) { + var found = false; + if (!region.class) { + return true; + } + region.class.split(' ').forEach(function (c) { + if (classes.indexOf(c) >= 0) { found = true; } + }); + return !found; + }); + + return config.regions; + }; + + c3_chart_fn.data = function () {}; + c3_chart_fn.data.get = function (targetId) { + var target = this.data.getAsTarget(targetId); + return isDefined(target) ? target.values.map(function (d) { return d.value; }) : undefined; + }; + c3_chart_fn.data.getAsTarget = function (targetId) { + var targets = this.data.targets.filter(function (t) { return t.id === targetId; }); + return targets.length > 0 ? targets[0] : undefined; + }; + c3_chart_fn.data.names = function (names) { + var $$ = this.internal, config = $$.config; + if (!arguments.length) { return config.data_names; } + Object.keys(names).forEach(function (id) { + config.data_names[id] = names[id]; + }); + $$.redraw({withLegend: true}); + return config.data_names; + }; + c3_chart_fn.data.colors = function (colors) { + var $$ = this.internal, config = $$.config; + if (!arguments.length) { return config.data_colors; } + Object.keys(colors).forEach(function (id) { + config.data_colors[id] = colors[id]; + }); + $$.redraw({withLegend: true}); + return config.data_colors; + }; + + c3_chart_fn.category = function (i, category) { + var $$ = this.internal, config = $$.config; + if (arguments.length > 1) { + config.axis_x_categories[i] = category; + $$.redraw(); + } + return config.axis_x_categories[i]; + }; + c3_chart_fn.categories = function (categories) { + var $$ = this.internal, config = $$.config; + if (!arguments.length) { return config.axis_x_categories; } + config.axis_x_categories = categories; + $$.redraw(); + return config.axis_x_categories; + }; + + // TODO: fix + c3_chart_fn.color = function (id) { + var $$ = this.internal; + return $$.color(id); // more patterns + }; + + c3_chart_fn.x = function (x) { + var $$ = this.internal; + if (arguments.length) { + $$.updateTargetX($$.data.targets, x); + $$.redraw({withUpdateOrgXDomain: true, withUpdateXDomain: true}); + } + return $$.data.xs; + }; + c3_chart_fn.xs = function (xs) { + var $$ = this.internal; + if (arguments.length) { + $$.updateTargetXs($$.data.targets, xs); + $$.redraw({withUpdateOrgXDomain: true, withUpdateXDomain: true}); + } + return $$.data.xs; + }; + + c3_chart_fn.axis = function () {}; + c3_chart_fn.axis.labels = function (labels) { + var $$ = this.internal; + if (arguments.length) { + Object.keys(labels).forEach(function (axisId) { + $$.setAxisLabelText(axisId, labels[axisId]); + }); + $$.updateAxisLabels(); + } + // TODO: return some values? + }; + c3_chart_fn.axis.max = function (max) { + var $$ = this.internal, config = $$.config; + if (arguments.length) { + if (typeof max === 'object') { + if (isValue(max.x)) { config.axis_x_max = max.x; } + if (isValue(max.y)) { config.axis_y_max = max.y; } + if (isValue(max.y2)) { config.axis_y2_max = max.y2; } + } else { + config.axis_y_max = config.axis_y2_max = max; + } + $$.redraw({withUpdateOrgXDomain: true, withUpdateXDomain: true}); + } + }; + c3_chart_fn.axis.min = function (min) { + var $$ = this.internal, config = $$.config; + if (arguments.length) { + if (typeof min === 'object') { + if (isValue(min.x)) { config.axis_x_min = min.x; } + if (isValue(min.y)) { config.axis_y_min = min.y; } + if (isValue(min.y2)) { config.axis_y2_min = min.y2; } + } else { + config.axis_y_min = config.axis_y2_min = min; + } + $$.redraw({withUpdateOrgXDomain: true, withUpdateXDomain: true}); + } + }; + c3_chart_fn.axis.range = function (range) { + if (arguments.length) { + if (isDefined(range.max)) { this.axis.max(range.max); } + if (isDefined(range.min)) { this.axis.min(range.min); } + } + }; + + c3_chart_fn.legend = function () {}; + c3_chart_fn.legend.show = function (targetIds) { + var $$ = this.internal; + $$.showLegend($$.mapToTargetIds(targetIds)); + $$.updateAndRedraw({withLegend: true}); + }; + c3_chart_fn.legend.hide = function (targetIds) { + var $$ = this.internal; + $$.hideLegend($$.mapToTargetIds(targetIds)); + $$.updateAndRedraw({withLegend: true}); + }; + + c3_chart_fn.resize = function (size) { + var $$ = this.internal, config = $$.config; + config.size_width = size ? size.width : null; + config.size_height = size ? size.height : null; + this.flush(); + }; + + c3_chart_fn.flush = function () { + var $$ = this.internal; + $$.updateAndRedraw({withLegend: true, withTransition: false, withTransitionForTransform: false}); + }; + + c3_chart_fn.destroy = function () { + var $$ = this.internal; + $$.data.targets = undefined; + $$.data.xs = {}; + $$.selectChart.classed('c3', false).html(""); + window.onresize = null; + }; + + c3_chart_fn.tooltip = function () {}; + c3_chart_fn.tooltip.show = function (args) { + var $$ = this.internal, index, mouse; + + // determine mouse position on the chart + if (args.mouse) { + mouse = args.mouse; + } + + // determine focus data + if (args.data) { + if ($$.isMultipleX()) { + // if multiple xs, target point will be determined by mouse + mouse = [$$.x(args.data.x), $$.getYScale(args.data.id)(args.data.value)]; + index = null; + } else { + // TODO: when tooltip_grouped = false + index = isValue(args.data.index) ? args.data.index : $$.getIndexByX(args.data.x); + } + } + else if (args.x) { + index = $$.getIndexByX(args.x); + } + else if (args.index) { + index = args.index; + } + + // emulate mouse events to show + $$.dispatchEvent('mouseover', index, mouse); + $$.dispatchEvent('mousemove', index, mouse); + }; + c3_chart_fn.tooltip.hide = function () { + // TODO: get target data by checking the state of focus + this.internal.dispatchEvent('mouseout', 0); + }; + + // Features: + // 1. category axis + // 2. ceil values of translate/x/y to int for half pixel antialiasing + function c3_axis(d3, isCategory) { + var scale = d3.scale.linear(), orient = "bottom", innerTickSize = 6, outerTickSize = 6, tickPadding = 3, tickValues = null, tickFormat, tickArguments; + + var tickOffset = 0, tickCulling = true, tickCentered; + + function axisX(selection, x) { + selection.attr("transform", function (d) { + return "translate(" + Math.ceil(x(d) + tickOffset) + ", 0)"; + }); + } + function axisY(selection, y) { + selection.attr("transform", function (d) { + return "translate(0," + Math.ceil(y(d)) + ")"; + }); + } + function scaleExtent(domain) { + var start = domain[0], stop = domain[domain.length - 1]; + return start < stop ? [ start, stop ] : [ stop, start ]; + } + function generateTicks(scale) { + var i, domain, ticks = []; + if (scale.ticks) { + return scale.ticks.apply(scale, tickArguments); + } + domain = scale.domain(); + for (i = Math.ceil(domain[0]); i < domain[1]; i++) { + ticks.push(i); + } + if (ticks.length > 0 && ticks[0] > 0) { + ticks.unshift(ticks[0] - (ticks[1] - ticks[0])); + } + return ticks; + } + function copyScale() { + var newScale = scale.copy(), domain; + if (isCategory) { + domain = scale.domain(); + newScale.domain([domain[0], domain[1] - 1]); + } + return newScale; + } + function textFormatted(v) { + return tickFormat ? tickFormat(v) : v; + } + function axis(g) { + g.each(function () { + var g = d3.select(this); + var scale0 = this.__chart__ || scale, scale1 = this.__chart__ = copyScale(); + + var ticks = tickValues ? tickValues : generateTicks(scale1), + tick = g.selectAll(".tick").data(ticks, scale1), + tickEnter = tick.enter().insert("g", ".domain").attr("class", "tick").style("opacity", 1e-6), + // MEMO: No exit transition. The reason is this transition affects max tick width calculation because old tick will be included in the ticks. + tickExit = tick.exit().remove(), + tickUpdate = d3.transition(tick).style("opacity", 1), + tickTransform, tickX; + + var range = scale.rangeExtent ? scale.rangeExtent() : scaleExtent(scale.range()), + path = g.selectAll(".domain").data([ 0 ]), + pathUpdate = (path.enter().append("path").attr("class", "domain"), d3.transition(path)); + tickEnter.append("line"); + tickEnter.append("text"); + + var lineEnter = tickEnter.select("line"), + lineUpdate = tickUpdate.select("line"), + text = tick.select("text").text(textFormatted), + textEnter = tickEnter.select("text"), + textUpdate = tickUpdate.select("text"); + + if (isCategory) { + tickOffset = Math.ceil((scale1(1) - scale1(0)) / 2); + tickX = tickCentered ? 0 : tickOffset; + } else { + tickOffset = tickX = 0; + } + + function tickSize(d) { + var tickPosition = scale(d) + tickOffset; + return range[0] < tickPosition && tickPosition < range[1] ? innerTickSize : 0; + } + + switch (orient) { + case "bottom": + { + tickTransform = axisX; + lineEnter.attr("y2", innerTickSize); + textEnter.attr("y", Math.max(innerTickSize, 0) + tickPadding); + lineUpdate.attr("x1", tickX).attr("x2", tickX).attr("y2", tickSize); + textUpdate.attr("x", 0).attr("y", Math.max(innerTickSize, 0) + tickPadding); + text.attr("dy", ".71em").style("text-anchor", "middle"); + pathUpdate.attr("d", "M" + range[0] + "," + outerTickSize + "V0H" + range[1] + "V" + outerTickSize); + break; + } + case "top": + { + tickTransform = axisX; + lineEnter.attr("y2", -innerTickSize); + textEnter.attr("y", -(Math.max(innerTickSize, 0) + tickPadding)); + lineUpdate.attr("x2", 0).attr("y2", -innerTickSize); + textUpdate.attr("x", 0).attr("y", -(Math.max(innerTickSize, 0) + tickPadding)); + text.attr("dy", "0em").style("text-anchor", "middle"); + pathUpdate.attr("d", "M" + range[0] + "," + -outerTickSize + "V0H" + range[1] + "V" + -outerTickSize); + break; + } + case "left": + { + tickTransform = axisY; + lineEnter.attr("x2", -innerTickSize); + textEnter.attr("x", -(Math.max(innerTickSize, 0) + tickPadding)); + lineUpdate.attr("x2", -innerTickSize).attr("y2", 0); + textUpdate.attr("x", -(Math.max(innerTickSize, 0) + tickPadding)).attr("y", tickOffset); + text.attr("dy", ".32em").style("text-anchor", "end"); + pathUpdate.attr("d", "M" + -outerTickSize + "," + range[0] + "H0V" + range[1] + "H" + -outerTickSize); + break; + } + case "right": + { + tickTransform = axisY; + lineEnter.attr("x2", innerTickSize); + textEnter.attr("x", Math.max(innerTickSize, 0) + tickPadding); + lineUpdate.attr("x2", innerTickSize).attr("y2", 0); + textUpdate.attr("x", Math.max(innerTickSize, 0) + tickPadding).attr("y", 0); + text.attr("dy", ".32em").style("text-anchor", "start"); + pathUpdate.attr("d", "M" + outerTickSize + "," + range[0] + "H0V" + range[1] + "H" + outerTickSize); + break; + } + } + if (scale1.rangeBand) { + var x = scale1, dx = x.rangeBand() / 2; + scale0 = scale1 = function (d) { + return x(d) + dx; + }; + } else if (scale0.rangeBand) { + scale0 = scale1; + } else { + tickExit.call(tickTransform, scale1); + } + tickEnter.call(tickTransform, scale0); + tickUpdate.call(tickTransform, scale1); + }); + } + axis.scale = function (x) { + if (!arguments.length) { return scale; } + scale = x; + return axis; + }; + axis.orient = function (x) { + if (!arguments.length) { return orient; } + orient = x in {top: 1, right: 1, bottom: 1, left: 1} ? x + "" : "bottom"; + return axis; + }; + axis.tickFormat = function (format) { + if (!arguments.length) { return tickFormat; } + tickFormat = format; + return axis; + }; + axis.tickCentered = function (isCentered) { + if (!arguments.length) { return tickCentered; } + tickCentered = isCentered; + return axis; + }; + axis.tickOffset = function () { // This will be overwritten when normal x axis + return tickOffset; + }; + axis.ticks = function () { + if (!arguments.length) { return tickArguments; } + tickArguments = arguments; + return axis; + }; + axis.tickCulling = function (culling) { + if (!arguments.length) { return tickCulling; } + tickCulling = culling; + return axis; + }; + axis.tickValues = function (x) { + if (typeof x === 'function') { + tickValues = function () { + return x(scale.domain()); + }; + } + else { + if (!arguments.length) { return tickValues; } + tickValues = x; + } + return axis; + }; + return axis; + } + + if (typeof define === 'function' && define.amd) { + define("c3", ["d3"], c3); + } else if ('undefined' !== typeof exports && 'undefined' !== typeof module) { + module.exports = c3; + } else { + window.c3 = c3; + } + +})(window); diff --git a/snf-admin-app/synnefo_admin/admin/static/js/charts.js b/snf-admin-app/synnefo_admin/admin/static/js/charts.js new file mode 100644 index 0000000000000000000000000000000000000000..79910f01282db89c433ed60703b5ae0a2ab5361f --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/js/charts.js @@ -0,0 +1,578 @@ +String.prototype.capitalize = function() { + return this.charAt(0).toUpperCase() + this.slice(1); +}; + + +String.prototype.sanitize = function() { + new_string = ""; + for (var i = 0, len = this.length; i < len; i++) { + c = this.charAt(i); + if (/^[a-zA-Z0-9]$/.test(c)) { + new_string += c; + } else if (/^[\.\-\_\:\~\(\)\,\']$/.test(c)) { + new_string += c; + } else { + new_string += "_"; // replace it with a safe character + } + } + return new_string; +}; + +var chart_options = { + color: { + pattern: ['#68B3F0','#EEC04C','#FF6F90','#A9DDD9','#7474F1', '#8EBE6D', '#C77529', '#F53939', '#FAA330', '#AD57EE'], + } +}; + +// Shorten any value that is more than 100,000. +// Its presentation will be as a number multiplied by a power of 10. The power +// sign will be "^", if we wish to have non-html tags, else <sup></sup>. +function shorten(value, non_html) { + if (value < 1000000) + return value; + + non_html = non_html || false; + + var i = 0; + while (value > 10) { + value = value / 10; + i++; + } + + ret = value.toFixed(3) + ' x 10'; + if (non_html) { + return ret + '^' + i.toString(); + } else { + return ret + i.toString().sup(); + } +} + + +function humanize(value, unit) { + if (!unit) { + return shorten(value); + } + + var units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + var i = 0; + while (value >= 1024) { + i++; + value = value / 1024; + } + + return shorten(Math.round(value)) + ' ' + units[i]; +} + + +function percentify(value, total) { + if (total === 0) + return 0; + return Math.round(100 * (value / total)); +} + + +// Decide whether a chart presents purely size data (storage, RAM etc.) +function is_size_chart(units) { + if (!units) { + return false; + } + + // If at least one unit in the list is null, then this is not a size chart. + for (var i = 0; i < units.length; i++) { + if (units[i] === null) { + return false; + } + } + + return true; +} + + +// Convert all number elements of a given list to their logarithmic (base 10) +// values. Should a list contain another list, the calculations continue +// recursively. +function convert2log(list) { + for (var i = 0; i < list.length; i++) { + item = list[i]; + + if (Array.isArray(item)) + item = convert2log(item); + else if (typeof item === 'string') + continue; + else + list[i] = Math.log(item) / Math.LN10; + } + + return list; +} + + +function create_pie_chart(cols) { + var c3_opts = { + data: { + columns: cols, + type: 'pie', + labels: true, + }, + color: { + pattern: chart_options.color.pattern, + }, + //bindto: '#infra-usage', + tooltip: { + format: { + value: function (value, ratio, id, index) { + var prc = ratio * 100; + prc = prc.toFixed(1); + return value + " (" + prc + "%)"; + } + } + } + }; + + return c3_opts; +} + + +function create_bar_chart(cols, log_scale) { + // By default use natural scale for the Y-axis, unless if prompted + // otherwise. + log_scale = log_scale || false; + + if (log_scale) + cols = convert2log(cols); + + var c3_opts = { + data: { + x: 'x', + columns: cols, + type: 'bar', + labels: { + format: { + y: d3.format("d") + }, + }, + }, + color: { + pattern: chart_options.color.pattern, + }, + + //bindto: '#infra-usage', + axis: { + x: { + type: 'category', // this needed to load string x value + label: { + //text: 'x-axis text', + //position: 'outer-center', + }, + }, + y: { + label: { + //text: 'Usage Percentage', + //position: 'outer-middle', + }, + tick: { + format: d3.format('d') + } + } + }, + grid: { + y: { + //show: true, + show: true, + } + }, + }; + + // If the Y-axis uses a log scale, then convert its ticks and the data labels + // to natural numbers (10^x), in order to achieve the log effect. For more + // info on this trick, read here: + // + // https://github.com/masayuki0812/c3/issues/252#issuecomment-47167150 + // + if (log_scale) { + c3_opts.axis.y.tick.format = function(d) { + return Math.round(Math.pow(10, d)); }; + c3_opts.data.labels = { + format: { + y: function(d) { + return Math.round(Math.pow(10, d)); + } + } + }; + } + + return c3_opts; +} + +// The argument names for the arrays are "used" and "free", but they refer to +// any two arrays whose item sum forms a total. +function create_stacked_chart(categories, used, free, units) { + // Get the category names for the used and free arrays respectively. + var used_label = used[0]; + var free_label = free[0]; + + // Add these category names in any array that derives from the above. + var used_prc = [used_label]; + var free_prc = [free_label]; + var used_human = [used_label]; + var free_human = [free_label]; + var unit = null; + + for (var i = 1; i < used.length; i++) { + var u = used[i]; + var f = free[i]; + + if (units) { + unit = units[i - 1]; + } + + used_human.push(humanize(u, unit)); + free_human.push(humanize(f, unit)); + used_prc.push(percentify(u, u + f)); + free_prc.push(percentify(f, u + f)); + } + + var c3_opts = create_bar_chart([categories, used, free]); + + c3_opts.data.groups = [[used_label, free_label]]; + c3_opts.tooltip = { + format: { + value: function (value, ratio, id, index) { + if (id == used_label) { + return used_human[index + 1] + " (" + used_prc[index + 1] + "%)"; + } else { + return free_human[index + 1] + " (" + free_prc[index + 1] + "%)"; + } + } + } + }; + c3_opts._priv = { + 'used_human': used_human, + 'free_human': free_human, + 'used_prc': used_prc, + 'free_prc': free_prc, + }; + + //Convert the Y-axis and the data labels to size format. + size_format_fn = function (v) { + return humanize(v, units[0]); + }; + + //Shorten values in the Y-axis, if necessary. + num_format_fn = function (v) { + return shorten(v, true); + }; + + if (is_size_chart(units)) { + c3_opts.data.labels.format.y = size_format_fn; + c3_opts.axis.y.tick.format = size_format_fn; + } else { + c3_opts.data.labels.format.y = num_format_fn; + c3_opts.axis.y.tick.format = num_format_fn; + } + + return c3_opts; +} + + +function create_usage_percentage_chart(categories, used, free, units) { + format_fn = function (d) { + return d + "%"; + }; + + var c3_opts = create_stacked_chart(categories, used, free, units); + c3_opts.data.columns = [categories, c3_opts._priv.used_prc, + c3_opts._priv.free_prc]; + c3_opts.axis.y.tick.format = format_fn; + + return c3_opts; +} + + +//TODO: Wrap long data labels +function infraUsage(data) { + categories = { + 'astakos.pending_app': 'Project apps', + 'cyclades.cpu': 'CPUs', + 'cyclades.disk': 'System Disk', + 'cyclades.floating_ip': 'Public (Floating) IPs', + 'cyclades.network.private': 'Private Networks', + 'cyclades.ram': 'RAM', + 'cyclades.vm': 'VMs', + 'pithos.diskspace': 'Storage space', + }; + + var displayed_categories = ['x']; + var used = ['Used']; + var free = ['Free']; + var units = []; + + for (var key in categories) { + // why do we need it? + if (!categories.hasOwnProperty(key)) { + continue; // jumps over one iteration + } + + displayed_categories.push(categories[key]); + + resource = data.resources.all[key]; + var u = resource.used; + var f = resource.allocated - resource.used; + units.push(resource.unit); + + used.push(u); + free.push(f); + } + + c3_opts = create_usage_percentage_chart(displayed_categories, used, free, + units); + c3_opts.bindto = '#infra-usage'; + c3_opts.axis.y.label.text = 'Usage Percentage'; + c3_opts.axis.y.label.position = 'outer-middle'; + + var chart = c3.generate(c3_opts); +} + + +function resourceUsage(data, name) { + var wrapDomID = 'resource-'+name.replace(/\./gi, '_')+'-wrap'; + var domID = 'resource-'+name.replace(/\./gi, '_'); + $('#resource-usage').append('<div id="'+wrapDomID+'"></div>'); + var chartTitle = '<h3>'+name+' usage</h3>'; + $('#'+wrapDomID).append(chartTitle); + $('#'+wrapDomID).append('<div id="'+domID+'"></div>'); + + var providers = data.providers.slice(); + var used = ['Used']; + var free = ['Free']; + var units = []; + var provider = ""; + + for (var i = 0; i < providers.length; i++) { + provider = providers[i]; + + resource = data.resources[provider][name]; + var u = resource.used; + var f = resource.allocated - resource.used; + units.push(resource.unit); + + used.push(u); + free.push(f); + } + //Prepend x label + providers.unshift('x'); + + c3_opts = create_stacked_chart(providers, used, free, units); + c3_opts.bindto = '#'+domID; + c3_opts.axis.rotated = true; + c3_opts.axis.y.label.text = data.resources.all[name].description; + c3_opts.axis.y.label.position = 'outer-center'; + + var chart = c3.generate(c3_opts); +} + + +function statusPerProvider(data) { + var key = ""; + + // Create a list of lists. Each list will start with the name of the + // provider, as per c3's requirements. + var providers = []; + //This for-in loop works this way only for JSON objects. Otherwise, we + //need to check if the key belongs to the object prototype. + for (key in data.users) { + providers.push([key]); + } + + // Get list of user statuses (e.g. active, total, verified), and prepend it + // with an 'x' that is necessary for c3. + var statuses = ['x']; + for (key in data.users.all) { + statuses.push(key); + } + + // Fill each provider list with the number of users that have a certain + // status. + for (var i = 1; i < statuses.length; i++) { + for (var j = 0; j < providers.length; j++) { + var provider = providers[j][0]; + var status = statuses[i]; + providers[j].push(data.users[provider][status]); + } + } + + //Prepend the providers lists with the status categories. + providers.unshift(statuses); + + // Create logarithmic bar chart. + var c3_opts = create_bar_chart(providers, true); + c3_opts.bindto = '#provider-status'; + c3_opts.axis.y.label.text = 'Users (log scale)'; + c3_opts.axis.y.label.position = 'outer-middle'; + c3_opts.axis.x.label.text = 'Status categories'; + c3_opts.axis.x.label.position = 'outer-center'; + + var chart = c3.generate(c3_opts); +} + + +function statusPerProviderReversed(data) { + var key = ""; + + // Get list of user statuses (e.g. active, total, verified), and prepend it + // with an 'x' that is necessary for c3. + var statuses = []; + for (key in data.users.all) { + statuses.push([key]); + } + + // Create a list of lists. Each list will start with the name of the + // provider, as per c3's requirements. + var providers = ['x']; + //This for-in loop works this way only for JSON objects. Otherwise, we + //need to check if the key belongs to the object prototype. + for (key in data.users) { + providers.push(key); + } + + // Fill each provider list with the number of users that have a certain + // status. + for (var i = 1; i < providers.length; i++) { + for (var j = 0; j < statuses.length; j++) { + var status = statuses[j][0]; + var provider = providers[i]; + statuses[j].push(data.users[provider][status]); + } + } + + //Prepend the providers lists with the status categories. + statuses.unshift(providers); + + // Create logarithmic bar chart. + var c3_opts = create_bar_chart(statuses, true); + c3_opts.bindto = '#provider-status-reversed'; + c3_opts.axis.y.label.text = 'Users (log scale)'; + c3_opts.axis.y.label.position = 'outer-middle'; + c3_opts.axis.x.label.text = 'Providers'; + c3_opts.axis.x.label.position = 'outer-center'; + + var chart = c3.generate(c3_opts); +} + + +function exclusiveProviders(data) { + providers = data.providers.slice(); + var excl = ['Exclusive']; + var non_excl = ['Non-exclusive']; + + for (var i = 0; i < providers.length; i++) { + var provider = providers[i]; + var prov_data = data.users[provider]; + + var e = prov_data.exclusive; + var ne = prov_data.active - prov_data.exclusive; + + excl.push(e); + non_excl.push(ne); + } + //Prepend x label + providers.unshift('x'); + + c3_opts = create_stacked_chart(providers, excl, non_excl); + c3_opts.bindto = '#provider-exclusiveness'; + c3_opts.axis.y.tick.format = d3.format("d"); + c3_opts.axis.y.label.text = 'Active Users'; + c3_opts.axis.y.label.position = 'outer-middle'; + c3_opts.axis.x.label.text = 'Providers'; + c3_opts.axis.x.label.position = 'outer-center'; + + var chart = c3.generate(c3_opts); +} + + +function serverStatus(data) { + var servers = data.servers; + var server_data = []; + var status = ""; + + for (status in data.servers) { + var count = servers[status].count; + server_data.push([status.capitalize(), count]); + } + + var c3_opts = create_pie_chart(server_data); + c3_opts.bindto = "#server-status"; + var chart = c3.generate(c3_opts); +} + + +function ipPoolStatus(data) { + var ip_pools = data.ip_pools; + var status = ""; + + var ip_data = []; + for (status in ip_pools) { + var ip_sp = ip_pools[status]; + var a = ip_sp.total - ip_sp.free; + var f = ip_sp.free; + + ip_data.push([status.capitalize() + ' - Allocated', a]); + ip_data.push([status.capitalize() + ' - Free', f]); + } + + var c3_opts = create_pie_chart(ip_data); + c3_opts.bindto = "#ip-pool-status"; + + var chart = c3.generate(c3_opts); +} + + +function diskTemplates(data) { + var servers = data.servers; + var templates = {}; + + for (var status in servers) { + var disks = servers[status].disk; + for (var key in disks) { + if (!templates.hasOwnProperty(key)) { + templates[key] = 0; + } + for (var flavor in disks[key]) { + templates[key] += disks[key][flavor]; + } + } + } + + var disk_data = []; + for (var t in templates) { + if (!templates.hasOwnProperty(t)) { + continue; + } + disk_data.push([t.capitalize(), templates[t]]); + } + + var c3_opts = create_pie_chart(disk_data); + c3_opts.bindto = "#disk-templates"; + + var chart = c3.generate(c3_opts); +} + + +function imagesStats(data) { + var images = data.images; + + var image_data = []; + for (var i in images) { + image_data.push([i.sanitize(), images[i]]); + } + + var c3_opts = create_pie_chart(image_data); + c3_opts.bindto = "#images"; + + var chart = c3.generate(c3_opts); +} + +$(document).ready(function() { + sticker(); +}); diff --git a/snf-admin-app/synnefo_admin/admin/static/js/charts_common.js b/snf-admin-app/synnefo_admin/admin/static/js/charts_common.js new file mode 100644 index 0000000000000000000000000000000000000000..77ebd24d11f74f38fcf0a7508f07bcf31628a60d --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/js/charts_common.js @@ -0,0 +1,58 @@ +$(document).ready(function() { + var astakos_stats_url = $('#charts').data("astakos-stats"); + var cyclades_stats_url = $('#charts').data("cyclades-stats"); + var astakos_stats = {}; + var cyclades_stats = {}; + + $.ajax({ + url: astakos_stats_url, + type: 'GET', + contentType: 'application/json', + success: function(response, statusText, jqXHR) { + astakos_stats = response; + infraUsage(astakos_stats); + for (var key in astakos_stats.resources.all) { + resourceUsage(astakos_stats, key); + } + statusPerProvider(astakos_stats); + statusPerProviderReversed(astakos_stats); + exclusiveProviders(astakos_stats); + }, + error: function(jqXHR, statusText) { + console.log('error', statusText); + } + }); + + $.ajax({ + url: cyclades_stats_url, + type: 'GET', + contentType: 'application/json', + success: function(response, statusText, jqXHR) { + cyclades_stats = response; + serverStatus(cyclades_stats); + ipPoolStatus(cyclades_stats); + diskTemplates(cyclades_stats); + imagesStats(cyclades_stats); + }, + error: function(jqXHR, statusText) { + console.log('error', statusText); + } + }); + + // when page is loaded show charts whose sidebar a is active + function display_charts_init(){ + $('.charts .sidebar a.active').each(function(){ + var el = $(this).attr('data-chart'); + $('.well').find('div[data-chart='+el+']').show(); + }); + } + + display_charts_init(); + + $('.charts .sidebar a').click(function(e) { + e.preventDefault(); + $(this).toggleClass('active'); + var el = $(this).attr('data-chart'); + $('.well').find('div[data-chart='+el+']').stop(true, false).slideToggle('slow'); + }); +}); diff --git a/snf-admin-app/synnefo_admin/admin/static/js/common.js b/snf-admin-app/synnefo_admin/admin/static/js/common.js new file mode 100644 index 0000000000000000000000000000000000000000..2d12624cb1daca5bf301844d3dece56a3f81205e --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/js/common.js @@ -0,0 +1,325 @@ +snf = { + filters: {}, + modals: { + performAction: function(modal, notificationArea, warningMsg, itemsCount, countAction) { + var $modal = $(modal); + var $notificationArea = $(notificationArea); + var $actionBtn = $modal.find('.apply-action') + var url = $actionBtn.attr('data-url'); + var actionName = $actionBtn.find('span').text(); + var logID = 'action-'+countAction; + var data = { + op: $actionBtn.attr('data-op'), + target: $actionBtn.attr('data-target'), + ids: $actionBtn.attr('data-ids') + } + var contactAction = (data.op === 'contact' ? true : false); + + if(contactAction) { + data['sender'] = $modal.find('input[name="sender"]').val(); + data['subject'] = $modal.find('input[name="subject"]').val(); + data['text'] = $modal.find('textarea[name="text"]').val(); + } + $.ajax({ + url: url, + type: 'POST', + data: JSON.stringify(data), + contentType: 'application/json', + timeout: 100000, + beforeSend: function(jqXHR, settings) { + $('.no-notifications:not(.hidden)').addClass('hidden'); + var pendingMsg = _.template(snf.modals.html.notifyPending, ({logID: logID, actionName: actionName, removeBtn: snf.modals.html.removeLogLine, itemsCount: itemsCount})); + if($notificationArea.find('.warning').length === 0) { + $notificationArea.find('.container').append(pendingMsg); + } + else { + $notificationArea.find('.warning').before(pendingMsg); + } + snf.modals.showBottomModal($notificationArea); + $notificationArea.find('.warning').fadeIn('slow'); + }, + success: function(response, statusText, jqXHR) { + var successMsg = _.template(snf.modals.html.notifySuccess, ({actionName: actionName, removeBtn: snf.modals.html.removeLogLine, itemsCount: itemsCount})); + if($notificationArea.find('.warning').length === 0) { + $notificationArea.find('.container').append(warningMsg); + } + $notificationArea.find('#'+logID).replaceWith(successMsg); + snf.modals.showBottomModal($notificationArea); + }, + error: function(jqXHR, statusText) { + var htmlErrorSum =_.template(snf.modals.html.notifyErrorSum, ({actionName: actionName, removeBtn: snf.modals.html.removeLogLine, itemsCount: itemsCount})); + var htmlErrorReason, htmlErrorIDs, htmlError; + if(jqXHR.responseJSON === undefined) { + htmlErrorReason = _.template(snf.modals.html.notifyErrorReason, {description: jqXHR.statusText+' (code: '+jqXHR.status+').'}); + htmlErrorIDs = ''; + } + else { + + htmlErrorReason = _.template(snf.modals.html.notifyErrorReason, {description: jqXHR.responseJSON.result}); + htmlErrorIDs = _.template(snf.modals.html.notifyErrorIDs, {ids: jqXHR.responseJSON.error_ids.toString().replace(/\,/gi, ', ')}); + } + var logs = htmlErrorSum + _.template(snf.modals.html.notifyErrorDetails, {list: htmlErrorReason+htmlErrorIDs}); + htmlError = _.template(snf.modals.html.notifyError, {logInfo: logs}); + if($notificationArea.find('.warning').length === 0) { + $notificationArea.find('.container').append(warningMsg); + } + $notificationArea.find('#'+logID).replaceWith(htmlError); + + snf.modals.showBottomModal($notificationArea); + } + }); + }, + showBottomModal: function($modal) { + var height = -$modal.outerHeight(true); + $modal.css('bottom', height) + $modal.animate({'bottom': '0px'}, 'slow'); + }, + hideBottomModal: function($modal) { + var height = -$modal.outerHeight(true) + $modal.animate({'bottom': height}, 'slow', function() { + if($modal.find('.log').length === 0) { + $modal.find('.warning').remove(); + $('.no-notifications').removeClass('hidden'); + } + }); + }, + toggleBottomModal: function($modal) { + if($modal.css('bottom') !== '0px') { + snf.modals.showBottomModal($modal); + } + else { + snf.modals.hideBottomModal($modal); + } + }, + showError: function(modal, errorSign) { + var $modal = $(modal); + var $errorMsg = $modal.find('*[data-error="'+errorSign+'"]'); + $errorMsg.show(); + }, + resetErrors: function (modal) { + var $modal = $(modal); + $modal.find('.error-sign').hide(); + }, + checkInput: function(modal, inputArea, errorSign) { + var $inputArea = $(inputArea); + var $errorSign = $(modal).find('*[data-error="'+errorSign+'"]'); + + $inputArea.keyup(function() { + if($.trim($inputArea.val())) { + $errorSign.hide(); + } + }); + }, + checkEmail: function(modal, inputArea, errorSign) { + var $inputArea = $(inputArea); + var $errorSign = $(modal).find('*[data-error="'+errorSign+'"]'); + + $inputArea.keyup(function() { + if(snf.modals.validateEmail($inputArea.val())) { + $errorSign.hide(); + } + }); + }, + validateEmail: function(email) { + var reg = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + //For emails that are between these symbols: + // '<': less than, '>': greater_than + var lt_gt_reg = /^\<(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))\>$/; + + var chunks = email.split(" "); + if (chunks.length == 1) { + return (reg.test(email) || lt_gt_reg.test(email)) + } else { + chunk = chunks[chunks.length - 1]; + return lt_gt_reg.test(chunk); + } + }, + validateContactForm: function(modal) { + var $modal = $(modal); + var $emailSubj = $modal.find('*[name="subject"]'); + var $emailBody = $modal.find('*[name="text"]'); + var $emailSender = $modal.find('*[name="sender"]'); + var noError = true; + if(!$.trim($emailSubj.val())) { + snf.modals.showError($modal, 'empty-subject'); + snf.modals.checkInput($modal, $emailSubj, 'empty-subject'); + noError = false; + } + if(!$.trim($emailBody.val())) { + snf.modals.showError($modal, 'empty-body') + snf.modals.checkInput($modal, $emailBody, 'empty-body'); + noError = false; + } + if(!$.trim($emailSender.val())) { + snf.modals.showError($modal, 'empty-sender') + snf.modals.checkInput($modal, $emailSender, 'empty-sender'); + noError = false; + } + if(!snf.modals.validateEmail($emailSender.val()) && $.trim($emailSender.val())) { + snf.modals.showError($modal, 'invalid-email') + snf.modals.checkEmail($modal, $emailSender, 'invalid-email'); + noError = false; + } + return noError; + }, + resetInputs: function(modal) { + var $modal = $(modal); + $modal.find('input').each(function() { + $(this).val(snf.modals[$(this).attr('name')]); + }); + + $modal.find('textarea').each(function() { + $(this).val(snf.modals[$(this).attr('name')]); + }); + }, + html: { + singleItemInfo: '<dl class="dl-horizontal info-list"><dt>Name:</dt><dd><%= name %></dd><dt>ID:</dt><dd><%= id %></dd><dl>', + removeLogLine: '<a href="" class="remove-icon remove-log" title="Remove this line">X</a>', + notifyPending: '<p class="log" id="<%= logID %>"><span class="pending state-icon snf-font-admin snf-exclamation-sign"></span>Action <b>"<%= actionName %>"</b><% if (itemsCount==1) { %> for <%= itemsCount %> item <% } else if (itemsCount>0) { %> for <%= itemsCount %> items <% } %> is <b class="pending">pending</b>.<%= removeBtn %></p>', + notifySuccess: '<p class="log"><span class="success state-icon snf-font-admin snf-ok"></span>Action <b>"<%= actionName %>"</b><% if (itemsCount==1) { %> for <%= itemsCount %> item <% } else if (itemsCount>0) { %> for <%= itemsCount %> items <% } %> <b class="succeed">succeeded</b>.<%= removeBtn %></p>', + notifyError: '<div class="log"><%= logInfo %></div>', + notifyErrorSum: '<p><span class="error state-icon snf-font-admin snf-remove"></span>Action <b>"<%= actionName %>"</b><% if (itemsCount==1) { %> for <%= itemsCount %> item <% } else if (itemsCount>0) { %> for <%= itemsCount %> items <% } %> <b class="error">failed</b>.<%= removeBtn %></p>', + notifyErrorDetails: '<dl class="dl-horizontal"><%= list %></dl>', + notifyErrorReason: '<dt>Reason:</dt><dd><%= description %></dd>', + notifyErrorIDs: '<dt>IDs:</dt><dd><%= ids %></dd>', + notifyRefreshPage: '<p class="warning">The data of the page maybe out of date. Refresh it, to update them.</p>', + notifyReloadTable: '<p class="warning">You may need to reload the table before making any new selections.<span class="wrap"><a class="clear-reload warning-btn">Clear selected and reload</a></span></p>', + warningDuplicates: '<p class="warning-duplicate">Duplicate accounts have been detected</p>', + commonRow: '<tr data-itemid=<%= itemID %> <% if(hidden) { %> class="hidden-row" <% } %> ><td class="item-name"><%= itemName %></td><td class="item-id"><%= itemID %></td><td class="owner-name"><%= ownerName %></td><td class="owner-email"><div class="wrap"><a class="remove" title="Remove item from selection">X</a><%= ownerEmail %></div></td></tr>', + contactRow: '<tr <% if(showAssociations) { %> title="related with: <%= associations %>" <% } %> data-itemid=<%= itemID %> <% if(hidden) { %> class="hidden-row" <% } %> ><td class="full-name"><%= fullName %></td><td class="email"><div class="wrap"><a class="remove" title="Remove item from selection">X</a><%= email %></div></td></tr>', + } + }, + tables: { + html: { + selectAllBtn: '<a href="" class="select select-all line-btn" data-karma="neutral" data-caution="warning" data-toggle="modal" data-target="#massive-actions-warning"><span>Select All</span></a>', + selectPageBtn: '<a href="" id="select-page" class="select line-btn" data-karma="neutral" data-caution="none"><span>Select Page</span></a>', + toggleSelected: '<a href="" class="toggle-selected extra-btn line-btn" data-karma="neutral"><span class="text">Show selected </span><span class="badge num selected-num">0</span></a>', + reloadTable: '<a href="" class="line-btn reload-table" data-karma="neutral" data-caution="none" title="Reload table"><span class="snf-font-reload"></span></a>', + clearSelected: '<a href="" id="clear-all" class="disabled deselect line-btn" data-karma="neutral" data-caution="warning" data-toggle="modal" data-target="#clear-all-warning"><span class="snf-font-remove"></span><span>Clear All</span></a>', + toggleNotifications: '', + showTips: '', + trimedCell: '<span title="click to see"><span data-container="body" data-toggle="popover" data-placement="bottom" data-content="<%= data %>"><%= trimmedData %>...</span></span>', + checkboxCell: '<span class="snf-checkbox-unchecked selection-indicator select"></span><span class="snf-checkbox-checked selection-indicator select"></span><%= content %>', + summary: '<a title="Show summary" href="#" class="summary-expand expand-area"><span class="snf-font-admin snf-angle-down"></span></a><dl class="info-summary dl-horizontal"><%= list %></dl>', + summaryLine: '<dt><%= key %></dt><dd><%= value %></dd>', + detailsBtn: '<a title="Details" href="<%= url %>" class="details-link"><span class="snf-font-admin snf-search"></span></a>' + } + }, + timer: 0, + ajaxdelay: 400 +}; + +function setThemeIcon() { + var span = $('#toggle-theme').find('span'); + var c = $.cookie('theme'); + if ( c == 'dark') { + span.addClass('snf-sun-2-full'); + span.removeClass('snf-moon-1'); + } else { + span.addClass('snf-moon-1'); + span.removeClass('snf-sun-2-full'); + } +} + +function sticker() { + if ($("#sticker").length>0) { + var s = $("#sticker"); + } else { + return; + } + var pos = s.position(); + + $(window).scroll(function() { + var windowpos = $(window).scrollTop(); + // 80 the navbar fixed height + if (windowpos >= pos.top - 80) { + s.addClass("stick"); + } else { + s.removeClass("stick"); + } + }); +} + + +$(document).ready(function(){ + + setThemeIcon(); + var $notificationArea = $('.notify'); + $notificationArea.css('bottom', -$notificationArea.outerHeight(true)) + $('.error-sign').click(function(e) { + e.preventDefault(); + }); + + + $("[data-toggle=popover]").click(function(e) { + e.preventDefault(); + }); + $("#select-filters").click(function(e) { + e.preventDefault(); + }); + $("[data-toggle=popover]").popover(); + $("[data-toggle=tooltip]").tooltip(); + + $('body').on('click', function (e) { + //did not click a popover toggle or popover + if ($(e.target).data('toggle') !== 'popover' && $(e.target).closest('a').attr('id') !== 'select-filters' && $(e.target).parents('.popover.in').length === 0) { + $('[data-toggle="popover"]').popover('hide'); + $('#select-filters').popover('hide'); + } +}); + + $('.modal').on('hidden.bs.modal', function () { + $(this).find('.cancel').trigger('click'); + }); + + $('#toggle-notifications').click(function(e) { + e.preventDefault(); + snf.modals.toggleBottomModal($notificationArea); + }); + + $(document).keyup(function(e) { + if (!($(e.target).closest("input")[0] || $(e.target).closest("textarea")[0])) { + if(e.keyCode === 73) { + $('#toggle-notifications').trigger('click'); + } + } + }); + + + $notificationArea.on('click', '.remove-log', function(e) { + e.preventDefault(); + var $log = $(this).closest('.log'); + $log.fadeOut('slow', function() { + $log.remove(); + if($notificationArea.find('.log').length === 0) { + $notificationArea.find('.close-notifications').trigger('click'); + + } + }); + }); + + $notificationArea.on('click', '.close-notifications', function(e) { + e.preventDefault(); + snf.modals.hideBottomModal($notificationArea); + }); + + $('.modal[data-type="contact"]').find('input, textarea').each(function() { + snf.modals[$(this).attr('name')] = $(this).val() + }); + + $('.disabled').click(function(e){ + e.preventDefault(); + }); + + // toggle themes + $('#toggle-theme').click(function(e) { + var newC = ( $.cookie('theme') == 'dark' )? 'light': 'dark'; + $.cookie('theme', newC , {expires: 365, path: '/'}); + }); + + $('a').click(function(e){ + if ($(this).data('noclick')) { + e.preventDefault(); + } + }); +}); diff --git a/snf-admin-app/synnefo_admin/admin/static/js/d3.v3.min.js b/snf-admin-app/synnefo_admin/admin/static/js/d3.v3.min.js new file mode 100644 index 0000000000000000000000000000000000000000..88550ae5124aa569de51a05a62b667d1f7b9cf9b --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/js/d3.v3.min.js @@ -0,0 +1,5 @@ +!function(){function n(n,t){return t>n?-1:n>t?1:n>=t?0:0/0}function t(n){return null!=n&&!isNaN(n)}function e(n){return{left:function(t,e,r,u){for(arguments.length<3&&(r=0),arguments.length<4&&(u=t.length);u>r;){var i=r+u>>>1;n(t[i],e)<0?r=i+1:u=i}return r},right:function(t,e,r,u){for(arguments.length<3&&(r=0),arguments.length<4&&(u=t.length);u>r;){var i=r+u>>>1;n(t[i],e)>0?u=i:r=i+1}return r}}}function r(n){return n.length}function u(n){for(var t=1;n*t%1;)t*=10;return t}function i(n,t){try{for(var e in t)Object.defineProperty(n.prototype,e,{value:t[e],enumerable:!1})}catch(r){n.prototype=t}}function o(){}function a(n){return ia+n in this}function c(n){return n=ia+n,n in this&&delete this[n]}function s(){var n=[];return this.forEach(function(t){n.push(t)}),n}function l(){var n=0;for(var t in this)t.charCodeAt(0)===oa&&++n;return n}function f(){for(var n in this)if(n.charCodeAt(0)===oa)return!1;return!0}function h(){}function g(n,t,e){return function(){var r=e.apply(t,arguments);return r===t?n:r}}function p(n,t){if(t in n)return t;t=t.charAt(0).toUpperCase()+t.substring(1);for(var e=0,r=aa.length;r>e;++e){var u=aa[e]+t;if(u in n)return u}}function v(){}function d(){}function m(n){function t(){for(var t,r=e,u=-1,i=r.length;++u<i;)(t=r[u].on)&&t.apply(this,arguments);return n}var e=[],r=new o;return t.on=function(t,u){var i,o=r.get(t);return arguments.length<2?o&&o.on:(o&&(o.on=null,e=e.slice(0,i=e.indexOf(o)).concat(e.slice(i+1)),r.remove(t)),u&&e.push(r.set(t,{on:u})),n)},t}function y(){Zo.event.preventDefault()}function x(){for(var n,t=Zo.event;n=t.sourceEvent;)t=n;return t}function M(n){for(var t=new d,e=0,r=arguments.length;++e<r;)t[arguments[e]]=m(t);return t.of=function(e,r){return function(u){try{var i=u.sourceEvent=Zo.event;u.target=n,Zo.event=u,t[u.type].apply(e,r)}finally{Zo.event=i}}},t}function _(n){return sa(n,pa),n}function b(n){return"function"==typeof n?n:function(){return la(n,this)}}function w(n){return"function"==typeof n?n:function(){return fa(n,this)}}function S(n,t){function e(){this.removeAttribute(n)}function r(){this.removeAttributeNS(n.space,n.local)}function u(){this.setAttribute(n,t)}function i(){this.setAttributeNS(n.space,n.local,t)}function o(){var e=t.apply(this,arguments);null==e?this.removeAttribute(n):this.setAttribute(n,e)}function a(){var e=t.apply(this,arguments);null==e?this.removeAttributeNS(n.space,n.local):this.setAttributeNS(n.space,n.local,e)}return n=Zo.ns.qualify(n),null==t?n.local?r:e:"function"==typeof t?n.local?a:o:n.local?i:u}function k(n){return n.trim().replace(/\s+/g," ")}function E(n){return new RegExp("(?:^|\\s+)"+Zo.requote(n)+"(?:\\s+|$)","g")}function A(n){return(n+"").trim().split(/^|\s+/)}function C(n,t){function e(){for(var e=-1;++e<u;)n[e](this,t)}function r(){for(var e=-1,r=t.apply(this,arguments);++e<u;)n[e](this,r)}n=A(n).map(N);var u=n.length;return"function"==typeof t?r:e}function N(n){var t=E(n);return function(e,r){if(u=e.classList)return r?u.add(n):u.remove(n);var u=e.getAttribute("class")||"";r?(t.lastIndex=0,t.test(u)||e.setAttribute("class",k(u+" "+n))):e.setAttribute("class",k(u.replace(t," ")))}}function z(n,t,e){function r(){this.style.removeProperty(n)}function u(){this.style.setProperty(n,t,e)}function i(){var r=t.apply(this,arguments);null==r?this.style.removeProperty(n):this.style.setProperty(n,r,e)}return null==t?r:"function"==typeof t?i:u}function L(n,t){function e(){delete this[n]}function r(){this[n]=t}function u(){var e=t.apply(this,arguments);null==e?delete this[n]:this[n]=e}return null==t?e:"function"==typeof t?u:r}function T(n){return"function"==typeof n?n:(n=Zo.ns.qualify(n)).local?function(){return this.ownerDocument.createElementNS(n.space,n.local)}:function(){return this.ownerDocument.createElementNS(this.namespaceURI,n)}}function q(n){return{__data__:n}}function R(n){return function(){return ga(this,n)}}function D(t){return arguments.length||(t=n),function(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}}function P(n,t){for(var e=0,r=n.length;r>e;e++)for(var u,i=n[e],o=0,a=i.length;a>o;o++)(u=i[o])&&t(u,o,e);return n}function U(n){return sa(n,da),n}function j(n){var t,e;return function(r,u,i){var o,a=n[i].update,c=a.length;for(i!=e&&(e=i,t=0),u>=t&&(t=u+1);!(o=a[t])&&++t<c;);return o}}function H(){var n=this.__transition__;n&&++n.active}function F(n,t,e){function r(){var t=this[o];t&&(this.removeEventListener(n,t,t.$),delete this[o])}function u(){var u=c(t,Xo(arguments));r.call(this),this.addEventListener(n,this[o]=u,u.$=e),u._=t}function i(){var t,e=new RegExp("^__on([^.]+)"+Zo.requote(n)+"$");for(var r in this)if(t=r.match(e)){var u=this[r];this.removeEventListener(t[1],u,u.$),delete this[r]}}var o="__on"+n,a=n.indexOf("."),c=O;a>0&&(n=n.substring(0,a));var s=ya.get(n);return s&&(n=s,c=Y),a?t?u:r:t?v:i}function O(n,t){return function(e){var r=Zo.event;Zo.event=e,t[0]=this.__data__;try{n.apply(this,t)}finally{Zo.event=r}}}function Y(n,t){var e=O(n,t);return function(n){var t=this,r=n.relatedTarget;r&&(r===t||8&r.compareDocumentPosition(t))||e.call(t,n)}}function I(){var n=".dragsuppress-"+ ++Ma,t="click"+n,e=Zo.select(Wo).on("touchmove"+n,y).on("dragstart"+n,y).on("selectstart"+n,y);if(xa){var r=Bo.style,u=r[xa];r[xa]="none"}return function(i){function o(){e.on(t,null)}e.on(n,null),xa&&(r[xa]=u),i&&(e.on(t,function(){y(),o()},!0),setTimeout(o,0))}}function Z(n,t){t.changedTouches&&(t=t.changedTouches[0]);var e=n.ownerSVGElement||n;if(e.createSVGPoint){var r=e.createSVGPoint();if(0>_a&&(Wo.scrollX||Wo.scrollY)){e=Zo.select("body").append("svg").style({position:"absolute",top:0,left:0,margin:0,padding:0,border:"none"},"important");var u=e[0][0].getScreenCTM();_a=!(u.f||u.e),e.remove()}return _a?(r.x=t.pageX,r.y=t.pageY):(r.x=t.clientX,r.y=t.clientY),r=r.matrixTransform(n.getScreenCTM().inverse()),[r.x,r.y]}var i=n.getBoundingClientRect();return[t.clientX-i.left-n.clientLeft,t.clientY-i.top-n.clientTop]}function V(){return Zo.event.changedTouches[0].identifier}function X(){return Zo.event.target}function $(){return Wo}function B(n){return n>0?1:0>n?-1:0}function W(n,t,e){return(t[0]-n[0])*(e[1]-n[1])-(t[1]-n[1])*(e[0]-n[0])}function J(n){return n>1?0:-1>n?ba:Math.acos(n)}function G(n){return n>1?Sa:-1>n?-Sa:Math.asin(n)}function K(n){return((n=Math.exp(n))-1/n)/2}function Q(n){return((n=Math.exp(n))+1/n)/2}function nt(n){return((n=Math.exp(2*n))-1)/(n+1)}function tt(n){return(n=Math.sin(n/2))*n}function et(){}function rt(n,t,e){return this instanceof rt?(this.h=+n,this.s=+t,void(this.l=+e)):arguments.length<2?n instanceof rt?new rt(n.h,n.s,n.l):mt(""+n,yt,rt):new rt(n,t,e)}function ut(n,t,e){function r(n){return n>360?n-=360:0>n&&(n+=360),60>n?i+(o-i)*n/60:180>n?o:240>n?i+(o-i)*(240-n)/60:i}function u(n){return Math.round(255*r(n))}var i,o;return n=isNaN(n)?0:(n%=360)<0?n+360:n,t=isNaN(t)?0:0>t?0:t>1?1:t,e=0>e?0:e>1?1:e,o=.5>=e?e*(1+t):e+t-e*t,i=2*e-o,new gt(u(n+120),u(n),u(n-120))}function it(n,t,e){return this instanceof it?(this.h=+n,this.c=+t,void(this.l=+e)):arguments.length<2?n instanceof it?new it(n.h,n.c,n.l):n instanceof at?st(n.l,n.a,n.b):st((n=xt((n=Zo.rgb(n)).r,n.g,n.b)).l,n.a,n.b):new it(n,t,e)}function ot(n,t,e){return isNaN(n)&&(n=0),isNaN(t)&&(t=0),new at(e,Math.cos(n*=Aa)*t,Math.sin(n)*t)}function at(n,t,e){return this instanceof at?(this.l=+n,this.a=+t,void(this.b=+e)):arguments.length<2?n instanceof at?new at(n.l,n.a,n.b):n instanceof it?ot(n.l,n.c,n.h):xt((n=gt(n)).r,n.g,n.b):new at(n,t,e)}function ct(n,t,e){var r=(n+16)/116,u=r+t/500,i=r-e/200;return u=lt(u)*ja,r=lt(r)*Ha,i=lt(i)*Fa,new gt(ht(3.2404542*u-1.5371385*r-.4985314*i),ht(-.969266*u+1.8760108*r+.041556*i),ht(.0556434*u-.2040259*r+1.0572252*i))}function st(n,t,e){return n>0?new it(Math.atan2(e,t)*Ca,Math.sqrt(t*t+e*e),n):new it(0/0,0/0,n)}function lt(n){return n>.206893034?n*n*n:(n-4/29)/7.787037}function ft(n){return n>.008856?Math.pow(n,1/3):7.787037*n+4/29}function ht(n){return Math.round(255*(.00304>=n?12.92*n:1.055*Math.pow(n,1/2.4)-.055))}function gt(n,t,e){return this instanceof gt?(this.r=~~n,this.g=~~t,void(this.b=~~e)):arguments.length<2?n instanceof gt?new gt(n.r,n.g,n.b):mt(""+n,gt,ut):new gt(n,t,e)}function pt(n){return new gt(n>>16,255&n>>8,255&n)}function vt(n){return pt(n)+""}function dt(n){return 16>n?"0"+Math.max(0,n).toString(16):Math.min(255,n).toString(16)}function mt(n,t,e){var r,u,i,o=0,a=0,c=0;if(r=/([a-z]+)\((.*)\)/i.exec(n))switch(u=r[2].split(","),r[1]){case"hsl":return e(parseFloat(u[0]),parseFloat(u[1])/100,parseFloat(u[2])/100);case"rgb":return t(_t(u[0]),_t(u[1]),_t(u[2]))}return(i=Ia.get(n))?t(i.r,i.g,i.b):(null==n||"#"!==n.charAt(0)||isNaN(i=parseInt(n.substring(1),16))||(4===n.length?(o=(3840&i)>>4,o=o>>4|o,a=240&i,a=a>>4|a,c=15&i,c=c<<4|c):7===n.length&&(o=(16711680&i)>>16,a=(65280&i)>>8,c=255&i)),t(o,a,c))}function yt(n,t,e){var r,u,i=Math.min(n/=255,t/=255,e/=255),o=Math.max(n,t,e),a=o-i,c=(o+i)/2;return a?(u=.5>c?a/(o+i):a/(2-o-i),r=n==o?(t-e)/a+(e>t?6:0):t==o?(e-n)/a+2:(n-t)/a+4,r*=60):(r=0/0,u=c>0&&1>c?0:r),new rt(r,u,c)}function xt(n,t,e){n=Mt(n),t=Mt(t),e=Mt(e);var r=ft((.4124564*n+.3575761*t+.1804375*e)/ja),u=ft((.2126729*n+.7151522*t+.072175*e)/Ha),i=ft((.0193339*n+.119192*t+.9503041*e)/Fa);return at(116*u-16,500*(r-u),200*(u-i))}function Mt(n){return(n/=255)<=.04045?n/12.92:Math.pow((n+.055)/1.055,2.4)}function _t(n){var t=parseFloat(n);return"%"===n.charAt(n.length-1)?Math.round(2.55*t):t}function bt(n){return"function"==typeof n?n:function(){return n}}function wt(n){return n}function St(n){return function(t,e,r){return 2===arguments.length&&"function"==typeof e&&(r=e,e=null),kt(t,e,n,r)}}function kt(n,t,e,r){function u(){var n,t=c.status;if(!t&&c.responseText||t>=200&&300>t||304===t){try{n=e.call(i,c)}catch(r){return o.error.call(i,r),void 0}o.load.call(i,n)}else o.error.call(i,c)}var i={},o=Zo.dispatch("beforesend","progress","load","error"),a={},c=new XMLHttpRequest,s=null;return!Wo.XDomainRequest||"withCredentials"in c||!/^(http(s)?:)?\/\//.test(n)||(c=new XDomainRequest),"onload"in c?c.onload=c.onerror=u:c.onreadystatechange=function(){c.readyState>3&&u()},c.onprogress=function(n){var t=Zo.event;Zo.event=n;try{o.progress.call(i,c)}finally{Zo.event=t}},i.header=function(n,t){return n=(n+"").toLowerCase(),arguments.length<2?a[n]:(null==t?delete a[n]:a[n]=t+"",i)},i.mimeType=function(n){return arguments.length?(t=null==n?null:n+"",i):t},i.responseType=function(n){return arguments.length?(s=n,i):s},i.response=function(n){return e=n,i},["get","post"].forEach(function(n){i[n]=function(){return i.send.apply(i,[n].concat(Xo(arguments)))}}),i.send=function(e,r,u){if(2===arguments.length&&"function"==typeof r&&(u=r,r=null),c.open(e,n,!0),null==t||"accept"in a||(a.accept=t+",*/*"),c.setRequestHeader)for(var l in a)c.setRequestHeader(l,a[l]);return null!=t&&c.overrideMimeType&&c.overrideMimeType(t),null!=s&&(c.responseType=s),null!=u&&i.on("error",u).on("load",function(n){u(null,n)}),o.beforesend.call(i,c),c.send(null==r?null:r),i},i.abort=function(){return c.abort(),i},Zo.rebind(i,o,"on"),null==r?i:i.get(Et(r))}function Et(n){return 1===n.length?function(t,e){n(null==t?e:null)}:n}function At(){var n=Ct(),t=Nt()-n;t>24?(isFinite(t)&&(clearTimeout($a),$a=setTimeout(At,t)),Xa=0):(Xa=1,Wa(At))}function Ct(){var n=Date.now();for(Ba=Za;Ba;)n>=Ba.t&&(Ba.f=Ba.c(n-Ba.t)),Ba=Ba.n;return n}function Nt(){for(var n,t=Za,e=1/0;t;)t.f?t=n?n.n=t.n:Za=t.n:(t.t<e&&(e=t.t),t=(n=t).n);return Va=n,e}function zt(n,t){return t-(n?Math.ceil(Math.log(n)/Math.LN10):1)}function Lt(n,t){var e=Math.pow(10,3*ua(8-t));return{scale:t>8?function(n){return n/e}:function(n){return n*e},symbol:n}}function Tt(n){var t=n.decimal,e=n.thousands,r=n.grouping,u=n.currency,i=r?function(n){for(var t=n.length,u=[],i=0,o=r[0];t>0&&o>0;)u.push(n.substring(t-=o,t+o)),o=r[i=(i+1)%r.length];return u.reverse().join(e)}:wt;return function(n){var e=Ga.exec(n),r=e[1]||" ",o=e[2]||">",a=e[3]||"",c=e[4]||"",s=e[5],l=+e[6],f=e[7],h=e[8],g=e[9],p=1,v="",d="",m=!1;switch(h&&(h=+h.substring(1)),(s||"0"===r&&"="===o)&&(s=r="0",o="=",f&&(l-=Math.floor((l-1)/4))),g){case"n":f=!0,g="g";break;case"%":p=100,d="%",g="f";break;case"p":p=100,d="%",g="r";break;case"b":case"o":case"x":case"X":"#"===c&&(v="0"+g.toLowerCase());case"c":case"d":m=!0,h=0;break;case"s":p=-1,g="r"}"$"===c&&(v=u[0],d=u[1]),"r"!=g||h||(g="g"),null!=h&&("g"==g?h=Math.max(1,Math.min(21,h)):("e"==g||"f"==g)&&(h=Math.max(0,Math.min(20,h)))),g=Ka.get(g)||qt;var y=s&&f;return function(n){var e=d;if(m&&n%1)return"";var u=0>n||0===n&&0>1/n?(n=-n,"-"):a;if(0>p){var c=Zo.formatPrefix(n,h);n=c.scale(n),e=c.symbol+d}else n*=p;n=g(n,h);var x=n.lastIndexOf("."),M=0>x?n:n.substring(0,x),_=0>x?"":t+n.substring(x+1);!s&&f&&(M=i(M));var b=v.length+M.length+_.length+(y?0:u.length),w=l>b?new Array(b=l-b+1).join(r):"";return y&&(M=i(w+M)),u+=v,n=M+_,("<"===o?u+n+w:">"===o?w+u+n:"^"===o?w.substring(0,b>>=1)+u+n+w.substring(b):u+(y?n:w+n))+e}}}function qt(n){return n+""}function Rt(){this._=new Date(arguments.length>1?Date.UTC.apply(this,arguments):arguments[0])}function Dt(n,t,e){function r(t){var e=n(t),r=i(e,1);return r-t>t-e?e:r}function u(e){return t(e=n(new nc(e-1)),1),e}function i(n,e){return t(n=new nc(+n),e),n}function o(n,r,i){var o=u(n),a=[];if(i>1)for(;r>o;)e(o)%i||a.push(new Date(+o)),t(o,1);else for(;r>o;)a.push(new Date(+o)),t(o,1);return a}function a(n,t,e){try{nc=Rt;var r=new Rt;return r._=n,o(r,t,e)}finally{nc=Date}}n.floor=n,n.round=r,n.ceil=u,n.offset=i,n.range=o;var c=n.utc=Pt(n);return c.floor=c,c.round=Pt(r),c.ceil=Pt(u),c.offset=Pt(i),c.range=a,n}function Pt(n){return function(t,e){try{nc=Rt;var r=new Rt;return r._=t,n(r,e)._}finally{nc=Date}}}function Ut(n){function t(n){function t(t){for(var e,u,i,o=[],a=-1,c=0;++a<r;)37===n.charCodeAt(a)&&(o.push(n.substring(c,a)),null!=(u=ec[e=n.charAt(++a)])&&(e=n.charAt(++a)),(i=C[e])&&(e=i(t,null==u?"e"===e?" ":"0":u)),o.push(e),c=a+1);return o.push(n.substring(c,a)),o.join("")}var r=n.length;return t.parse=function(t){var r={y:1900,m:0,d:1,H:0,M:0,S:0,L:0,Z:null},u=e(r,n,t,0);if(u!=t.length)return null;"p"in r&&(r.H=r.H%12+12*r.p);var i=null!=r.Z&&nc!==Rt,o=new(i?Rt:nc);return"j"in r?o.setFullYear(r.y,0,r.j):"w"in r&&("W"in r||"U"in r)?(o.setFullYear(r.y,0,1),o.setFullYear(r.y,0,"W"in r?(r.w+6)%7+7*r.W-(o.getDay()+5)%7:r.w+7*r.U-(o.getDay()+6)%7)):o.setFullYear(r.y,r.m,r.d),o.setHours(r.H+Math.floor(r.Z/100),r.M+r.Z%100,r.S,r.L),i?o._:o},t.toString=function(){return n},t}function e(n,t,e,r){for(var u,i,o,a=0,c=t.length,s=e.length;c>a;){if(r>=s)return-1;if(u=t.charCodeAt(a++),37===u){if(o=t.charAt(a++),i=N[o in ec?t.charAt(a++):o],!i||(r=i(n,e,r))<0)return-1}else if(u!=e.charCodeAt(r++))return-1}return r}function r(n,t,e){b.lastIndex=0;var r=b.exec(t.substring(e));return r?(n.w=w.get(r[0].toLowerCase()),e+r[0].length):-1}function u(n,t,e){M.lastIndex=0;var r=M.exec(t.substring(e));return r?(n.w=_.get(r[0].toLowerCase()),e+r[0].length):-1}function i(n,t,e){E.lastIndex=0;var r=E.exec(t.substring(e));return r?(n.m=A.get(r[0].toLowerCase()),e+r[0].length):-1}function o(n,t,e){S.lastIndex=0;var r=S.exec(t.substring(e));return r?(n.m=k.get(r[0].toLowerCase()),e+r[0].length):-1}function a(n,t,r){return e(n,C.c.toString(),t,r)}function c(n,t,r){return e(n,C.x.toString(),t,r)}function s(n,t,r){return e(n,C.X.toString(),t,r)}function l(n,t,e){var r=x.get(t.substring(e,e+=2).toLowerCase());return null==r?-1:(n.p=r,e)}var f=n.dateTime,h=n.date,g=n.time,p=n.periods,v=n.days,d=n.shortDays,m=n.months,y=n.shortMonths;t.utc=function(n){function e(n){try{nc=Rt;var t=new nc;return t._=n,r(t)}finally{nc=Date}}var r=t(n);return e.parse=function(n){try{nc=Rt;var t=r.parse(n);return t&&t._}finally{nc=Date}},e.toString=r.toString,e},t.multi=t.utc.multi=re;var x=Zo.map(),M=Ht(v),_=Ft(v),b=Ht(d),w=Ft(d),S=Ht(m),k=Ft(m),E=Ht(y),A=Ft(y);p.forEach(function(n,t){x.set(n.toLowerCase(),t)});var C={a:function(n){return d[n.getDay()]},A:function(n){return v[n.getDay()]},b:function(n){return y[n.getMonth()]},B:function(n){return m[n.getMonth()]},c:t(f),d:function(n,t){return jt(n.getDate(),t,2)},e:function(n,t){return jt(n.getDate(),t,2)},H:function(n,t){return jt(n.getHours(),t,2)},I:function(n,t){return jt(n.getHours()%12||12,t,2)},j:function(n,t){return jt(1+Qa.dayOfYear(n),t,3)},L:function(n,t){return jt(n.getMilliseconds(),t,3)},m:function(n,t){return jt(n.getMonth()+1,t,2)},M:function(n,t){return jt(n.getMinutes(),t,2)},p:function(n){return p[+(n.getHours()>=12)]},S:function(n,t){return jt(n.getSeconds(),t,2)},U:function(n,t){return jt(Qa.sundayOfYear(n),t,2)},w:function(n){return n.getDay()},W:function(n,t){return jt(Qa.mondayOfYear(n),t,2)},x:t(h),X:t(g),y:function(n,t){return jt(n.getFullYear()%100,t,2)},Y:function(n,t){return jt(n.getFullYear()%1e4,t,4)},Z:te,"%":function(){return"%"}},N={a:r,A:u,b:i,B:o,c:a,d:Wt,e:Wt,H:Gt,I:Gt,j:Jt,L:ne,m:Bt,M:Kt,p:l,S:Qt,U:Yt,w:Ot,W:It,x:c,X:s,y:Vt,Y:Zt,Z:Xt,"%":ee};return t}function jt(n,t,e){var r=0>n?"-":"",u=(r?-n:n)+"",i=u.length;return r+(e>i?new Array(e-i+1).join(t)+u:u)}function Ht(n){return new RegExp("^(?:"+n.map(Zo.requote).join("|")+")","i")}function Ft(n){for(var t=new o,e=-1,r=n.length;++e<r;)t.set(n[e].toLowerCase(),e);return t}function Ot(n,t,e){rc.lastIndex=0;var r=rc.exec(t.substring(e,e+1));return r?(n.w=+r[0],e+r[0].length):-1}function Yt(n,t,e){rc.lastIndex=0;var r=rc.exec(t.substring(e));return r?(n.U=+r[0],e+r[0].length):-1}function It(n,t,e){rc.lastIndex=0;var r=rc.exec(t.substring(e));return r?(n.W=+r[0],e+r[0].length):-1}function Zt(n,t,e){rc.lastIndex=0;var r=rc.exec(t.substring(e,e+4));return r?(n.y=+r[0],e+r[0].length):-1}function Vt(n,t,e){rc.lastIndex=0;var r=rc.exec(t.substring(e,e+2));return r?(n.y=$t(+r[0]),e+r[0].length):-1}function Xt(n,t,e){return/^[+-]\d{4}$/.test(t=t.substring(e,e+5))?(n.Z=-t,e+5):-1}function $t(n){return n+(n>68?1900:2e3)}function Bt(n,t,e){rc.lastIndex=0;var r=rc.exec(t.substring(e,e+2));return r?(n.m=r[0]-1,e+r[0].length):-1}function Wt(n,t,e){rc.lastIndex=0;var r=rc.exec(t.substring(e,e+2));return r?(n.d=+r[0],e+r[0].length):-1}function Jt(n,t,e){rc.lastIndex=0;var r=rc.exec(t.substring(e,e+3));return r?(n.j=+r[0],e+r[0].length):-1}function Gt(n,t,e){rc.lastIndex=0;var r=rc.exec(t.substring(e,e+2));return r?(n.H=+r[0],e+r[0].length):-1}function Kt(n,t,e){rc.lastIndex=0;var r=rc.exec(t.substring(e,e+2));return r?(n.M=+r[0],e+r[0].length):-1}function Qt(n,t,e){rc.lastIndex=0;var r=rc.exec(t.substring(e,e+2));return r?(n.S=+r[0],e+r[0].length):-1}function ne(n,t,e){rc.lastIndex=0;var r=rc.exec(t.substring(e,e+3));return r?(n.L=+r[0],e+r[0].length):-1}function te(n){var t=n.getTimezoneOffset(),e=t>0?"-":"+",r=~~(ua(t)/60),u=ua(t)%60;return e+jt(r,"0",2)+jt(u,"0",2)}function ee(n,t,e){uc.lastIndex=0;var r=uc.exec(t.substring(e,e+1));return r?e+r[0].length:-1}function re(n){for(var t=n.length,e=-1;++e<t;)n[e][0]=this(n[e][0]);return function(t){for(var e=0,r=n[e];!r[1](t);)r=n[++e];return r[0](t)}}function ue(){}function ie(n,t,e){var r=e.s=n+t,u=r-n,i=r-u;e.t=n-i+(t-u)}function oe(n,t){n&&cc.hasOwnProperty(n.type)&&cc[n.type](n,t)}function ae(n,t,e){var r,u=-1,i=n.length-e;for(t.lineStart();++u<i;)r=n[u],t.point(r[0],r[1],r[2]);t.lineEnd()}function ce(n,t){var e=-1,r=n.length;for(t.polygonStart();++e<r;)ae(n[e],t,1);t.polygonEnd()}function se(){function n(n,t){n*=Aa,t=t*Aa/2+ba/4;var e=n-r,o=e>=0?1:-1,a=o*e,c=Math.cos(t),s=Math.sin(t),l=i*s,f=u*c+l*Math.cos(a),h=l*o*Math.sin(a);lc.add(Math.atan2(h,f)),r=n,u=c,i=s}var t,e,r,u,i;fc.point=function(o,a){fc.point=n,r=(t=o)*Aa,u=Math.cos(a=(e=a)*Aa/2+ba/4),i=Math.sin(a)},fc.lineEnd=function(){n(t,e)}}function le(n){var t=n[0],e=n[1],r=Math.cos(e);return[r*Math.cos(t),r*Math.sin(t),Math.sin(e)]}function fe(n,t){return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]}function he(n,t){return[n[1]*t[2]-n[2]*t[1],n[2]*t[0]-n[0]*t[2],n[0]*t[1]-n[1]*t[0]]}function ge(n,t){n[0]+=t[0],n[1]+=t[1],n[2]+=t[2]}function pe(n,t){return[n[0]*t,n[1]*t,n[2]*t]}function ve(n){var t=Math.sqrt(n[0]*n[0]+n[1]*n[1]+n[2]*n[2]);n[0]/=t,n[1]/=t,n[2]/=t}function de(n){return[Math.atan2(n[1],n[0]),G(n[2])]}function me(n,t){return ua(n[0]-t[0])<ka&&ua(n[1]-t[1])<ka}function ye(n,t){n*=Aa;var e=Math.cos(t*=Aa);xe(e*Math.cos(n),e*Math.sin(n),Math.sin(t))}function xe(n,t,e){++hc,pc+=(n-pc)/hc,vc+=(t-vc)/hc,dc+=(e-dc)/hc}function Me(){function n(n,u){n*=Aa;var i=Math.cos(u*=Aa),o=i*Math.cos(n),a=i*Math.sin(n),c=Math.sin(u),s=Math.atan2(Math.sqrt((s=e*c-r*a)*s+(s=r*o-t*c)*s+(s=t*a-e*o)*s),t*o+e*a+r*c);gc+=s,mc+=s*(t+(t=o)),yc+=s*(e+(e=a)),xc+=s*(r+(r=c)),xe(t,e,r)}var t,e,r;wc.point=function(u,i){u*=Aa;var o=Math.cos(i*=Aa);t=o*Math.cos(u),e=o*Math.sin(u),r=Math.sin(i),wc.point=n,xe(t,e,r)}}function _e(){wc.point=ye}function be(){function n(n,t){n*=Aa;var e=Math.cos(t*=Aa),o=e*Math.cos(n),a=e*Math.sin(n),c=Math.sin(t),s=u*c-i*a,l=i*o-r*c,f=r*a-u*o,h=Math.sqrt(s*s+l*l+f*f),g=r*o+u*a+i*c,p=h&&-J(g)/h,v=Math.atan2(h,g);Mc+=p*s,_c+=p*l,bc+=p*f,gc+=v,mc+=v*(r+(r=o)),yc+=v*(u+(u=a)),xc+=v*(i+(i=c)),xe(r,u,i)}var t,e,r,u,i;wc.point=function(o,a){t=o,e=a,wc.point=n,o*=Aa;var c=Math.cos(a*=Aa);r=c*Math.cos(o),u=c*Math.sin(o),i=Math.sin(a),xe(r,u,i)},wc.lineEnd=function(){n(t,e),wc.lineEnd=_e,wc.point=ye}}function we(){return!0}function Se(n,t,e,r,u){var i=[],o=[];if(n.forEach(function(n){if(!((t=n.length-1)<=0)){var t,e=n[0],r=n[t];if(me(e,r)){u.lineStart();for(var a=0;t>a;++a)u.point((e=n[a])[0],e[1]);return u.lineEnd(),void 0}var c=new Ee(e,n,null,!0),s=new Ee(e,null,c,!1);c.o=s,i.push(c),o.push(s),c=new Ee(r,n,null,!1),s=new Ee(r,null,c,!0),c.o=s,i.push(c),o.push(s)}}),o.sort(t),ke(i),ke(o),i.length){for(var a=0,c=e,s=o.length;s>a;++a)o[a].e=c=!c;for(var l,f,h=i[0];;){for(var g=h,p=!0;g.v;)if((g=g.n)===h)return;l=g.z,u.lineStart();do{if(g.v=g.o.v=!0,g.e){if(p)for(var a=0,s=l.length;s>a;++a)u.point((f=l[a])[0],f[1]);else r(g.x,g.n.x,1,u);g=g.n}else{if(p){l=g.p.z;for(var a=l.length-1;a>=0;--a)u.point((f=l[a])[0],f[1])}else r(g.x,g.p.x,-1,u);g=g.p}g=g.o,l=g.z,p=!p}while(!g.v);u.lineEnd()}}}function ke(n){if(t=n.length){for(var t,e,r=0,u=n[0];++r<t;)u.n=e=n[r],e.p=u,u=e;u.n=e=n[0],e.p=u}}function Ee(n,t,e,r){this.x=n,this.z=t,this.o=e,this.e=r,this.v=!1,this.n=this.p=null}function Ae(n,t,e,r){return function(u,i){function o(t,e){var r=u(t,e);n(t=r[0],e=r[1])&&i.point(t,e)}function a(n,t){var e=u(n,t);d.point(e[0],e[1])}function c(){y.point=a,d.lineStart()}function s(){y.point=o,d.lineEnd()}function l(n,t){v.push([n,t]);var e=u(n,t);M.point(e[0],e[1])}function f(){M.lineStart(),v=[]}function h(){l(v[0][0],v[0][1]),M.lineEnd();var n,t=M.clean(),e=x.buffer(),r=e.length;if(v.pop(),p.push(v),v=null,r)if(1&t){n=e[0];var u,r=n.length-1,o=-1;if(r>0){for(_||(i.polygonStart(),_=!0),i.lineStart();++o<r;)i.point((u=n[o])[0],u[1]);i.lineEnd()}}else r>1&&2&t&&e.push(e.pop().concat(e.shift())),g.push(e.filter(Ce))}var g,p,v,d=t(i),m=u.invert(r[0],r[1]),y={point:o,lineStart:c,lineEnd:s,polygonStart:function(){y.point=l,y.lineStart=f,y.lineEnd=h,g=[],p=[]},polygonEnd:function(){y.point=o,y.lineStart=c,y.lineEnd=s,g=Zo.merge(g);var n=Le(m,p);g.length?(_||(i.polygonStart(),_=!0),Se(g,ze,n,e,i)):n&&(_||(i.polygonStart(),_=!0),i.lineStart(),e(null,null,1,i),i.lineEnd()),_&&(i.polygonEnd(),_=!1),g=p=null},sphere:function(){i.polygonStart(),i.lineStart(),e(null,null,1,i),i.lineEnd(),i.polygonEnd()}},x=Ne(),M=t(x),_=!1;return y}}function Ce(n){return n.length>1}function Ne(){var n,t=[];return{lineStart:function(){t.push(n=[])},point:function(t,e){n.push([t,e])},lineEnd:v,buffer:function(){var e=t;return t=[],n=null,e},rejoin:function(){t.length>1&&t.push(t.pop().concat(t.shift()))}}}function ze(n,t){return((n=n.x)[0]<0?n[1]-Sa-ka:Sa-n[1])-((t=t.x)[0]<0?t[1]-Sa-ka:Sa-t[1])}function Le(n,t){var e=n[0],r=n[1],u=[Math.sin(e),-Math.cos(e),0],i=0,o=0;lc.reset();for(var a=0,c=t.length;c>a;++a){var s=t[a],l=s.length;if(l)for(var f=s[0],h=f[0],g=f[1]/2+ba/4,p=Math.sin(g),v=Math.cos(g),d=1;;){d===l&&(d=0),n=s[d];var m=n[0],y=n[1]/2+ba/4,x=Math.sin(y),M=Math.cos(y),_=m-h,b=_>=0?1:-1,w=b*_,S=w>ba,k=p*x;if(lc.add(Math.atan2(k*b*Math.sin(w),v*M+k*Math.cos(w))),i+=S?_+b*wa:_,S^h>=e^m>=e){var E=he(le(f),le(n));ve(E);var A=he(u,E);ve(A);var C=(S^_>=0?-1:1)*G(A[2]);(r>C||r===C&&(E[0]||E[1]))&&(o+=S^_>=0?1:-1)}if(!d++)break;h=m,p=x,v=M,f=n}}return(-ka>i||ka>i&&0>lc)^1&o}function Te(n){var t,e=0/0,r=0/0,u=0/0;return{lineStart:function(){n.lineStart(),t=1},point:function(i,o){var a=i>0?ba:-ba,c=ua(i-e);ua(c-ba)<ka?(n.point(e,r=(r+o)/2>0?Sa:-Sa),n.point(u,r),n.lineEnd(),n.lineStart(),n.point(a,r),n.point(i,r),t=0):u!==a&&c>=ba&&(ua(e-u)<ka&&(e-=u*ka),ua(i-a)<ka&&(i-=a*ka),r=qe(e,r,i,o),n.point(u,r),n.lineEnd(),n.lineStart(),n.point(a,r),t=0),n.point(e=i,r=o),u=a},lineEnd:function(){n.lineEnd(),e=r=0/0},clean:function(){return 2-t}}}function qe(n,t,e,r){var u,i,o=Math.sin(n-e);return ua(o)>ka?Math.atan((Math.sin(t)*(i=Math.cos(r))*Math.sin(e)-Math.sin(r)*(u=Math.cos(t))*Math.sin(n))/(u*i*o)):(t+r)/2}function Re(n,t,e,r){var u;if(null==n)u=e*Sa,r.point(-ba,u),r.point(0,u),r.point(ba,u),r.point(ba,0),r.point(ba,-u),r.point(0,-u),r.point(-ba,-u),r.point(-ba,0),r.point(-ba,u);else if(ua(n[0]-t[0])>ka){var i=n[0]<t[0]?ba:-ba;u=e*i/2,r.point(-i,u),r.point(0,u),r.point(i,u)}else r.point(t[0],t[1])}function De(n){function t(n,t){return Math.cos(n)*Math.cos(t)>i}function e(n){var e,i,c,s,l;return{lineStart:function(){s=c=!1,l=1},point:function(f,h){var g,p=[f,h],v=t(f,h),d=o?v?0:u(f,h):v?u(f+(0>f?ba:-ba),h):0;if(!e&&(s=c=v)&&n.lineStart(),v!==c&&(g=r(e,p),(me(e,g)||me(p,g))&&(p[0]+=ka,p[1]+=ka,v=t(p[0],p[1]))),v!==c)l=0,v?(n.lineStart(),g=r(p,e),n.point(g[0],g[1])):(g=r(e,p),n.point(g[0],g[1]),n.lineEnd()),e=g;else if(a&&e&&o^v){var m;d&i||!(m=r(p,e,!0))||(l=0,o?(n.lineStart(),n.point(m[0][0],m[0][1]),n.point(m[1][0],m[1][1]),n.lineEnd()):(n.point(m[1][0],m[1][1]),n.lineEnd(),n.lineStart(),n.point(m[0][0],m[0][1])))}!v||e&&me(e,p)||n.point(p[0],p[1]),e=p,c=v,i=d},lineEnd:function(){c&&n.lineEnd(),e=null},clean:function(){return l|(s&&c)<<1}}}function r(n,t,e){var r=le(n),u=le(t),o=[1,0,0],a=he(r,u),c=fe(a,a),s=a[0],l=c-s*s;if(!l)return!e&&n;var f=i*c/l,h=-i*s/l,g=he(o,a),p=pe(o,f),v=pe(a,h);ge(p,v);var d=g,m=fe(p,d),y=fe(d,d),x=m*m-y*(fe(p,p)-1);if(!(0>x)){var M=Math.sqrt(x),_=pe(d,(-m-M)/y);if(ge(_,p),_=de(_),!e)return _;var b,w=n[0],S=t[0],k=n[1],E=t[1];w>S&&(b=w,w=S,S=b);var A=S-w,C=ua(A-ba)<ka,N=C||ka>A;if(!C&&k>E&&(b=k,k=E,E=b),N?C?k+E>0^_[1]<(ua(_[0]-w)<ka?k:E):k<=_[1]&&_[1]<=E:A>ba^(w<=_[0]&&_[0]<=S)){var z=pe(d,(-m+M)/y);return ge(z,p),[_,de(z)]}}}function u(t,e){var r=o?n:ba-n,u=0;return-r>t?u|=1:t>r&&(u|=2),-r>e?u|=4:e>r&&(u|=8),u}var i=Math.cos(n),o=i>0,a=ua(i)>ka,c=sr(n,6*Aa);return Ae(t,e,c,o?[0,-n]:[-ba,n-ba])}function Pe(n,t,e,r){return function(u){var i,o=u.a,a=u.b,c=o.x,s=o.y,l=a.x,f=a.y,h=0,g=1,p=l-c,v=f-s;if(i=n-c,p||!(i>0)){if(i/=p,0>p){if(h>i)return;g>i&&(g=i)}else if(p>0){if(i>g)return;i>h&&(h=i)}if(i=e-c,p||!(0>i)){if(i/=p,0>p){if(i>g)return;i>h&&(h=i)}else if(p>0){if(h>i)return;g>i&&(g=i)}if(i=t-s,v||!(i>0)){if(i/=v,0>v){if(h>i)return;g>i&&(g=i)}else if(v>0){if(i>g)return;i>h&&(h=i)}if(i=r-s,v||!(0>i)){if(i/=v,0>v){if(i>g)return;i>h&&(h=i)}else if(v>0){if(h>i)return;g>i&&(g=i)}return h>0&&(u.a={x:c+h*p,y:s+h*v}),1>g&&(u.b={x:c+g*p,y:s+g*v}),u}}}}}}function Ue(n,t,e,r){function u(r,u){return ua(r[0]-n)<ka?u>0?0:3:ua(r[0]-e)<ka?u>0?2:1:ua(r[1]-t)<ka?u>0?1:0:u>0?3:2}function i(n,t){return o(n.x,t.x)}function o(n,t){var e=u(n,1),r=u(t,1);return e!==r?e-r:0===e?t[1]-n[1]:1===e?n[0]-t[0]:2===e?n[1]-t[1]:t[0]-n[0]}return function(a){function c(n){for(var t=0,e=d.length,r=n[1],u=0;e>u;++u)for(var i,o=1,a=d[u],c=a.length,s=a[0];c>o;++o)i=a[o],s[1]<=r?i[1]>r&&W(s,i,n)>0&&++t:i[1]<=r&&W(s,i,n)<0&&--t,s=i;return 0!==t}function s(i,a,c,s){var l=0,f=0;if(null==i||(l=u(i,c))!==(f=u(a,c))||o(i,a)<0^c>0){do s.point(0===l||3===l?n:e,l>1?r:t);while((l=(l+c+4)%4)!==f)}else s.point(a[0],a[1])}function l(u,i){return u>=n&&e>=u&&i>=t&&r>=i}function f(n,t){l(n,t)&&a.point(n,t)}function h(){N.point=p,d&&d.push(m=[]),S=!0,w=!1,_=b=0/0}function g(){v&&(p(y,x),M&&w&&A.rejoin(),v.push(A.buffer())),N.point=f,w&&a.lineEnd()}function p(n,t){n=Math.max(-kc,Math.min(kc,n)),t=Math.max(-kc,Math.min(kc,t));var e=l(n,t);if(d&&m.push([n,t]),S)y=n,x=t,M=e,S=!1,e&&(a.lineStart(),a.point(n,t));else if(e&&w)a.point(n,t);else{var r={a:{x:_,y:b},b:{x:n,y:t}};C(r)?(w||(a.lineStart(),a.point(r.a.x,r.a.y)),a.point(r.b.x,r.b.y),e||a.lineEnd(),k=!1):e&&(a.lineStart(),a.point(n,t),k=!1)}_=n,b=t,w=e}var v,d,m,y,x,M,_,b,w,S,k,E=a,A=Ne(),C=Pe(n,t,e,r),N={point:f,lineStart:h,lineEnd:g,polygonStart:function(){a=A,v=[],d=[],k=!0},polygonEnd:function(){a=E,v=Zo.merge(v);var t=c([n,r]),e=k&&t,u=v.length;(e||u)&&(a.polygonStart(),e&&(a.lineStart(),s(null,null,1,a),a.lineEnd()),u&&Se(v,i,t,s,a),a.polygonEnd()),v=d=m=null}};return N}}function je(n,t){function e(e,r){return e=n(e,r),t(e[0],e[1])}return n.invert&&t.invert&&(e.invert=function(e,r){return e=t.invert(e,r),e&&n.invert(e[0],e[1])}),e}function He(n){var t=0,e=ba/3,r=tr(n),u=r(t,e);return u.parallels=function(n){return arguments.length?r(t=n[0]*ba/180,e=n[1]*ba/180):[180*(t/ba),180*(e/ba)]},u}function Fe(n,t){function e(n,t){var e=Math.sqrt(i-2*u*Math.sin(t))/u;return[e*Math.sin(n*=u),o-e*Math.cos(n)]}var r=Math.sin(n),u=(r+Math.sin(t))/2,i=1+r*(2*u-r),o=Math.sqrt(i)/u;return e.invert=function(n,t){var e=o-t;return[Math.atan2(n,e)/u,G((i-(n*n+e*e)*u*u)/(2*u))]},e}function Oe(){function n(n,t){Ac+=u*n-r*t,r=n,u=t}var t,e,r,u;Tc.point=function(i,o){Tc.point=n,t=r=i,e=u=o},Tc.lineEnd=function(){n(t,e)}}function Ye(n,t){Cc>n&&(Cc=n),n>zc&&(zc=n),Nc>t&&(Nc=t),t>Lc&&(Lc=t)}function Ie(){function n(n,t){o.push("M",n,",",t,i)}function t(n,t){o.push("M",n,",",t),a.point=e}function e(n,t){o.push("L",n,",",t)}function r(){a.point=n}function u(){o.push("Z")}var i=Ze(4.5),o=[],a={point:n,lineStart:function(){a.point=t},lineEnd:r,polygonStart:function(){a.lineEnd=u},polygonEnd:function(){a.lineEnd=r,a.point=n},pointRadius:function(n){return i=Ze(n),a},result:function(){if(o.length){var n=o.join("");return o=[],n}}};return a}function Ze(n){return"m0,"+n+"a"+n+","+n+" 0 1,1 0,"+-2*n+"a"+n+","+n+" 0 1,1 0,"+2*n+"z"}function Ve(n,t){pc+=n,vc+=t,++dc}function Xe(){function n(n,r){var u=n-t,i=r-e,o=Math.sqrt(u*u+i*i);mc+=o*(t+n)/2,yc+=o*(e+r)/2,xc+=o,Ve(t=n,e=r)}var t,e;Rc.point=function(r,u){Rc.point=n,Ve(t=r,e=u)}}function $e(){Rc.point=Ve}function Be(){function n(n,t){var e=n-r,i=t-u,o=Math.sqrt(e*e+i*i);mc+=o*(r+n)/2,yc+=o*(u+t)/2,xc+=o,o=u*n-r*t,Mc+=o*(r+n),_c+=o*(u+t),bc+=3*o,Ve(r=n,u=t)}var t,e,r,u;Rc.point=function(i,o){Rc.point=n,Ve(t=r=i,e=u=o)},Rc.lineEnd=function(){n(t,e)}}function We(n){function t(t,e){n.moveTo(t,e),n.arc(t,e,o,0,wa)}function e(t,e){n.moveTo(t,e),a.point=r}function r(t,e){n.lineTo(t,e)}function u(){a.point=t}function i(){n.closePath()}var o=4.5,a={point:t,lineStart:function(){a.point=e},lineEnd:u,polygonStart:function(){a.lineEnd=i},polygonEnd:function(){a.lineEnd=u,a.point=t},pointRadius:function(n){return o=n,a},result:v};return a}function Je(n){function t(n){return(a?r:e)(n)}function e(t){return Qe(t,function(e,r){e=n(e,r),t.point(e[0],e[1])})}function r(t){function e(e,r){e=n(e,r),t.point(e[0],e[1])}function r(){x=0/0,S.point=i,t.lineStart()}function i(e,r){var i=le([e,r]),o=n(e,r);u(x,M,y,_,b,w,x=o[0],M=o[1],y=e,_=i[0],b=i[1],w=i[2],a,t),t.point(x,M)}function o(){S.point=e,t.lineEnd()}function c(){r(),S.point=s,S.lineEnd=l}function s(n,t){i(f=n,h=t),g=x,p=M,v=_,d=b,m=w,S.point=i}function l(){u(x,M,y,_,b,w,g,p,f,v,d,m,a,t),S.lineEnd=o,o()}var f,h,g,p,v,d,m,y,x,M,_,b,w,S={point:e,lineStart:r,lineEnd:o,polygonStart:function(){t.polygonStart(),S.lineStart=c},polygonEnd:function(){t.polygonEnd(),S.lineStart=r}};return S}function u(t,e,r,a,c,s,l,f,h,g,p,v,d,m){var y=l-t,x=f-e,M=y*y+x*x;if(M>4*i&&d--){var _=a+g,b=c+p,w=s+v,S=Math.sqrt(_*_+b*b+w*w),k=Math.asin(w/=S),E=ua(ua(w)-1)<ka||ua(r-h)<ka?(r+h)/2:Math.atan2(b,_),A=n(E,k),C=A[0],N=A[1],z=C-t,L=N-e,T=x*z-y*L;(T*T/M>i||ua((y*z+x*L)/M-.5)>.3||o>a*g+c*p+s*v)&&(u(t,e,r,a,c,s,C,N,E,_/=S,b/=S,w,d,m),m.point(C,N),u(C,N,E,_,b,w,l,f,h,g,p,v,d,m))}}var i=.5,o=Math.cos(30*Aa),a=16; +return t.precision=function(n){return arguments.length?(a=(i=n*n)>0&&16,t):Math.sqrt(i)},t}function Ge(n){var t=Je(function(t,e){return n([t*Ca,e*Ca])});return function(n){return er(t(n))}}function Ke(n){this.stream=n}function Qe(n,t){return{point:t,sphere:function(){n.sphere()},lineStart:function(){n.lineStart()},lineEnd:function(){n.lineEnd()},polygonStart:function(){n.polygonStart()},polygonEnd:function(){n.polygonEnd()}}}function nr(n){return tr(function(){return n})()}function tr(n){function t(n){return n=a(n[0]*Aa,n[1]*Aa),[n[0]*h+c,s-n[1]*h]}function e(n){return n=a.invert((n[0]-c)/h,(s-n[1])/h),n&&[n[0]*Ca,n[1]*Ca]}function r(){a=je(o=ir(m,y,x),i);var n=i(v,d);return c=g-n[0]*h,s=p+n[1]*h,u()}function u(){return l&&(l.valid=!1,l=null),t}var i,o,a,c,s,l,f=Je(function(n,t){return n=i(n,t),[n[0]*h+c,s-n[1]*h]}),h=150,g=480,p=250,v=0,d=0,m=0,y=0,x=0,M=Sc,_=wt,b=null,w=null;return t.stream=function(n){return l&&(l.valid=!1),l=er(M(o,f(_(n)))),l.valid=!0,l},t.clipAngle=function(n){return arguments.length?(M=null==n?(b=n,Sc):De((b=+n)*Aa),u()):b},t.clipExtent=function(n){return arguments.length?(w=n,_=n?Ue(n[0][0],n[0][1],n[1][0],n[1][1]):wt,u()):w},t.scale=function(n){return arguments.length?(h=+n,r()):h},t.translate=function(n){return arguments.length?(g=+n[0],p=+n[1],r()):[g,p]},t.center=function(n){return arguments.length?(v=n[0]%360*Aa,d=n[1]%360*Aa,r()):[v*Ca,d*Ca]},t.rotate=function(n){return arguments.length?(m=n[0]%360*Aa,y=n[1]%360*Aa,x=n.length>2?n[2]%360*Aa:0,r()):[m*Ca,y*Ca,x*Ca]},Zo.rebind(t,f,"precision"),function(){return i=n.apply(this,arguments),t.invert=i.invert&&e,r()}}function er(n){return Qe(n,function(t,e){n.point(t*Aa,e*Aa)})}function rr(n,t){return[n,t]}function ur(n,t){return[n>ba?n-wa:-ba>n?n+wa:n,t]}function ir(n,t,e){return n?t||e?je(ar(n),cr(t,e)):ar(n):t||e?cr(t,e):ur}function or(n){return function(t,e){return t+=n,[t>ba?t-wa:-ba>t?t+wa:t,e]}}function ar(n){var t=or(n);return t.invert=or(-n),t}function cr(n,t){function e(n,t){var e=Math.cos(t),a=Math.cos(n)*e,c=Math.sin(n)*e,s=Math.sin(t),l=s*r+a*u;return[Math.atan2(c*i-l*o,a*r-s*u),G(l*i+c*o)]}var r=Math.cos(n),u=Math.sin(n),i=Math.cos(t),o=Math.sin(t);return e.invert=function(n,t){var e=Math.cos(t),a=Math.cos(n)*e,c=Math.sin(n)*e,s=Math.sin(t),l=s*i-c*o;return[Math.atan2(c*i+s*o,a*r+l*u),G(l*r-a*u)]},e}function sr(n,t){var e=Math.cos(n),r=Math.sin(n);return function(u,i,o,a){var c=o*t;null!=u?(u=lr(e,u),i=lr(e,i),(o>0?i>u:u>i)&&(u+=o*wa)):(u=n+o*wa,i=n-.5*c);for(var s,l=u;o>0?l>i:i>l;l-=c)a.point((s=de([e,-r*Math.cos(l),-r*Math.sin(l)]))[0],s[1])}}function lr(n,t){var e=le(t);e[0]-=n,ve(e);var r=J(-e[1]);return((-e[2]<0?-r:r)+2*Math.PI-ka)%(2*Math.PI)}function fr(n,t,e){var r=Zo.range(n,t-ka,e).concat(t);return function(n){return r.map(function(t){return[n,t]})}}function hr(n,t,e){var r=Zo.range(n,t-ka,e).concat(t);return function(n){return r.map(function(t){return[t,n]})}}function gr(n){return n.source}function pr(n){return n.target}function vr(n,t,e,r){var u=Math.cos(t),i=Math.sin(t),o=Math.cos(r),a=Math.sin(r),c=u*Math.cos(n),s=u*Math.sin(n),l=o*Math.cos(e),f=o*Math.sin(e),h=2*Math.asin(Math.sqrt(tt(r-t)+u*o*tt(e-n))),g=1/Math.sin(h),p=h?function(n){var t=Math.sin(n*=h)*g,e=Math.sin(h-n)*g,r=e*c+t*l,u=e*s+t*f,o=e*i+t*a;return[Math.atan2(u,r)*Ca,Math.atan2(o,Math.sqrt(r*r+u*u))*Ca]}:function(){return[n*Ca,t*Ca]};return p.distance=h,p}function dr(){function n(n,u){var i=Math.sin(u*=Aa),o=Math.cos(u),a=ua((n*=Aa)-t),c=Math.cos(a);Dc+=Math.atan2(Math.sqrt((a=o*Math.sin(a))*a+(a=r*i-e*o*c)*a),e*i+r*o*c),t=n,e=i,r=o}var t,e,r;Pc.point=function(u,i){t=u*Aa,e=Math.sin(i*=Aa),r=Math.cos(i),Pc.point=n},Pc.lineEnd=function(){Pc.point=Pc.lineEnd=v}}function mr(n,t){function e(t,e){var r=Math.cos(t),u=Math.cos(e),i=n(r*u);return[i*u*Math.sin(t),i*Math.sin(e)]}return e.invert=function(n,e){var r=Math.sqrt(n*n+e*e),u=t(r),i=Math.sin(u),o=Math.cos(u);return[Math.atan2(n*i,r*o),Math.asin(r&&e*i/r)]},e}function yr(n,t){function e(n,t){o>0?-Sa+ka>t&&(t=-Sa+ka):t>Sa-ka&&(t=Sa-ka);var e=o/Math.pow(u(t),i);return[e*Math.sin(i*n),o-e*Math.cos(i*n)]}var r=Math.cos(n),u=function(n){return Math.tan(ba/4+n/2)},i=n===t?Math.sin(n):Math.log(r/Math.cos(t))/Math.log(u(t)/u(n)),o=r*Math.pow(u(n),i)/i;return i?(e.invert=function(n,t){var e=o-t,r=B(i)*Math.sqrt(n*n+e*e);return[Math.atan2(n,e)/i,2*Math.atan(Math.pow(o/r,1/i))-Sa]},e):Mr}function xr(n,t){function e(n,t){var e=i-t;return[e*Math.sin(u*n),i-e*Math.cos(u*n)]}var r=Math.cos(n),u=n===t?Math.sin(n):(r-Math.cos(t))/(t-n),i=r/u+n;return ua(u)<ka?rr:(e.invert=function(n,t){var e=i-t;return[Math.atan2(n,e)/u,i-B(u)*Math.sqrt(n*n+e*e)]},e)}function Mr(n,t){return[n,Math.log(Math.tan(ba/4+t/2))]}function _r(n){var t,e=nr(n),r=e.scale,u=e.translate,i=e.clipExtent;return e.scale=function(){var n=r.apply(e,arguments);return n===e?t?e.clipExtent(null):e:n},e.translate=function(){var n=u.apply(e,arguments);return n===e?t?e.clipExtent(null):e:n},e.clipExtent=function(n){var o=i.apply(e,arguments);if(o===e){if(t=null==n){var a=ba*r(),c=u();i([[c[0]-a,c[1]-a],[c[0]+a,c[1]+a]])}}else t&&(o=null);return o},e.clipExtent(null)}function br(n,t){return[Math.log(Math.tan(ba/4+t/2)),-n]}function wr(n){return n[0]}function Sr(n){return n[1]}function kr(n){for(var t=n.length,e=[0,1],r=2,u=2;t>u;u++){for(;r>1&&W(n[e[r-2]],n[e[r-1]],n[u])<=0;)--r;e[r++]=u}return e.slice(0,r)}function Er(n,t){return n[0]-t[0]||n[1]-t[1]}function Ar(n,t,e){return(e[0]-t[0])*(n[1]-t[1])<(e[1]-t[1])*(n[0]-t[0])}function Cr(n,t,e,r){var u=n[0],i=e[0],o=t[0]-u,a=r[0]-i,c=n[1],s=e[1],l=t[1]-c,f=r[1]-s,h=(a*(c-s)-f*(u-i))/(f*o-a*l);return[u+h*o,c+h*l]}function Nr(n){var t=n[0],e=n[n.length-1];return!(t[0]-e[0]||t[1]-e[1])}function zr(){Gr(this),this.edge=this.site=this.circle=null}function Lr(n){var t=Bc.pop()||new zr;return t.site=n,t}function Tr(n){Yr(n),Vc.remove(n),Bc.push(n),Gr(n)}function qr(n){var t=n.circle,e=t.x,r=t.cy,u={x:e,y:r},i=n.P,o=n.N,a=[n];Tr(n);for(var c=i;c.circle&&ua(e-c.circle.x)<ka&&ua(r-c.circle.cy)<ka;)i=c.P,a.unshift(c),Tr(c),c=i;a.unshift(c),Yr(c);for(var s=o;s.circle&&ua(e-s.circle.x)<ka&&ua(r-s.circle.cy)<ka;)o=s.N,a.push(s),Tr(s),s=o;a.push(s),Yr(s);var l,f=a.length;for(l=1;f>l;++l)s=a[l],c=a[l-1],Br(s.edge,c.site,s.site,u);c=a[0],s=a[f-1],s.edge=Xr(c.site,s.site,null,u),Or(c),Or(s)}function Rr(n){for(var t,e,r,u,i=n.x,o=n.y,a=Vc._;a;)if(r=Dr(a,o)-i,r>ka)a=a.L;else{if(u=i-Pr(a,o),!(u>ka)){r>-ka?(t=a.P,e=a):u>-ka?(t=a,e=a.N):t=e=a;break}if(!a.R){t=a;break}a=a.R}var c=Lr(n);if(Vc.insert(t,c),t||e){if(t===e)return Yr(t),e=Lr(t.site),Vc.insert(c,e),c.edge=e.edge=Xr(t.site,c.site),Or(t),Or(e),void 0;if(!e)return c.edge=Xr(t.site,c.site),void 0;Yr(t),Yr(e);var s=t.site,l=s.x,f=s.y,h=n.x-l,g=n.y-f,p=e.site,v=p.x-l,d=p.y-f,m=2*(h*d-g*v),y=h*h+g*g,x=v*v+d*d,M={x:(d*y-g*x)/m+l,y:(h*x-v*y)/m+f};Br(e.edge,s,p,M),c.edge=Xr(s,n,null,M),e.edge=Xr(n,p,null,M),Or(t),Or(e)}}function Dr(n,t){var e=n.site,r=e.x,u=e.y,i=u-t;if(!i)return r;var o=n.P;if(!o)return-1/0;e=o.site;var a=e.x,c=e.y,s=c-t;if(!s)return a;var l=a-r,f=1/i-1/s,h=l/s;return f?(-h+Math.sqrt(h*h-2*f*(l*l/(-2*s)-c+s/2+u-i/2)))/f+r:(r+a)/2}function Pr(n,t){var e=n.N;if(e)return Dr(e,t);var r=n.site;return r.y===t?r.x:1/0}function Ur(n){this.site=n,this.edges=[]}function jr(n){for(var t,e,r,u,i,o,a,c,s,l,f=n[0][0],h=n[1][0],g=n[0][1],p=n[1][1],v=Zc,d=v.length;d--;)if(i=v[d],i&&i.prepare())for(a=i.edges,c=a.length,o=0;c>o;)l=a[o].end(),r=l.x,u=l.y,s=a[++o%c].start(),t=s.x,e=s.y,(ua(r-t)>ka||ua(u-e)>ka)&&(a.splice(o,0,new Wr($r(i.site,l,ua(r-f)<ka&&p-u>ka?{x:f,y:ua(t-f)<ka?e:p}:ua(u-p)<ka&&h-r>ka?{x:ua(e-p)<ka?t:h,y:p}:ua(r-h)<ka&&u-g>ka?{x:h,y:ua(t-h)<ka?e:g}:ua(u-g)<ka&&r-f>ka?{x:ua(e-g)<ka?t:f,y:g}:null),i.site,null)),++c)}function Hr(n,t){return t.angle-n.angle}function Fr(){Gr(this),this.x=this.y=this.arc=this.site=this.cy=null}function Or(n){var t=n.P,e=n.N;if(t&&e){var r=t.site,u=n.site,i=e.site;if(r!==i){var o=u.x,a=u.y,c=r.x-o,s=r.y-a,l=i.x-o,f=i.y-a,h=2*(c*f-s*l);if(!(h>=-Ea)){var g=c*c+s*s,p=l*l+f*f,v=(f*g-s*p)/h,d=(c*p-l*g)/h,f=d+a,m=Wc.pop()||new Fr;m.arc=n,m.site=u,m.x=v+o,m.y=f+Math.sqrt(v*v+d*d),m.cy=f,n.circle=m;for(var y=null,x=$c._;x;)if(m.y<x.y||m.y===x.y&&m.x<=x.x){if(!x.L){y=x.P;break}x=x.L}else{if(!x.R){y=x;break}x=x.R}$c.insert(y,m),y||(Xc=m)}}}}function Yr(n){var t=n.circle;t&&(t.P||(Xc=t.N),$c.remove(t),Wc.push(t),Gr(t),n.circle=null)}function Ir(n){for(var t,e=Ic,r=Pe(n[0][0],n[0][1],n[1][0],n[1][1]),u=e.length;u--;)t=e[u],(!Zr(t,n)||!r(t)||ua(t.a.x-t.b.x)<ka&&ua(t.a.y-t.b.y)<ka)&&(t.a=t.b=null,e.splice(u,1))}function Zr(n,t){var e=n.b;if(e)return!0;var r,u,i=n.a,o=t[0][0],a=t[1][0],c=t[0][1],s=t[1][1],l=n.l,f=n.r,h=l.x,g=l.y,p=f.x,v=f.y,d=(h+p)/2,m=(g+v)/2;if(v===g){if(o>d||d>=a)return;if(h>p){if(i){if(i.y>=s)return}else i={x:d,y:c};e={x:d,y:s}}else{if(i){if(i.y<c)return}else i={x:d,y:s};e={x:d,y:c}}}else if(r=(h-p)/(v-g),u=m-r*d,-1>r||r>1)if(h>p){if(i){if(i.y>=s)return}else i={x:(c-u)/r,y:c};e={x:(s-u)/r,y:s}}else{if(i){if(i.y<c)return}else i={x:(s-u)/r,y:s};e={x:(c-u)/r,y:c}}else if(v>g){if(i){if(i.x>=a)return}else i={x:o,y:r*o+u};e={x:a,y:r*a+u}}else{if(i){if(i.x<o)return}else i={x:a,y:r*a+u};e={x:o,y:r*o+u}}return n.a=i,n.b=e,!0}function Vr(n,t){this.l=n,this.r=t,this.a=this.b=null}function Xr(n,t,e,r){var u=new Vr(n,t);return Ic.push(u),e&&Br(u,n,t,e),r&&Br(u,t,n,r),Zc[n.i].edges.push(new Wr(u,n,t)),Zc[t.i].edges.push(new Wr(u,t,n)),u}function $r(n,t,e){var r=new Vr(n,null);return r.a=t,r.b=e,Ic.push(r),r}function Br(n,t,e,r){n.a||n.b?n.l===e?n.b=r:n.a=r:(n.a=r,n.l=t,n.r=e)}function Wr(n,t,e){var r=n.a,u=n.b;this.edge=n,this.site=t,this.angle=e?Math.atan2(e.y-t.y,e.x-t.x):n.l===t?Math.atan2(u.x-r.x,r.y-u.y):Math.atan2(r.x-u.x,u.y-r.y)}function Jr(){this._=null}function Gr(n){n.U=n.C=n.L=n.R=n.P=n.N=null}function Kr(n,t){var e=t,r=t.R,u=e.U;u?u.L===e?u.L=r:u.R=r:n._=r,r.U=u,e.U=r,e.R=r.L,e.R&&(e.R.U=e),r.L=e}function Qr(n,t){var e=t,r=t.L,u=e.U;u?u.L===e?u.L=r:u.R=r:n._=r,r.U=u,e.U=r,e.L=r.R,e.L&&(e.L.U=e),r.R=e}function nu(n){for(;n.L;)n=n.L;return n}function tu(n,t){var e,r,u,i=n.sort(eu).pop();for(Ic=[],Zc=new Array(n.length),Vc=new Jr,$c=new Jr;;)if(u=Xc,i&&(!u||i.y<u.y||i.y===u.y&&i.x<u.x))(i.x!==e||i.y!==r)&&(Zc[i.i]=new Ur(i),Rr(i),e=i.x,r=i.y),i=n.pop();else{if(!u)break;qr(u.arc)}t&&(Ir(t),jr(t));var o={cells:Zc,edges:Ic};return Vc=$c=Ic=Zc=null,o}function eu(n,t){return t.y-n.y||t.x-n.x}function ru(n,t,e){return(n.x-e.x)*(t.y-n.y)-(n.x-t.x)*(e.y-n.y)}function uu(n){return n.x}function iu(n){return n.y}function ou(){return{leaf:!0,nodes:[],point:null,x:null,y:null}}function au(n,t,e,r,u,i){if(!n(t,e,r,u,i)){var o=.5*(e+u),a=.5*(r+i),c=t.nodes;c[0]&&au(n,c[0],e,r,o,a),c[1]&&au(n,c[1],o,r,u,a),c[2]&&au(n,c[2],e,a,o,i),c[3]&&au(n,c[3],o,a,u,i)}}function cu(n,t){n=Zo.rgb(n),t=Zo.rgb(t);var e=n.r,r=n.g,u=n.b,i=t.r-e,o=t.g-r,a=t.b-u;return function(n){return"#"+dt(Math.round(e+i*n))+dt(Math.round(r+o*n))+dt(Math.round(u+a*n))}}function su(n,t){var e,r={},u={};for(e in n)e in t?r[e]=hu(n[e],t[e]):u[e]=n[e];for(e in t)e in n||(u[e]=t[e]);return function(n){for(e in r)u[e]=r[e](n);return u}}function lu(n,t){return t-=n=+n,function(e){return n+t*e}}function fu(n,t){var e,r,u,i=Gc.lastIndex=Kc.lastIndex=0,o=-1,a=[],c=[];for(n+="",t+="";(e=Gc.exec(n))&&(r=Kc.exec(t));)(u=r.index)>i&&(u=t.substring(i,u),a[o]?a[o]+=u:a[++o]=u),(e=e[0])===(r=r[0])?a[o]?a[o]+=r:a[++o]=r:(a[++o]=null,c.push({i:o,x:lu(e,r)})),i=Kc.lastIndex;return i<t.length&&(u=t.substring(i),a[o]?a[o]+=u:a[++o]=u),a.length<2?c[0]?(t=c[0].x,function(n){return t(n)+""}):function(){return t}:(t=c.length,function(n){for(var e,r=0;t>r;++r)a[(e=c[r]).i]=e.x(n);return a.join("")})}function hu(n,t){for(var e,r=Zo.interpolators.length;--r>=0&&!(e=Zo.interpolators[r](n,t)););return e}function gu(n,t){var e,r=[],u=[],i=n.length,o=t.length,a=Math.min(n.length,t.length);for(e=0;a>e;++e)r.push(hu(n[e],t[e]));for(;i>e;++e)u[e]=n[e];for(;o>e;++e)u[e]=t[e];return function(n){for(e=0;a>e;++e)u[e]=r[e](n);return u}}function pu(n){return function(t){return 0>=t?0:t>=1?1:n(t)}}function vu(n){return function(t){return 1-n(1-t)}}function du(n){return function(t){return.5*(.5>t?n(2*t):2-n(2-2*t))}}function mu(n){return n*n}function yu(n){return n*n*n}function xu(n){if(0>=n)return 0;if(n>=1)return 1;var t=n*n,e=t*n;return 4*(.5>n?e:3*(n-t)+e-.75)}function Mu(n){return function(t){return Math.pow(t,n)}}function _u(n){return 1-Math.cos(n*Sa)}function bu(n){return Math.pow(2,10*(n-1))}function wu(n){return 1-Math.sqrt(1-n*n)}function Su(n,t){var e;return arguments.length<2&&(t=.45),arguments.length?e=t/wa*Math.asin(1/n):(n=1,e=t/4),function(r){return 1+n*Math.pow(2,-10*r)*Math.sin((r-e)*wa/t)}}function ku(n){return n||(n=1.70158),function(t){return t*t*((n+1)*t-n)}}function Eu(n){return 1/2.75>n?7.5625*n*n:2/2.75>n?7.5625*(n-=1.5/2.75)*n+.75:2.5/2.75>n?7.5625*(n-=2.25/2.75)*n+.9375:7.5625*(n-=2.625/2.75)*n+.984375}function Au(n,t){n=Zo.hcl(n),t=Zo.hcl(t);var e=n.h,r=n.c,u=n.l,i=t.h-e,o=t.c-r,a=t.l-u;return isNaN(o)&&(o=0,r=isNaN(r)?t.c:r),isNaN(i)?(i=0,e=isNaN(e)?t.h:e):i>180?i-=360:-180>i&&(i+=360),function(n){return ot(e+i*n,r+o*n,u+a*n)+""}}function Cu(n,t){n=Zo.hsl(n),t=Zo.hsl(t);var e=n.h,r=n.s,u=n.l,i=t.h-e,o=t.s-r,a=t.l-u;return isNaN(o)&&(o=0,r=isNaN(r)?t.s:r),isNaN(i)?(i=0,e=isNaN(e)?t.h:e):i>180?i-=360:-180>i&&(i+=360),function(n){return ut(e+i*n,r+o*n,u+a*n)+""}}function Nu(n,t){n=Zo.lab(n),t=Zo.lab(t);var e=n.l,r=n.a,u=n.b,i=t.l-e,o=t.a-r,a=t.b-u;return function(n){return ct(e+i*n,r+o*n,u+a*n)+""}}function zu(n,t){return t-=n,function(e){return Math.round(n+t*e)}}function Lu(n){var t=[n.a,n.b],e=[n.c,n.d],r=qu(t),u=Tu(t,e),i=qu(Ru(e,t,-u))||0;t[0]*e[1]<e[0]*t[1]&&(t[0]*=-1,t[1]*=-1,r*=-1,u*=-1),this.rotate=(r?Math.atan2(t[1],t[0]):Math.atan2(-e[0],e[1]))*Ca,this.translate=[n.e,n.f],this.scale=[r,i],this.skew=i?Math.atan2(u,i)*Ca:0}function Tu(n,t){return n[0]*t[0]+n[1]*t[1]}function qu(n){var t=Math.sqrt(Tu(n,n));return t&&(n[0]/=t,n[1]/=t),t}function Ru(n,t,e){return n[0]+=e*t[0],n[1]+=e*t[1],n}function Du(n,t){var e,r=[],u=[],i=Zo.transform(n),o=Zo.transform(t),a=i.translate,c=o.translate,s=i.rotate,l=o.rotate,f=i.skew,h=o.skew,g=i.scale,p=o.scale;return a[0]!=c[0]||a[1]!=c[1]?(r.push("translate(",null,",",null,")"),u.push({i:1,x:lu(a[0],c[0])},{i:3,x:lu(a[1],c[1])})):c[0]||c[1]?r.push("translate("+c+")"):r.push(""),s!=l?(s-l>180?l+=360:l-s>180&&(s+=360),u.push({i:r.push(r.pop()+"rotate(",null,")")-2,x:lu(s,l)})):l&&r.push(r.pop()+"rotate("+l+")"),f!=h?u.push({i:r.push(r.pop()+"skewX(",null,")")-2,x:lu(f,h)}):h&&r.push(r.pop()+"skewX("+h+")"),g[0]!=p[0]||g[1]!=p[1]?(e=r.push(r.pop()+"scale(",null,",",null,")"),u.push({i:e-4,x:lu(g[0],p[0])},{i:e-2,x:lu(g[1],p[1])})):(1!=p[0]||1!=p[1])&&r.push(r.pop()+"scale("+p+")"),e=u.length,function(n){for(var t,i=-1;++i<e;)r[(t=u[i]).i]=t.x(n);return r.join("")}}function Pu(n,t){return t=t-(n=+n)?1/(t-n):0,function(e){return(e-n)*t}}function Uu(n,t){return t=t-(n=+n)?1/(t-n):0,function(e){return Math.max(0,Math.min(1,(e-n)*t))}}function ju(n){for(var t=n.source,e=n.target,r=Fu(t,e),u=[t];t!==r;)t=t.parent,u.push(t);for(var i=u.length;e!==r;)u.splice(i,0,e),e=e.parent;return u}function Hu(n){for(var t=[],e=n.parent;null!=e;)t.push(n),n=e,e=e.parent;return t.push(n),t}function Fu(n,t){if(n===t)return n;for(var e=Hu(n),r=Hu(t),u=e.pop(),i=r.pop(),o=null;u===i;)o=u,u=e.pop(),i=r.pop();return o}function Ou(n){n.fixed|=2}function Yu(n){n.fixed&=-7}function Iu(n){n.fixed|=4,n.px=n.x,n.py=n.y}function Zu(n){n.fixed&=-5}function Vu(n,t,e){var r=0,u=0;if(n.charge=0,!n.leaf)for(var i,o=n.nodes,a=o.length,c=-1;++c<a;)i=o[c],null!=i&&(Vu(i,t,e),n.charge+=i.charge,r+=i.charge*i.cx,u+=i.charge*i.cy);if(n.point){n.leaf||(n.point.x+=Math.random()-.5,n.point.y+=Math.random()-.5);var s=t*e[n.point.index];n.charge+=n.pointCharge=s,r+=s*n.point.x,u+=s*n.point.y}n.cx=r/n.charge,n.cy=u/n.charge}function Xu(n,t){return Zo.rebind(n,t,"sort","children","value"),n.nodes=n,n.links=Ku,n}function $u(n,t){for(var e=[n];null!=(n=e.pop());)if(t(n),(u=n.children)&&(r=u.length))for(var r,u;--r>=0;)e.push(u[r])}function Bu(n,t){for(var e=[n],r=[];null!=(n=e.pop());)if(r.push(n),(i=n.children)&&(u=i.length))for(var u,i,o=-1;++o<u;)e.push(i[o]);for(;null!=(n=r.pop());)t(n)}function Wu(n){return n.children}function Ju(n){return n.value}function Gu(n,t){return t.value-n.value}function Ku(n){return Zo.merge(n.map(function(n){return(n.children||[]).map(function(t){return{source:n,target:t}})}))}function Qu(n){return n.x}function ni(n){return n.y}function ti(n,t,e){n.y0=t,n.y=e}function ei(n){return Zo.range(n.length)}function ri(n){for(var t=-1,e=n[0].length,r=[];++t<e;)r[t]=0;return r}function ui(n){for(var t,e=1,r=0,u=n[0][1],i=n.length;i>e;++e)(t=n[e][1])>u&&(r=e,u=t);return r}function ii(n){return n.reduce(oi,0)}function oi(n,t){return n+t[1]}function ai(n,t){return ci(n,Math.ceil(Math.log(t.length)/Math.LN2+1))}function ci(n,t){for(var e=-1,r=+n[0],u=(n[1]-r)/t,i=[];++e<=t;)i[e]=u*e+r;return i}function si(n){return[Zo.min(n),Zo.max(n)]}function li(n,t){return n.value-t.value}function fi(n,t){var e=n._pack_next;n._pack_next=t,t._pack_prev=n,t._pack_next=e,e._pack_prev=t}function hi(n,t){n._pack_next=t,t._pack_prev=n}function gi(n,t){var e=t.x-n.x,r=t.y-n.y,u=n.r+t.r;return.999*u*u>e*e+r*r}function pi(n){function t(n){l=Math.min(n.x-n.r,l),f=Math.max(n.x+n.r,f),h=Math.min(n.y-n.r,h),g=Math.max(n.y+n.r,g)}if((e=n.children)&&(s=e.length)){var e,r,u,i,o,a,c,s,l=1/0,f=-1/0,h=1/0,g=-1/0;if(e.forEach(vi),r=e[0],r.x=-r.r,r.y=0,t(r),s>1&&(u=e[1],u.x=u.r,u.y=0,t(u),s>2))for(i=e[2],yi(r,u,i),t(i),fi(r,i),r._pack_prev=i,fi(i,u),u=r._pack_next,o=3;s>o;o++){yi(r,u,i=e[o]);var p=0,v=1,d=1;for(a=u._pack_next;a!==u;a=a._pack_next,v++)if(gi(a,i)){p=1;break}if(1==p)for(c=r._pack_prev;c!==a._pack_prev&&!gi(c,i);c=c._pack_prev,d++);p?(d>v||v==d&&u.r<r.r?hi(r,u=a):hi(r=c,u),o--):(fi(r,i),u=i,t(i))}var m=(l+f)/2,y=(h+g)/2,x=0;for(o=0;s>o;o++)i=e[o],i.x-=m,i.y-=y,x=Math.max(x,i.r+Math.sqrt(i.x*i.x+i.y*i.y));n.r=x,e.forEach(di)}}function vi(n){n._pack_next=n._pack_prev=n}function di(n){delete n._pack_next,delete n._pack_prev}function mi(n,t,e,r){var u=n.children;if(n.x=t+=r*n.x,n.y=e+=r*n.y,n.r*=r,u)for(var i=-1,o=u.length;++i<o;)mi(u[i],t,e,r)}function yi(n,t,e){var r=n.r+e.r,u=t.x-n.x,i=t.y-n.y;if(r&&(u||i)){var o=t.r+e.r,a=u*u+i*i;o*=o,r*=r;var c=.5+(r-o)/(2*a),s=Math.sqrt(Math.max(0,2*o*(r+a)-(r-=a)*r-o*o))/(2*a);e.x=n.x+c*u+s*i,e.y=n.y+c*i-s*u}else e.x=n.x+r,e.y=n.y}function xi(n,t){return n.parent==t.parent?1:2}function Mi(n){var t=n.children;return t.length?t[0]:n.t}function _i(n){var t,e=n.children;return(t=e.length)?e[t-1]:n.t}function bi(n,t,e){var r=e/(t.i-n.i);t.c-=r,t.s+=e,n.c+=r,t.z+=e,t.m+=e}function wi(n){for(var t,e=0,r=0,u=n.children,i=u.length;--i>=0;)t=u[i],t.z+=e,t.m+=e,e+=t.s+(r+=t.c)}function Si(n,t,e){return n.a.parent===t.parent?n.a:e}function ki(n){return 1+Zo.max(n,function(n){return n.y})}function Ei(n){return n.reduce(function(n,t){return n+t.x},0)/n.length}function Ai(n){var t=n.children;return t&&t.length?Ai(t[0]):n}function Ci(n){var t,e=n.children;return e&&(t=e.length)?Ci(e[t-1]):n}function Ni(n){return{x:n.x,y:n.y,dx:n.dx,dy:n.dy}}function zi(n,t){var e=n.x+t[3],r=n.y+t[0],u=n.dx-t[1]-t[3],i=n.dy-t[0]-t[2];return 0>u&&(e+=u/2,u=0),0>i&&(r+=i/2,i=0),{x:e,y:r,dx:u,dy:i}}function Li(n){var t=n[0],e=n[n.length-1];return e>t?[t,e]:[e,t]}function Ti(n){return n.rangeExtent?n.rangeExtent():Li(n.range())}function qi(n,t,e,r){var u=e(n[0],n[1]),i=r(t[0],t[1]);return function(n){return i(u(n))}}function Ri(n,t){var e,r=0,u=n.length-1,i=n[r],o=n[u];return i>o&&(e=r,r=u,u=e,e=i,i=o,o=e),n[r]=t.floor(i),n[u]=t.ceil(o),n}function Di(n){return n?{floor:function(t){return Math.floor(t/n)*n},ceil:function(t){return Math.ceil(t/n)*n}}:ss}function Pi(n,t,e,r){var u=[],i=[],o=0,a=Math.min(n.length,t.length)-1;for(n[a]<n[0]&&(n=n.slice().reverse(),t=t.slice().reverse());++o<=a;)u.push(e(n[o-1],n[o])),i.push(r(t[o-1],t[o]));return function(t){var e=Zo.bisect(n,t,1,a)-1;return i[e](u[e](t))}}function Ui(n,t,e,r){function u(){var u=Math.min(n.length,t.length)>2?Pi:qi,c=r?Uu:Pu;return o=u(n,t,c,e),a=u(t,n,c,hu),i}function i(n){return o(n)}var o,a;return i.invert=function(n){return a(n)},i.domain=function(t){return arguments.length?(n=t.map(Number),u()):n},i.range=function(n){return arguments.length?(t=n,u()):t},i.rangeRound=function(n){return i.range(n).interpolate(zu)},i.clamp=function(n){return arguments.length?(r=n,u()):r},i.interpolate=function(n){return arguments.length?(e=n,u()):e},i.ticks=function(t){return Oi(n,t)},i.tickFormat=function(t,e){return Yi(n,t,e)},i.nice=function(t){return Hi(n,t),u()},i.copy=function(){return Ui(n,t,e,r)},u()}function ji(n,t){return Zo.rebind(n,t,"range","rangeRound","interpolate","clamp")}function Hi(n,t){return Ri(n,Di(Fi(n,t)[2]))}function Fi(n,t){null==t&&(t=10);var e=Li(n),r=e[1]-e[0],u=Math.pow(10,Math.floor(Math.log(r/t)/Math.LN10)),i=t/r*u;return.15>=i?u*=10:.35>=i?u*=5:.75>=i&&(u*=2),e[0]=Math.ceil(e[0]/u)*u,e[1]=Math.floor(e[1]/u)*u+.5*u,e[2]=u,e}function Oi(n,t){return Zo.range.apply(Zo,Fi(n,t))}function Yi(n,t,e){var r=Fi(n,t);if(e){var u=Ga.exec(e);if(u.shift(),"s"===u[8]){var i=Zo.formatPrefix(Math.max(ua(r[0]),ua(r[1])));return u[7]||(u[7]="."+Ii(i.scale(r[2]))),u[8]="f",e=Zo.format(u.join("")),function(n){return e(i.scale(n))+i.symbol}}u[7]||(u[7]="."+Zi(u[8],r)),e=u.join("")}else e=",."+Ii(r[2])+"f";return Zo.format(e)}function Ii(n){return-Math.floor(Math.log(n)/Math.LN10+.01)}function Zi(n,t){var e=Ii(t[2]);return n in ls?Math.abs(e-Ii(Math.max(ua(t[0]),ua(t[1]))))+ +("e"!==n):e-2*("%"===n)}function Vi(n,t,e,r){function u(n){return(e?Math.log(0>n?0:n):-Math.log(n>0?0:-n))/Math.log(t)}function i(n){return e?Math.pow(t,n):-Math.pow(t,-n)}function o(t){return n(u(t))}return o.invert=function(t){return i(n.invert(t))},o.domain=function(t){return arguments.length?(e=t[0]>=0,n.domain((r=t.map(Number)).map(u)),o):r},o.base=function(e){return arguments.length?(t=+e,n.domain(r.map(u)),o):t},o.nice=function(){var t=Ri(r.map(u),e?Math:hs);return n.domain(t),r=t.map(i),o},o.ticks=function(){var n=Li(r),o=[],a=n[0],c=n[1],s=Math.floor(u(a)),l=Math.ceil(u(c)),f=t%1?2:t;if(isFinite(l-s)){if(e){for(;l>s;s++)for(var h=1;f>h;h++)o.push(i(s)*h);o.push(i(s))}else for(o.push(i(s));s++<l;)for(var h=f-1;h>0;h--)o.push(i(s)*h);for(s=0;o[s]<a;s++);for(l=o.length;o[l-1]>c;l--);o=o.slice(s,l)}return o},o.tickFormat=function(n,t){if(!arguments.length)return fs;arguments.length<2?t=fs:"function"!=typeof t&&(t=Zo.format(t));var r,a=Math.max(.1,n/o.ticks().length),c=e?(r=1e-12,Math.ceil):(r=-1e-12,Math.floor);return function(n){return n/i(c(u(n)+r))<=a?t(n):""}},o.copy=function(){return Vi(n.copy(),t,e,r)},ji(o,n)}function Xi(n,t,e){function r(t){return n(u(t))}var u=$i(t),i=$i(1/t);return r.invert=function(t){return i(n.invert(t))},r.domain=function(t){return arguments.length?(n.domain((e=t.map(Number)).map(u)),r):e},r.ticks=function(n){return Oi(e,n)},r.tickFormat=function(n,t){return Yi(e,n,t)},r.nice=function(n){return r.domain(Hi(e,n))},r.exponent=function(o){return arguments.length?(u=$i(t=o),i=$i(1/t),n.domain(e.map(u)),r):t},r.copy=function(){return Xi(n.copy(),t,e)},ji(r,n)}function $i(n){return function(t){return 0>t?-Math.pow(-t,n):Math.pow(t,n)}}function Bi(n,t){function e(e){return i[((u.get(e)||("range"===t.t?u.set(e,n.push(e)):0/0))-1)%i.length]}function r(t,e){return Zo.range(n.length).map(function(n){return t+e*n})}var u,i,a;return e.domain=function(r){if(!arguments.length)return n;n=[],u=new o;for(var i,a=-1,c=r.length;++a<c;)u.has(i=r[a])||u.set(i,n.push(i));return e[t.t].apply(e,t.a)},e.range=function(n){return arguments.length?(i=n,a=0,t={t:"range",a:arguments},e):i},e.rangePoints=function(u,o){arguments.length<2&&(o=0);var c=u[0],s=u[1],l=(s-c)/(Math.max(1,n.length-1)+o);return i=r(n.length<2?(c+s)/2:c+l*o/2,l),a=0,t={t:"rangePoints",a:arguments},e},e.rangeBands=function(u,o,c){arguments.length<2&&(o=0),arguments.length<3&&(c=o);var s=u[1]<u[0],l=u[s-0],f=u[1-s],h=(f-l)/(n.length-o+2*c);return i=r(l+h*c,h),s&&i.reverse(),a=h*(1-o),t={t:"rangeBands",a:arguments},e},e.rangeRoundBands=function(u,o,c){arguments.length<2&&(o=0),arguments.length<3&&(c=o);var s=u[1]<u[0],l=u[s-0],f=u[1-s],h=Math.floor((f-l)/(n.length-o+2*c)),g=f-l-(n.length-o)*h;return i=r(l+Math.round(g/2),h),s&&i.reverse(),a=Math.round(h*(1-o)),t={t:"rangeRoundBands",a:arguments},e},e.rangeBand=function(){return a},e.rangeExtent=function(){return Li(t.a[0])},e.copy=function(){return Bi(n,t)},e.domain(n)}function Wi(e,r){function u(){var n=0,t=r.length;for(o=[];++n<t;)o[n-1]=Zo.quantile(e,n/t);return i}function i(n){return isNaN(n=+n)?void 0:r[Zo.bisect(o,n)]}var o;return i.domain=function(r){return arguments.length?(e=r.filter(t).sort(n),u()):e},i.range=function(n){return arguments.length?(r=n,u()):r},i.quantiles=function(){return o},i.invertExtent=function(n){return n=r.indexOf(n),0>n?[0/0,0/0]:[n>0?o[n-1]:e[0],n<o.length?o[n]:e[e.length-1]]},i.copy=function(){return Wi(e,r)},u()}function Ji(n,t,e){function r(t){return e[Math.max(0,Math.min(o,Math.floor(i*(t-n))))]}function u(){return i=e.length/(t-n),o=e.length-1,r}var i,o;return r.domain=function(e){return arguments.length?(n=+e[0],t=+e[e.length-1],u()):[n,t]},r.range=function(n){return arguments.length?(e=n,u()):e},r.invertExtent=function(t){return t=e.indexOf(t),t=0>t?0/0:t/i+n,[t,t+1/i]},r.copy=function(){return Ji(n,t,e)},u()}function Gi(n,t){function e(e){return e>=e?t[Zo.bisect(n,e)]:void 0}return e.domain=function(t){return arguments.length?(n=t,e):n},e.range=function(n){return arguments.length?(t=n,e):t},e.invertExtent=function(e){return e=t.indexOf(e),[n[e-1],n[e]]},e.copy=function(){return Gi(n,t)},e}function Ki(n){function t(n){return+n}return t.invert=t,t.domain=t.range=function(e){return arguments.length?(n=e.map(t),t):n},t.ticks=function(t){return Oi(n,t)},t.tickFormat=function(t,e){return Yi(n,t,e)},t.copy=function(){return Ki(n)},t}function Qi(n){return n.innerRadius}function no(n){return n.outerRadius}function to(n){return n.startAngle}function eo(n){return n.endAngle}function ro(n){function t(t){function o(){s.push("M",i(n(l),a))}for(var c,s=[],l=[],f=-1,h=t.length,g=bt(e),p=bt(r);++f<h;)u.call(this,c=t[f],f)?l.push([+g.call(this,c,f),+p.call(this,c,f)]):l.length&&(o(),l=[]);return l.length&&o(),s.length?s.join(""):null}var e=wr,r=Sr,u=we,i=uo,o=i.key,a=.7;return t.x=function(n){return arguments.length?(e=n,t):e},t.y=function(n){return arguments.length?(r=n,t):r},t.defined=function(n){return arguments.length?(u=n,t):u},t.interpolate=function(n){return arguments.length?(o="function"==typeof n?i=n:(i=xs.get(n)||uo).key,t):o},t.tension=function(n){return arguments.length?(a=n,t):a},t}function uo(n){return n.join("L")}function io(n){return uo(n)+"Z"}function oo(n){for(var t=0,e=n.length,r=n[0],u=[r[0],",",r[1]];++t<e;)u.push("H",(r[0]+(r=n[t])[0])/2,"V",r[1]);return e>1&&u.push("H",r[0]),u.join("")}function ao(n){for(var t=0,e=n.length,r=n[0],u=[r[0],",",r[1]];++t<e;)u.push("V",(r=n[t])[1],"H",r[0]);return u.join("")}function co(n){for(var t=0,e=n.length,r=n[0],u=[r[0],",",r[1]];++t<e;)u.push("H",(r=n[t])[0],"V",r[1]);return u.join("")}function so(n,t){return n.length<4?uo(n):n[1]+ho(n.slice(1,n.length-1),go(n,t))}function lo(n,t){return n.length<3?uo(n):n[0]+ho((n.push(n[0]),n),go([n[n.length-2]].concat(n,[n[1]]),t))}function fo(n,t){return n.length<3?uo(n):n[0]+ho(n,go(n,t))}function ho(n,t){if(t.length<1||n.length!=t.length&&n.length!=t.length+2)return uo(n);var e=n.length!=t.length,r="",u=n[0],i=n[1],o=t[0],a=o,c=1;if(e&&(r+="Q"+(i[0]-2*o[0]/3)+","+(i[1]-2*o[1]/3)+","+i[0]+","+i[1],u=n[1],c=2),t.length>1){a=t[1],i=n[c],c++,r+="C"+(u[0]+o[0])+","+(u[1]+o[1])+","+(i[0]-a[0])+","+(i[1]-a[1])+","+i[0]+","+i[1];for(var s=2;s<t.length;s++,c++)i=n[c],a=t[s],r+="S"+(i[0]-a[0])+","+(i[1]-a[1])+","+i[0]+","+i[1]}if(e){var l=n[c];r+="Q"+(i[0]+2*a[0]/3)+","+(i[1]+2*a[1]/3)+","+l[0]+","+l[1]}return r}function go(n,t){for(var e,r=[],u=(1-t)/2,i=n[0],o=n[1],a=1,c=n.length;++a<c;)e=i,i=o,o=n[a],r.push([u*(o[0]-e[0]),u*(o[1]-e[1])]);return r}function po(n){if(n.length<3)return uo(n);var t=1,e=n.length,r=n[0],u=r[0],i=r[1],o=[u,u,u,(r=n[1])[0]],a=[i,i,i,r[1]],c=[u,",",i,"L",xo(bs,o),",",xo(bs,a)];for(n.push(n[e-1]);++t<=e;)r=n[t],o.shift(),o.push(r[0]),a.shift(),a.push(r[1]),Mo(c,o,a);return n.pop(),c.push("L",r),c.join("")}function vo(n){if(n.length<4)return uo(n);for(var t,e=[],r=-1,u=n.length,i=[0],o=[0];++r<3;)t=n[r],i.push(t[0]),o.push(t[1]);for(e.push(xo(bs,i)+","+xo(bs,o)),--r;++r<u;)t=n[r],i.shift(),i.push(t[0]),o.shift(),o.push(t[1]),Mo(e,i,o);return e.join("")}function mo(n){for(var t,e,r=-1,u=n.length,i=u+4,o=[],a=[];++r<4;)e=n[r%u],o.push(e[0]),a.push(e[1]);for(t=[xo(bs,o),",",xo(bs,a)],--r;++r<i;)e=n[r%u],o.shift(),o.push(e[0]),a.shift(),a.push(e[1]),Mo(t,o,a);return t.join("")}function yo(n,t){var e=n.length-1;if(e)for(var r,u,i=n[0][0],o=n[0][1],a=n[e][0]-i,c=n[e][1]-o,s=-1;++s<=e;)r=n[s],u=s/e,r[0]=t*r[0]+(1-t)*(i+u*a),r[1]=t*r[1]+(1-t)*(o+u*c);return po(n)}function xo(n,t){return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]+n[3]*t[3]}function Mo(n,t,e){n.push("C",xo(Ms,t),",",xo(Ms,e),",",xo(_s,t),",",xo(_s,e),",",xo(bs,t),",",xo(bs,e))}function _o(n,t){return(t[1]-n[1])/(t[0]-n[0])}function bo(n){for(var t=0,e=n.length-1,r=[],u=n[0],i=n[1],o=r[0]=_o(u,i);++t<e;)r[t]=(o+(o=_o(u=i,i=n[t+1])))/2;return r[t]=o,r}function wo(n){for(var t,e,r,u,i=[],o=bo(n),a=-1,c=n.length-1;++a<c;)t=_o(n[a],n[a+1]),ua(t)<ka?o[a]=o[a+1]=0:(e=o[a]/t,r=o[a+1]/t,u=e*e+r*r,u>9&&(u=3*t/Math.sqrt(u),o[a]=u*e,o[a+1]=u*r));for(a=-1;++a<=c;)u=(n[Math.min(c,a+1)][0]-n[Math.max(0,a-1)][0])/(6*(1+o[a]*o[a])),i.push([u||0,o[a]*u||0]);return i}function So(n){return n.length<3?uo(n):n[0]+ho(n,wo(n))}function ko(n){for(var t,e,r,u=-1,i=n.length;++u<i;)t=n[u],e=t[0],r=t[1]+ms,t[0]=e*Math.cos(r),t[1]=e*Math.sin(r);return n}function Eo(n){function t(t){function c(){v.push("M",a(n(m),f),l,s(n(d.reverse()),f),"Z")}for(var h,g,p,v=[],d=[],m=[],y=-1,x=t.length,M=bt(e),_=bt(u),b=e===r?function(){return g}:bt(r),w=u===i?function(){return p}:bt(i);++y<x;)o.call(this,h=t[y],y)?(d.push([g=+M.call(this,h,y),p=+_.call(this,h,y)]),m.push([+b.call(this,h,y),+w.call(this,h,y)])):d.length&&(c(),d=[],m=[]);return d.length&&c(),v.length?v.join(""):null}var e=wr,r=wr,u=0,i=Sr,o=we,a=uo,c=a.key,s=a,l="L",f=.7;return t.x=function(n){return arguments.length?(e=r=n,t):r},t.x0=function(n){return arguments.length?(e=n,t):e},t.x1=function(n){return arguments.length?(r=n,t):r},t.y=function(n){return arguments.length?(u=i=n,t):i},t.y0=function(n){return arguments.length?(u=n,t):u},t.y1=function(n){return arguments.length?(i=n,t):i},t.defined=function(n){return arguments.length?(o=n,t):o},t.interpolate=function(n){return arguments.length?(c="function"==typeof n?a=n:(a=xs.get(n)||uo).key,s=a.reverse||a,l=a.closed?"M":"L",t):c},t.tension=function(n){return arguments.length?(f=n,t):f},t}function Ao(n){return n.radius}function Co(n){return[n.x,n.y]}function No(n){return function(){var t=n.apply(this,arguments),e=t[0],r=t[1]+ms;return[e*Math.cos(r),e*Math.sin(r)]}}function zo(){return 64}function Lo(){return"circle"}function To(n){var t=Math.sqrt(n/ba);return"M0,"+t+"A"+t+","+t+" 0 1,1 0,"+-t+"A"+t+","+t+" 0 1,1 0,"+t+"Z"}function qo(n,t){return sa(n,Cs),n.id=t,n}function Ro(n,t,e,r){var u=n.id;return P(n,"function"==typeof e?function(n,i,o){n.__transition__[u].tween.set(t,r(e.call(n,n.__data__,i,o)))}:(e=r(e),function(n){n.__transition__[u].tween.set(t,e)}))}function Do(n){return null==n&&(n=""),function(){this.textContent=n}}function Po(n,t,e,r){var u=n.__transition__||(n.__transition__={active:0,count:0}),i=u[e];if(!i){var a=r.time;i=u[e]={tween:new o,time:a,ease:r.ease,delay:r.delay,duration:r.duration},++u.count,Zo.timer(function(r){function o(r){return u.active>e?s():(u.active=e,i.event&&i.event.start.call(n,l,t),i.tween.forEach(function(e,r){(r=r.call(n,l,t))&&v.push(r)}),Zo.timer(function(){return p.c=c(r||1)?we:c,1},0,a),void 0)}function c(r){if(u.active!==e)return s();for(var o=r/g,a=f(o),c=v.length;c>0;)v[--c].call(n,a); +return o>=1?(i.event&&i.event.end.call(n,l,t),s()):void 0}function s(){return--u.count?delete u[e]:delete n.__transition__,1}var l=n.__data__,f=i.ease,h=i.delay,g=i.duration,p=Ba,v=[];return p.t=h+a,r>=h?o(r-h):(p.c=o,void 0)},0,a)}}function Uo(n,t){n.attr("transform",function(n){return"translate("+t(n)+",0)"})}function jo(n,t){n.attr("transform",function(n){return"translate(0,"+t(n)+")"})}function Ho(n){return n.toISOString()}function Fo(n,t,e){function r(t){return n(t)}function u(n,e){var r=n[1]-n[0],u=r/e,i=Zo.bisect(Us,u);return i==Us.length?[t.year,Fi(n.map(function(n){return n/31536e6}),e)[2]]:i?t[u/Us[i-1]<Us[i]/u?i-1:i]:[Fs,Fi(n,e)[2]]}return r.invert=function(t){return Oo(n.invert(t))},r.domain=function(t){return arguments.length?(n.domain(t),r):n.domain().map(Oo)},r.nice=function(n,t){function e(e){return!isNaN(e)&&!n.range(e,Oo(+e+1),t).length}var i=r.domain(),o=Li(i),a=null==n?u(o,10):"number"==typeof n&&u(o,n);return a&&(n=a[0],t=a[1]),r.domain(Ri(i,t>1?{floor:function(t){for(;e(t=n.floor(t));)t=Oo(t-1);return t},ceil:function(t){for(;e(t=n.ceil(t));)t=Oo(+t+1);return t}}:n))},r.ticks=function(n,t){var e=Li(r.domain()),i=null==n?u(e,10):"number"==typeof n?u(e,n):!n.range&&[{range:n},t];return i&&(n=i[0],t=i[1]),n.range(e[0],Oo(+e[1]+1),1>t?1:t)},r.tickFormat=function(){return e},r.copy=function(){return Fo(n.copy(),t,e)},ji(r,n)}function Oo(n){return new Date(n)}function Yo(n){return JSON.parse(n.responseText)}function Io(n){var t=$o.createRange();return t.selectNode($o.body),t.createContextualFragment(n.responseText)}var Zo={version:"3.4.11"};Date.now||(Date.now=function(){return+new Date});var Vo=[].slice,Xo=function(n){return Vo.call(n)},$o=document,Bo=$o.documentElement,Wo=window;try{Xo(Bo.childNodes)[0].nodeType}catch(Jo){Xo=function(n){for(var t=n.length,e=new Array(t);t--;)e[t]=n[t];return e}}try{$o.createElement("div").style.setProperty("opacity",0,"")}catch(Go){var Ko=Wo.Element.prototype,Qo=Ko.setAttribute,na=Ko.setAttributeNS,ta=Wo.CSSStyleDeclaration.prototype,ea=ta.setProperty;Ko.setAttribute=function(n,t){Qo.call(this,n,t+"")},Ko.setAttributeNS=function(n,t,e){na.call(this,n,t,e+"")},ta.setProperty=function(n,t,e){ea.call(this,n,t+"",e)}}Zo.ascending=n,Zo.descending=function(n,t){return n>t?-1:t>n?1:t>=n?0:0/0},Zo.min=function(n,t){var e,r,u=-1,i=n.length;if(1===arguments.length){for(;++u<i&&!(null!=(e=n[u])&&e>=e);)e=void 0;for(;++u<i;)null!=(r=n[u])&&e>r&&(e=r)}else{for(;++u<i&&!(null!=(e=t.call(n,n[u],u))&&e>=e);)e=void 0;for(;++u<i;)null!=(r=t.call(n,n[u],u))&&e>r&&(e=r)}return e},Zo.max=function(n,t){var e,r,u=-1,i=n.length;if(1===arguments.length){for(;++u<i&&!(null!=(e=n[u])&&e>=e);)e=void 0;for(;++u<i;)null!=(r=n[u])&&r>e&&(e=r)}else{for(;++u<i&&!(null!=(e=t.call(n,n[u],u))&&e>=e);)e=void 0;for(;++u<i;)null!=(r=t.call(n,n[u],u))&&r>e&&(e=r)}return e},Zo.extent=function(n,t){var e,r,u,i=-1,o=n.length;if(1===arguments.length){for(;++i<o&&!(null!=(e=u=n[i])&&e>=e);)e=u=void 0;for(;++i<o;)null!=(r=n[i])&&(e>r&&(e=r),r>u&&(u=r))}else{for(;++i<o&&!(null!=(e=u=t.call(n,n[i],i))&&e>=e);)e=void 0;for(;++i<o;)null!=(r=t.call(n,n[i],i))&&(e>r&&(e=r),r>u&&(u=r))}return[e,u]},Zo.sum=function(n,t){var e,r=0,u=n.length,i=-1;if(1===arguments.length)for(;++i<u;)isNaN(e=+n[i])||(r+=e);else for(;++i<u;)isNaN(e=+t.call(n,n[i],i))||(r+=e);return r},Zo.mean=function(n,e){var r,u=0,i=n.length,o=-1,a=i;if(1===arguments.length)for(;++o<i;)t(r=n[o])?u+=r:--a;else for(;++o<i;)t(r=e.call(n,n[o],o))?u+=r:--a;return a?u/a:void 0},Zo.quantile=function(n,t){var e=(n.length-1)*t+1,r=Math.floor(e),u=+n[r-1],i=e-r;return i?u+i*(n[r]-u):u},Zo.median=function(e,r){return arguments.length>1&&(e=e.map(r)),e=e.filter(t),e.length?Zo.quantile(e.sort(n),.5):void 0};var ra=e(n);Zo.bisectLeft=ra.left,Zo.bisect=Zo.bisectRight=ra.right,Zo.bisector=function(t){return e(1===t.length?function(e,r){return n(t(e),r)}:t)},Zo.shuffle=function(n){for(var t,e,r=n.length;r;)e=0|Math.random()*r--,t=n[r],n[r]=n[e],n[e]=t;return n},Zo.permute=function(n,t){for(var e=t.length,r=new Array(e);e--;)r[e]=n[t[e]];return r},Zo.pairs=function(n){for(var t,e=0,r=n.length-1,u=n[0],i=new Array(0>r?0:r);r>e;)i[e]=[t=u,u=n[++e]];return i},Zo.zip=function(){if(!(u=arguments.length))return[];for(var n=-1,t=Zo.min(arguments,r),e=new Array(t);++n<t;)for(var u,i=-1,o=e[n]=new Array(u);++i<u;)o[i]=arguments[i][n];return e},Zo.transpose=function(n){return Zo.zip.apply(Zo,n)},Zo.keys=function(n){var t=[];for(var e in n)t.push(e);return t},Zo.values=function(n){var t=[];for(var e in n)t.push(n[e]);return t},Zo.entries=function(n){var t=[];for(var e in n)t.push({key:e,value:n[e]});return t},Zo.merge=function(n){for(var t,e,r,u=n.length,i=-1,o=0;++i<u;)o+=n[i].length;for(e=new Array(o);--u>=0;)for(r=n[u],t=r.length;--t>=0;)e[--o]=r[t];return e};var ua=Math.abs;Zo.range=function(n,t,e){if(arguments.length<3&&(e=1,arguments.length<2&&(t=n,n=0)),1/0===(t-n)/e)throw new Error("infinite range");var r,i=[],o=u(ua(e)),a=-1;if(n*=o,t*=o,e*=o,0>e)for(;(r=n+e*++a)>t;)i.push(r/o);else for(;(r=n+e*++a)<t;)i.push(r/o);return i},Zo.map=function(n){var t=new o;if(n instanceof o)n.forEach(function(n,e){t.set(n,e)});else for(var e in n)t.set(e,n[e]);return t},i(o,{has:a,get:function(n){return this[ia+n]},set:function(n,t){return this[ia+n]=t},remove:c,keys:s,values:function(){var n=[];return this.forEach(function(t,e){n.push(e)}),n},entries:function(){var n=[];return this.forEach(function(t,e){n.push({key:t,value:e})}),n},size:l,empty:f,forEach:function(n){for(var t in this)t.charCodeAt(0)===oa&&n.call(this,t.substring(1),this[t])}});var ia="\x00",oa=ia.charCodeAt(0);Zo.nest=function(){function n(t,a,c){if(c>=i.length)return r?r.call(u,a):e?a.sort(e):a;for(var s,l,f,h,g=-1,p=a.length,v=i[c++],d=new o;++g<p;)(h=d.get(s=v(l=a[g])))?h.push(l):d.set(s,[l]);return t?(l=t(),f=function(e,r){l.set(e,n(t,r,c))}):(l={},f=function(e,r){l[e]=n(t,r,c)}),d.forEach(f),l}function t(n,e){if(e>=i.length)return n;var r=[],u=a[e++];return n.forEach(function(n,u){r.push({key:n,values:t(u,e)})}),u?r.sort(function(n,t){return u(n.key,t.key)}):r}var e,r,u={},i=[],a=[];return u.map=function(t,e){return n(e,t,0)},u.entries=function(e){return t(n(Zo.map,e,0),0)},u.key=function(n){return i.push(n),u},u.sortKeys=function(n){return a[i.length-1]=n,u},u.sortValues=function(n){return e=n,u},u.rollup=function(n){return r=n,u},u},Zo.set=function(n){var t=new h;if(n)for(var e=0,r=n.length;r>e;++e)t.add(n[e]);return t},i(h,{has:a,add:function(n){return this[ia+n]=!0,n},remove:function(n){return n=ia+n,n in this&&delete this[n]},values:s,size:l,empty:f,forEach:function(n){for(var t in this)t.charCodeAt(0)===oa&&n.call(this,t.substring(1))}}),Zo.behavior={},Zo.rebind=function(n,t){for(var e,r=1,u=arguments.length;++r<u;)n[e=arguments[r]]=g(n,t,t[e]);return n};var aa=["webkit","ms","moz","Moz","o","O"];Zo.dispatch=function(){for(var n=new d,t=-1,e=arguments.length;++t<e;)n[arguments[t]]=m(n);return n},d.prototype.on=function(n,t){var e=n.indexOf("."),r="";if(e>=0&&(r=n.substring(e+1),n=n.substring(0,e)),n)return arguments.length<2?this[n].on(r):this[n].on(r,t);if(2===arguments.length){if(null==t)for(n in this)this.hasOwnProperty(n)&&this[n].on(r,null);return this}},Zo.event=null,Zo.requote=function(n){return n.replace(ca,"\\$&")};var ca=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g,sa={}.__proto__?function(n,t){n.__proto__=t}:function(n,t){for(var e in t)n[e]=t[e]},la=function(n,t){return t.querySelector(n)},fa=function(n,t){return t.querySelectorAll(n)},ha=Bo.matches||Bo[p(Bo,"matchesSelector")],ga=function(n,t){return ha.call(n,t)};"function"==typeof Sizzle&&(la=function(n,t){return Sizzle(n,t)[0]||null},fa=Sizzle,ga=Sizzle.matchesSelector),Zo.selection=function(){return ma};var pa=Zo.selection.prototype=[];pa.select=function(n){var t,e,r,u,i=[];n=b(n);for(var o=-1,a=this.length;++o<a;){i.push(t=[]),t.parentNode=(r=this[o]).parentNode;for(var c=-1,s=r.length;++c<s;)(u=r[c])?(t.push(e=n.call(u,u.__data__,c,o)),e&&"__data__"in u&&(e.__data__=u.__data__)):t.push(null)}return _(i)},pa.selectAll=function(n){var t,e,r=[];n=w(n);for(var u=-1,i=this.length;++u<i;)for(var o=this[u],a=-1,c=o.length;++a<c;)(e=o[a])&&(r.push(t=Xo(n.call(e,e.__data__,a,u))),t.parentNode=e);return _(r)};var va={svg:"http://www.w3.org/2000/svg",xhtml:"http://www.w3.org/1999/xhtml",xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"};Zo.ns={prefix:va,qualify:function(n){var t=n.indexOf(":"),e=n;return t>=0&&(e=n.substring(0,t),n=n.substring(t+1)),va.hasOwnProperty(e)?{space:va[e],local:n}:n}},pa.attr=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node();return n=Zo.ns.qualify(n),n.local?e.getAttributeNS(n.space,n.local):e.getAttribute(n)}for(t in n)this.each(S(t,n[t]));return this}return this.each(S(n,t))},pa.classed=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node(),r=(n=A(n)).length,u=-1;if(t=e.classList){for(;++u<r;)if(!t.contains(n[u]))return!1}else for(t=e.getAttribute("class");++u<r;)if(!E(n[u]).test(t))return!1;return!0}for(t in n)this.each(C(t,n[t]));return this}return this.each(C(n,t))},pa.style=function(n,t,e){var r=arguments.length;if(3>r){if("string"!=typeof n){2>r&&(t="");for(e in n)this.each(z(e,n[e],t));return this}if(2>r)return Wo.getComputedStyle(this.node(),null).getPropertyValue(n);e=""}return this.each(z(n,t,e))},pa.property=function(n,t){if(arguments.length<2){if("string"==typeof n)return this.node()[n];for(t in n)this.each(L(t,n[t]));return this}return this.each(L(n,t))},pa.text=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.textContent=null==t?"":t}:null==n?function(){this.textContent=""}:function(){this.textContent=n}):this.node().textContent},pa.html=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.innerHTML=null==t?"":t}:null==n?function(){this.innerHTML=""}:function(){this.innerHTML=n}):this.node().innerHTML},pa.append=function(n){return n=T(n),this.select(function(){return this.appendChild(n.apply(this,arguments))})},pa.insert=function(n,t){return n=T(n),t=b(t),this.select(function(){return this.insertBefore(n.apply(this,arguments),t.apply(this,arguments)||null)})},pa.remove=function(){return this.each(function(){var n=this.parentNode;n&&n.removeChild(this)})},pa.data=function(n,t){function e(n,e){var r,u,i,a=n.length,f=e.length,h=Math.min(a,f),g=new Array(f),p=new Array(f),v=new Array(a);if(t){var d,m=new o,y=new o,x=[];for(r=-1;++r<a;)d=t.call(u=n[r],u.__data__,r),m.has(d)?v[r]=u:m.set(d,u),x.push(d);for(r=-1;++r<f;)d=t.call(e,i=e[r],r),(u=m.get(d))?(g[r]=u,u.__data__=i):y.has(d)||(p[r]=q(i)),y.set(d,i),m.remove(d);for(r=-1;++r<a;)m.has(x[r])&&(v[r]=n[r])}else{for(r=-1;++r<h;)u=n[r],i=e[r],u?(u.__data__=i,g[r]=u):p[r]=q(i);for(;f>r;++r)p[r]=q(e[r]);for(;a>r;++r)v[r]=n[r]}p.update=g,p.parentNode=g.parentNode=v.parentNode=n.parentNode,c.push(p),s.push(g),l.push(v)}var r,u,i=-1,a=this.length;if(!arguments.length){for(n=new Array(a=(r=this[0]).length);++i<a;)(u=r[i])&&(n[i]=u.__data__);return n}var c=U([]),s=_([]),l=_([]);if("function"==typeof n)for(;++i<a;)e(r=this[i],n.call(r,r.parentNode.__data__,i));else for(;++i<a;)e(r=this[i],n);return s.enter=function(){return c},s.exit=function(){return l},s},pa.datum=function(n){return arguments.length?this.property("__data__",n):this.property("__data__")},pa.filter=function(n){var t,e,r,u=[];"function"!=typeof n&&(n=R(n));for(var i=0,o=this.length;o>i;i++){u.push(t=[]),t.parentNode=(e=this[i]).parentNode;for(var a=0,c=e.length;c>a;a++)(r=e[a])&&n.call(r,r.__data__,a,i)&&t.push(r)}return _(u)},pa.order=function(){for(var n=-1,t=this.length;++n<t;)for(var e,r=this[n],u=r.length-1,i=r[u];--u>=0;)(e=r[u])&&(i&&i!==e.nextSibling&&i.parentNode.insertBefore(e,i),i=e);return this},pa.sort=function(n){n=D.apply(this,arguments);for(var t=-1,e=this.length;++t<e;)this[t].sort(n);return this.order()},pa.each=function(n){return P(this,function(t,e,r){n.call(t,t.__data__,e,r)})},pa.call=function(n){var t=Xo(arguments);return n.apply(t[0]=this,t),this},pa.empty=function(){return!this.node()},pa.node=function(){for(var n=0,t=this.length;t>n;n++)for(var e=this[n],r=0,u=e.length;u>r;r++){var i=e[r];if(i)return i}return null},pa.size=function(){var n=0;return this.each(function(){++n}),n};var da=[];Zo.selection.enter=U,Zo.selection.enter.prototype=da,da.append=pa.append,da.empty=pa.empty,da.node=pa.node,da.call=pa.call,da.size=pa.size,da.select=function(n){for(var t,e,r,u,i,o=[],a=-1,c=this.length;++a<c;){r=(u=this[a]).update,o.push(t=[]),t.parentNode=u.parentNode;for(var s=-1,l=u.length;++s<l;)(i=u[s])?(t.push(r[s]=e=n.call(u.parentNode,i.__data__,s,a)),e.__data__=i.__data__):t.push(null)}return _(o)},da.insert=function(n,t){return arguments.length<2&&(t=j(this)),pa.insert.call(this,n,t)},pa.transition=function(){for(var n,t,e=Ss||++Ns,r=[],u=ks||{time:Date.now(),ease:xu,delay:0,duration:250},i=-1,o=this.length;++i<o;){r.push(n=[]);for(var a=this[i],c=-1,s=a.length;++c<s;)(t=a[c])&&Po(t,c,e,u),n.push(t)}return qo(r,e)},pa.interrupt=function(){return this.each(H)},Zo.select=function(n){var t=["string"==typeof n?la(n,$o):n];return t.parentNode=Bo,_([t])},Zo.selectAll=function(n){var t=Xo("string"==typeof n?fa(n,$o):n);return t.parentNode=Bo,_([t])};var ma=Zo.select(Bo);pa.on=function(n,t,e){var r=arguments.length;if(3>r){if("string"!=typeof n){2>r&&(t=!1);for(e in n)this.each(F(e,n[e],t));return this}if(2>r)return(r=this.node()["__on"+n])&&r._;e=!1}return this.each(F(n,t,e))};var ya=Zo.map({mouseenter:"mouseover",mouseleave:"mouseout"});ya.forEach(function(n){"on"+n in $o&&ya.remove(n)});var xa="onselectstart"in $o?null:p(Bo.style,"userSelect"),Ma=0;Zo.mouse=function(n){return Z(n,x())};var _a=/WebKit/.test(Wo.navigator.userAgent)?-1:0;Zo.touches=function(n,t){return arguments.length<2&&(t=x().touches),t?Xo(t).map(function(t){var e=Z(n,t);return e.identifier=t.identifier,e}):[]},Zo.behavior.drag=function(){function n(){this.on("mousedown.drag",u).on("touchstart.drag",i)}function t(n,t,u,i,o){return function(){function a(){var n,e,r=t(h,v);r&&(n=r[0]-x[0],e=r[1]-x[1],p|=n|e,x=r,g({type:"drag",x:r[0]+s[0],y:r[1]+s[1],dx:n,dy:e}))}function c(){t(h,v)&&(m.on(i+d,null).on(o+d,null),y(p&&Zo.event.target===f),g({type:"dragend"}))}var s,l=this,f=Zo.event.target,h=l.parentNode,g=e.of(l,arguments),p=0,v=n(),d=".drag"+(null==v?"":"-"+v),m=Zo.select(u()).on(i+d,a).on(o+d,c),y=I(),x=t(h,v);r?(s=r.apply(l,arguments),s=[s.x-x[0],s.y-x[1]]):s=[0,0],g({type:"dragstart"})}}var e=M(n,"drag","dragstart","dragend"),r=null,u=t(v,Zo.mouse,$,"mousemove","mouseup"),i=t(V,Zo.touch,X,"touchmove","touchend");return n.origin=function(t){return arguments.length?(r=t,n):r},Zo.rebind(n,e,"on")};var ba=Math.PI,wa=2*ba,Sa=ba/2,ka=1e-6,Ea=ka*ka,Aa=ba/180,Ca=180/ba,Na=Math.SQRT2,za=2,La=4;Zo.interpolateZoom=function(n,t){function e(n){var t=n*y;if(m){var e=Q(v),o=i/(za*h)*(e*nt(Na*t+v)-K(v));return[r+o*s,u+o*l,i*e/Q(Na*t+v)]}return[r+n*s,u+n*l,i*Math.exp(Na*t)]}var r=n[0],u=n[1],i=n[2],o=t[0],a=t[1],c=t[2],s=o-r,l=a-u,f=s*s+l*l,h=Math.sqrt(f),g=(c*c-i*i+La*f)/(2*i*za*h),p=(c*c-i*i-La*f)/(2*c*za*h),v=Math.log(Math.sqrt(g*g+1)-g),d=Math.log(Math.sqrt(p*p+1)-p),m=d-v,y=(m||Math.log(c/i))/Na;return e.duration=1e3*y,e},Zo.behavior.zoom=function(){function n(n){n.on(A,s).on(Ra+".zoom",f).on("dblclick.zoom",h).on(z,l)}function t(n){return[(n[0]-S.x)/S.k,(n[1]-S.y)/S.k]}function e(n){return[n[0]*S.k+S.x,n[1]*S.k+S.y]}function r(n){S.k=Math.max(E[0],Math.min(E[1],n))}function u(n,t){t=e(t),S.x+=n[0]-t[0],S.y+=n[1]-t[1]}function i(){_&&_.domain(x.range().map(function(n){return(n-S.x)/S.k}).map(x.invert)),w&&w.domain(b.range().map(function(n){return(n-S.y)/S.k}).map(b.invert))}function o(n){n({type:"zoomstart"})}function a(n){i(),n({type:"zoom",scale:S.k,translate:[S.x,S.y]})}function c(n){n({type:"zoomend"})}function s(){function n(){l=1,u(Zo.mouse(r),h),a(s)}function e(){f.on(C,null).on(N,null),g(l&&Zo.event.target===i),c(s)}var r=this,i=Zo.event.target,s=L.of(r,arguments),l=0,f=Zo.select(Wo).on(C,n).on(N,e),h=t(Zo.mouse(r)),g=I();H.call(r),o(s)}function l(){function n(){var n=Zo.touches(g);return h=S.k,n.forEach(function(n){n.identifier in v&&(v[n.identifier]=t(n))}),n}function e(){var t=Zo.event.target;Zo.select(t).on(M,i).on(_,f),b.push(t);for(var e=Zo.event.changedTouches,o=0,c=e.length;c>o;++o)v[e[o].identifier]=null;var s=n(),l=Date.now();if(1===s.length){if(500>l-m){var h=s[0],g=v[h.identifier];r(2*S.k),u(h,g),y(),a(p)}m=l}else if(s.length>1){var h=s[0],x=s[1],w=h[0]-x[0],k=h[1]-x[1];d=w*w+k*k}}function i(){for(var n,t,e,i,o=Zo.touches(g),c=0,s=o.length;s>c;++c,i=null)if(e=o[c],i=v[e.identifier]){if(t)break;n=e,t=i}if(i){var l=(l=e[0]-n[0])*l+(l=e[1]-n[1])*l,f=d&&Math.sqrt(l/d);n=[(n[0]+e[0])/2,(n[1]+e[1])/2],t=[(t[0]+i[0])/2,(t[1]+i[1])/2],r(f*h)}m=null,u(n,t),a(p)}function f(){if(Zo.event.touches.length){for(var t=Zo.event.changedTouches,e=0,r=t.length;r>e;++e)delete v[t[e].identifier];for(var u in v)return void n()}Zo.selectAll(b).on(x,null),w.on(A,s).on(z,l),k(),c(p)}var h,g=this,p=L.of(g,arguments),v={},d=0,x=".zoom-"+Zo.event.changedTouches[0].identifier,M="touchmove"+x,_="touchend"+x,b=[],w=Zo.select(g).on(A,null).on(z,e),k=I();H.call(g),e(),o(p)}function f(){var n=L.of(this,arguments);d?clearTimeout(d):(g=t(p=v||Zo.mouse(this)),H.call(this),o(n)),d=setTimeout(function(){d=null,c(n)},50),y(),r(Math.pow(2,.002*Ta())*S.k),u(p,g),a(n)}function h(){var n=L.of(this,arguments),e=Zo.mouse(this),i=t(e),s=Math.log(S.k)/Math.LN2;o(n),r(Math.pow(2,Zo.event.shiftKey?Math.ceil(s)-1:Math.floor(s)+1)),u(e,i),a(n),c(n)}var g,p,v,d,m,x,_,b,w,S={x:0,y:0,k:1},k=[960,500],E=qa,A="mousedown.zoom",C="mousemove.zoom",N="mouseup.zoom",z="touchstart.zoom",L=M(n,"zoomstart","zoom","zoomend");return n.event=function(n){n.each(function(){var n=L.of(this,arguments),t=S;Ss?Zo.select(this).transition().each("start.zoom",function(){S=this.__chart__||{x:0,y:0,k:1},o(n)}).tween("zoom:zoom",function(){var e=k[0],r=k[1],u=e/2,i=r/2,o=Zo.interpolateZoom([(u-S.x)/S.k,(i-S.y)/S.k,e/S.k],[(u-t.x)/t.k,(i-t.y)/t.k,e/t.k]);return function(t){var r=o(t),c=e/r[2];this.__chart__=S={x:u-r[0]*c,y:i-r[1]*c,k:c},a(n)}}).each("end.zoom",function(){c(n)}):(this.__chart__=S,o(n),a(n),c(n))})},n.translate=function(t){return arguments.length?(S={x:+t[0],y:+t[1],k:S.k},i(),n):[S.x,S.y]},n.scale=function(t){return arguments.length?(S={x:S.x,y:S.y,k:+t},i(),n):S.k},n.scaleExtent=function(t){return arguments.length?(E=null==t?qa:[+t[0],+t[1]],n):E},n.center=function(t){return arguments.length?(v=t&&[+t[0],+t[1]],n):v},n.size=function(t){return arguments.length?(k=t&&[+t[0],+t[1]],n):k},n.x=function(t){return arguments.length?(_=t,x=t.copy(),S={x:0,y:0,k:1},n):_},n.y=function(t){return arguments.length?(w=t,b=t.copy(),S={x:0,y:0,k:1},n):w},Zo.rebind(n,L,"on")};var Ta,qa=[0,1/0],Ra="onwheel"in $o?(Ta=function(){return-Zo.event.deltaY*(Zo.event.deltaMode?120:1)},"wheel"):"onmousewheel"in $o?(Ta=function(){return Zo.event.wheelDelta},"mousewheel"):(Ta=function(){return-Zo.event.detail},"MozMousePixelScroll");Zo.color=et,et.prototype.toString=function(){return this.rgb()+""},Zo.hsl=rt;var Da=rt.prototype=new et;Da.brighter=function(n){return n=Math.pow(.7,arguments.length?n:1),new rt(this.h,this.s,this.l/n)},Da.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),new rt(this.h,this.s,n*this.l)},Da.rgb=function(){return ut(this.h,this.s,this.l)},Zo.hcl=it;var Pa=it.prototype=new et;Pa.brighter=function(n){return new it(this.h,this.c,Math.min(100,this.l+Ua*(arguments.length?n:1)))},Pa.darker=function(n){return new it(this.h,this.c,Math.max(0,this.l-Ua*(arguments.length?n:1)))},Pa.rgb=function(){return ot(this.h,this.c,this.l).rgb()},Zo.lab=at;var Ua=18,ja=.95047,Ha=1,Fa=1.08883,Oa=at.prototype=new et;Oa.brighter=function(n){return new at(Math.min(100,this.l+Ua*(arguments.length?n:1)),this.a,this.b)},Oa.darker=function(n){return new at(Math.max(0,this.l-Ua*(arguments.length?n:1)),this.a,this.b)},Oa.rgb=function(){return ct(this.l,this.a,this.b)},Zo.rgb=gt;var Ya=gt.prototype=new et;Ya.brighter=function(n){n=Math.pow(.7,arguments.length?n:1);var t=this.r,e=this.g,r=this.b,u=30;return t||e||r?(t&&u>t&&(t=u),e&&u>e&&(e=u),r&&u>r&&(r=u),new gt(Math.min(255,t/n),Math.min(255,e/n),Math.min(255,r/n))):new gt(u,u,u)},Ya.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),new gt(n*this.r,n*this.g,n*this.b)},Ya.hsl=function(){return yt(this.r,this.g,this.b)},Ya.toString=function(){return"#"+dt(this.r)+dt(this.g)+dt(this.b)};var Ia=Zo.map({aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074});Ia.forEach(function(n,t){Ia.set(n,pt(t))}),Zo.functor=bt,Zo.xhr=St(wt),Zo.dsv=function(n,t){function e(n,e,i){arguments.length<3&&(i=e,e=null);var o=kt(n,t,null==e?r:u(e),i);return o.row=function(n){return arguments.length?o.response(null==(e=n)?r:u(n)):e},o}function r(n){return e.parse(n.responseText)}function u(n){return function(t){return e.parse(t.responseText,n)}}function i(t){return t.map(o).join(n)}function o(n){return a.test(n)?'"'+n.replace(/\"/g,'""')+'"':n}var a=new RegExp('["'+n+"\n]"),c=n.charCodeAt(0);return e.parse=function(n,t){var r;return e.parseRows(n,function(n,e){if(r)return r(n,e-1);var u=new Function("d","return {"+n.map(function(n,t){return JSON.stringify(n)+": d["+t+"]"}).join(",")+"}");r=t?function(n,e){return t(u(n),e)}:u})},e.parseRows=function(n,t){function e(){if(l>=s)return o;if(u)return u=!1,i;var t=l;if(34===n.charCodeAt(t)){for(var e=t;e++<s;)if(34===n.charCodeAt(e)){if(34!==n.charCodeAt(e+1))break;++e}l=e+2;var r=n.charCodeAt(e+1);return 13===r?(u=!0,10===n.charCodeAt(e+2)&&++l):10===r&&(u=!0),n.substring(t+1,e).replace(/""/g,'"')}for(;s>l;){var r=n.charCodeAt(l++),a=1;if(10===r)u=!0;else if(13===r)u=!0,10===n.charCodeAt(l)&&(++l,++a);else if(r!==c)continue;return n.substring(t,l-a)}return n.substring(t)}for(var r,u,i={},o={},a=[],s=n.length,l=0,f=0;(r=e())!==o;){for(var h=[];r!==i&&r!==o;)h.push(r),r=e();(!t||(h=t(h,f++)))&&a.push(h)}return a},e.format=function(t){if(Array.isArray(t[0]))return e.formatRows(t);var r=new h,u=[];return t.forEach(function(n){for(var t in n)r.has(t)||u.push(r.add(t))}),[u.map(o).join(n)].concat(t.map(function(t){return u.map(function(n){return o(t[n])}).join(n)})).join("\n")},e.formatRows=function(n){return n.map(i).join("\n")},e},Zo.csv=Zo.dsv(",","text/csv"),Zo.tsv=Zo.dsv(" ","text/tab-separated-values"),Zo.touch=function(n,t,e){if(arguments.length<3&&(e=t,t=x().changedTouches),t)for(var r,u=0,i=t.length;i>u;++u)if((r=t[u]).identifier===e)return Z(n,r)};var Za,Va,Xa,$a,Ba,Wa=Wo[p(Wo,"requestAnimationFrame")]||function(n){setTimeout(n,17)};Zo.timer=function(n,t,e){var r=arguments.length;2>r&&(t=0),3>r&&(e=Date.now());var u=e+t,i={c:n,t:u,f:!1,n:null};Va?Va.n=i:Za=i,Va=i,Xa||($a=clearTimeout($a),Xa=1,Wa(At))},Zo.timer.flush=function(){Ct(),Nt()},Zo.round=function(n,t){return t?Math.round(n*(t=Math.pow(10,t)))/t:Math.round(n)};var Ja=["y","z","a","f","p","n","\xb5","m","","k","M","G","T","P","E","Z","Y"].map(Lt);Zo.formatPrefix=function(n,t){var e=0;return n&&(0>n&&(n*=-1),t&&(n=Zo.round(n,zt(n,t))),e=1+Math.floor(1e-12+Math.log(n)/Math.LN10),e=Math.max(-24,Math.min(24,3*Math.floor((e-1)/3)))),Ja[8+e/3]};var Ga=/(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i,Ka=Zo.map({b:function(n){return n.toString(2)},c:function(n){return String.fromCharCode(n)},o:function(n){return n.toString(8)},x:function(n){return n.toString(16)},X:function(n){return n.toString(16).toUpperCase()},g:function(n,t){return n.toPrecision(t)},e:function(n,t){return n.toExponential(t)},f:function(n,t){return n.toFixed(t)},r:function(n,t){return(n=Zo.round(n,zt(n,t))).toFixed(Math.max(0,Math.min(20,zt(n*(1+1e-15),t))))}}),Qa=Zo.time={},nc=Date;Rt.prototype={getDate:function(){return this._.getUTCDate()},getDay:function(){return this._.getUTCDay()},getFullYear:function(){return this._.getUTCFullYear()},getHours:function(){return this._.getUTCHours()},getMilliseconds:function(){return this._.getUTCMilliseconds()},getMinutes:function(){return this._.getUTCMinutes()},getMonth:function(){return this._.getUTCMonth()},getSeconds:function(){return this._.getUTCSeconds()},getTime:function(){return this._.getTime()},getTimezoneOffset:function(){return 0},valueOf:function(){return this._.valueOf()},setDate:function(){tc.setUTCDate.apply(this._,arguments)},setDay:function(){tc.setUTCDay.apply(this._,arguments)},setFullYear:function(){tc.setUTCFullYear.apply(this._,arguments)},setHours:function(){tc.setUTCHours.apply(this._,arguments)},setMilliseconds:function(){tc.setUTCMilliseconds.apply(this._,arguments)},setMinutes:function(){tc.setUTCMinutes.apply(this._,arguments)},setMonth:function(){tc.setUTCMonth.apply(this._,arguments)},setSeconds:function(){tc.setUTCSeconds.apply(this._,arguments)},setTime:function(){tc.setTime.apply(this._,arguments)}};var tc=Date.prototype;Qa.year=Dt(function(n){return n=Qa.day(n),n.setMonth(0,1),n},function(n,t){n.setFullYear(n.getFullYear()+t)},function(n){return n.getFullYear()}),Qa.years=Qa.year.range,Qa.years.utc=Qa.year.utc.range,Qa.day=Dt(function(n){var t=new nc(2e3,0);return t.setFullYear(n.getFullYear(),n.getMonth(),n.getDate()),t},function(n,t){n.setDate(n.getDate()+t)},function(n){return n.getDate()-1}),Qa.days=Qa.day.range,Qa.days.utc=Qa.day.utc.range,Qa.dayOfYear=function(n){var t=Qa.year(n);return Math.floor((n-t-6e4*(n.getTimezoneOffset()-t.getTimezoneOffset()))/864e5)},["sunday","monday","tuesday","wednesday","thursday","friday","saturday"].forEach(function(n,t){t=7-t;var e=Qa[n]=Dt(function(n){return(n=Qa.day(n)).setDate(n.getDate()-(n.getDay()+t)%7),n},function(n,t){n.setDate(n.getDate()+7*Math.floor(t))},function(n){var e=Qa.year(n).getDay();return Math.floor((Qa.dayOfYear(n)+(e+t)%7)/7)-(e!==t)});Qa[n+"s"]=e.range,Qa[n+"s"].utc=e.utc.range,Qa[n+"OfYear"]=function(n){var e=Qa.year(n).getDay();return Math.floor((Qa.dayOfYear(n)+(e+t)%7)/7)}}),Qa.week=Qa.sunday,Qa.weeks=Qa.sunday.range,Qa.weeks.utc=Qa.sunday.utc.range,Qa.weekOfYear=Qa.sundayOfYear;var ec={"-":"",_:" ",0:"0"},rc=/^\s*\d+/,uc=/^%/;Zo.locale=function(n){return{numberFormat:Tt(n),timeFormat:Ut(n)}};var ic=Zo.locale({decimal:".",thousands:",",grouping:[3],currency:["$",""],dateTime:"%a %b %e %X %Y",date:"%m/%d/%Y",time:"%H:%M:%S",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});Zo.format=ic.numberFormat,Zo.geo={},ue.prototype={s:0,t:0,add:function(n){ie(n,this.t,oc),ie(oc.s,this.s,this),this.s?this.t+=oc.t:this.s=oc.t},reset:function(){this.s=this.t=0},valueOf:function(){return this.s}};var oc=new ue;Zo.geo.stream=function(n,t){n&&ac.hasOwnProperty(n.type)?ac[n.type](n,t):oe(n,t)};var ac={Feature:function(n,t){oe(n.geometry,t)},FeatureCollection:function(n,t){for(var e=n.features,r=-1,u=e.length;++r<u;)oe(e[r].geometry,t)}},cc={Sphere:function(n,t){t.sphere()},Point:function(n,t){n=n.coordinates,t.point(n[0],n[1],n[2])},MultiPoint:function(n,t){for(var e=n.coordinates,r=-1,u=e.length;++r<u;)n=e[r],t.point(n[0],n[1],n[2])},LineString:function(n,t){ae(n.coordinates,t,0)},MultiLineString:function(n,t){for(var e=n.coordinates,r=-1,u=e.length;++r<u;)ae(e[r],t,0)},Polygon:function(n,t){ce(n.coordinates,t)},MultiPolygon:function(n,t){for(var e=n.coordinates,r=-1,u=e.length;++r<u;)ce(e[r],t)},GeometryCollection:function(n,t){for(var e=n.geometries,r=-1,u=e.length;++r<u;)oe(e[r],t)}};Zo.geo.area=function(n){return sc=0,Zo.geo.stream(n,fc),sc};var sc,lc=new ue,fc={sphere:function(){sc+=4*ba},point:v,lineStart:v,lineEnd:v,polygonStart:function(){lc.reset(),fc.lineStart=se},polygonEnd:function(){var n=2*lc;sc+=0>n?4*ba+n:n,fc.lineStart=fc.lineEnd=fc.point=v}};Zo.geo.bounds=function(){function n(n,t){x.push(M=[l=n,h=n]),f>t&&(f=t),t>g&&(g=t)}function t(t,e){var r=le([t*Aa,e*Aa]);if(m){var u=he(m,r),i=[u[1],-u[0],0],o=he(i,u);ve(o),o=de(o);var c=t-p,s=c>0?1:-1,v=o[0]*Ca*s,d=ua(c)>180;if(d^(v>s*p&&s*t>v)){var y=o[1]*Ca;y>g&&(g=y)}else if(v=(v+360)%360-180,d^(v>s*p&&s*t>v)){var y=-o[1]*Ca;f>y&&(f=y)}else f>e&&(f=e),e>g&&(g=e);d?p>t?a(l,t)>a(l,h)&&(h=t):a(t,h)>a(l,h)&&(l=t):h>=l?(l>t&&(l=t),t>h&&(h=t)):t>p?a(l,t)>a(l,h)&&(h=t):a(t,h)>a(l,h)&&(l=t)}else n(t,e);m=r,p=t}function e(){_.point=t}function r(){M[0]=l,M[1]=h,_.point=n,m=null}function u(n,e){if(m){var r=n-p;y+=ua(r)>180?r+(r>0?360:-360):r}else v=n,d=e;fc.point(n,e),t(n,e)}function i(){fc.lineStart()}function o(){u(v,d),fc.lineEnd(),ua(y)>ka&&(l=-(h=180)),M[0]=l,M[1]=h,m=null}function a(n,t){return(t-=n)<0?t+360:t}function c(n,t){return n[0]-t[0]}function s(n,t){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:n<t[0]||t[1]<n}var l,f,h,g,p,v,d,m,y,x,M,_={point:n,lineStart:e,lineEnd:r,polygonStart:function(){_.point=u,_.lineStart=i,_.lineEnd=o,y=0,fc.polygonStart()},polygonEnd:function(){fc.polygonEnd(),_.point=n,_.lineStart=e,_.lineEnd=r,0>lc?(l=-(h=180),f=-(g=90)):y>ka?g=90:-ka>y&&(f=-90),M[0]=l,M[1]=h}};return function(n){g=h=-(l=f=1/0),x=[],Zo.geo.stream(n,_);var t=x.length;if(t){x.sort(c);for(var e,r=1,u=x[0],i=[u];t>r;++r)e=x[r],s(e[0],u)||s(e[1],u)?(a(u[0],e[1])>a(u[0],u[1])&&(u[1]=e[1]),a(e[0],u[1])>a(u[0],u[1])&&(u[0]=e[0])):i.push(u=e); +for(var o,e,p=-1/0,t=i.length-1,r=0,u=i[t];t>=r;u=e,++r)e=i[r],(o=a(u[1],e[0]))>p&&(p=o,l=e[0],h=u[1])}return x=M=null,1/0===l||1/0===f?[[0/0,0/0],[0/0,0/0]]:[[l,f],[h,g]]}}(),Zo.geo.centroid=function(n){hc=gc=pc=vc=dc=mc=yc=xc=Mc=_c=bc=0,Zo.geo.stream(n,wc);var t=Mc,e=_c,r=bc,u=t*t+e*e+r*r;return Ea>u&&(t=mc,e=yc,r=xc,ka>gc&&(t=pc,e=vc,r=dc),u=t*t+e*e+r*r,Ea>u)?[0/0,0/0]:[Math.atan2(e,t)*Ca,G(r/Math.sqrt(u))*Ca]};var hc,gc,pc,vc,dc,mc,yc,xc,Mc,_c,bc,wc={sphere:v,point:ye,lineStart:Me,lineEnd:_e,polygonStart:function(){wc.lineStart=be},polygonEnd:function(){wc.lineStart=Me}},Sc=Ae(we,Te,Re,[-ba,-ba/2]),kc=1e9;Zo.geo.clipExtent=function(){var n,t,e,r,u,i,o={stream:function(n){return u&&(u.valid=!1),u=i(n),u.valid=!0,u},extent:function(a){return arguments.length?(i=Ue(n=+a[0][0],t=+a[0][1],e=+a[1][0],r=+a[1][1]),u&&(u.valid=!1,u=null),o):[[n,t],[e,r]]}};return o.extent([[0,0],[960,500]])},(Zo.geo.conicEqualArea=function(){return He(Fe)}).raw=Fe,Zo.geo.albers=function(){return Zo.geo.conicEqualArea().rotate([96,0]).center([-.6,38.7]).parallels([29.5,45.5]).scale(1070)},Zo.geo.albersUsa=function(){function n(n){var i=n[0],o=n[1];return t=null,e(i,o),t||(r(i,o),t)||u(i,o),t}var t,e,r,u,i=Zo.geo.albers(),o=Zo.geo.conicEqualArea().rotate([154,0]).center([-2,58.5]).parallels([55,65]),a=Zo.geo.conicEqualArea().rotate([157,0]).center([-3,19.9]).parallels([8,18]),c={point:function(n,e){t=[n,e]}};return n.invert=function(n){var t=i.scale(),e=i.translate(),r=(n[0]-e[0])/t,u=(n[1]-e[1])/t;return(u>=.12&&.234>u&&r>=-.425&&-.214>r?o:u>=.166&&.234>u&&r>=-.214&&-.115>r?a:i).invert(n)},n.stream=function(n){var t=i.stream(n),e=o.stream(n),r=a.stream(n);return{point:function(n,u){t.point(n,u),e.point(n,u),r.point(n,u)},sphere:function(){t.sphere(),e.sphere(),r.sphere()},lineStart:function(){t.lineStart(),e.lineStart(),r.lineStart()},lineEnd:function(){t.lineEnd(),e.lineEnd(),r.lineEnd()},polygonStart:function(){t.polygonStart(),e.polygonStart(),r.polygonStart()},polygonEnd:function(){t.polygonEnd(),e.polygonEnd(),r.polygonEnd()}}},n.precision=function(t){return arguments.length?(i.precision(t),o.precision(t),a.precision(t),n):i.precision()},n.scale=function(t){return arguments.length?(i.scale(t),o.scale(.35*t),a.scale(t),n.translate(i.translate())):i.scale()},n.translate=function(t){if(!arguments.length)return i.translate();var s=i.scale(),l=+t[0],f=+t[1];return e=i.translate(t).clipExtent([[l-.455*s,f-.238*s],[l+.455*s,f+.238*s]]).stream(c).point,r=o.translate([l-.307*s,f+.201*s]).clipExtent([[l-.425*s+ka,f+.12*s+ka],[l-.214*s-ka,f+.234*s-ka]]).stream(c).point,u=a.translate([l-.205*s,f+.212*s]).clipExtent([[l-.214*s+ka,f+.166*s+ka],[l-.115*s-ka,f+.234*s-ka]]).stream(c).point,n},n.scale(1070)};var Ec,Ac,Cc,Nc,zc,Lc,Tc={point:v,lineStart:v,lineEnd:v,polygonStart:function(){Ac=0,Tc.lineStart=Oe},polygonEnd:function(){Tc.lineStart=Tc.lineEnd=Tc.point=v,Ec+=ua(Ac/2)}},qc={point:Ye,lineStart:v,lineEnd:v,polygonStart:v,polygonEnd:v},Rc={point:Ve,lineStart:Xe,lineEnd:$e,polygonStart:function(){Rc.lineStart=Be},polygonEnd:function(){Rc.point=Ve,Rc.lineStart=Xe,Rc.lineEnd=$e}};Zo.geo.path=function(){function n(n){return n&&("function"==typeof a&&i.pointRadius(+a.apply(this,arguments)),o&&o.valid||(o=u(i)),Zo.geo.stream(n,o)),i.result()}function t(){return o=null,n}var e,r,u,i,o,a=4.5;return n.area=function(n){return Ec=0,Zo.geo.stream(n,u(Tc)),Ec},n.centroid=function(n){return pc=vc=dc=mc=yc=xc=Mc=_c=bc=0,Zo.geo.stream(n,u(Rc)),bc?[Mc/bc,_c/bc]:xc?[mc/xc,yc/xc]:dc?[pc/dc,vc/dc]:[0/0,0/0]},n.bounds=function(n){return zc=Lc=-(Cc=Nc=1/0),Zo.geo.stream(n,u(qc)),[[Cc,Nc],[zc,Lc]]},n.projection=function(n){return arguments.length?(u=(e=n)?n.stream||Ge(n):wt,t()):e},n.context=function(n){return arguments.length?(i=null==(r=n)?new Ie:new We(n),"function"!=typeof a&&i.pointRadius(a),t()):r},n.pointRadius=function(t){return arguments.length?(a="function"==typeof t?t:(i.pointRadius(+t),+t),n):a},n.projection(Zo.geo.albersUsa()).context(null)},Zo.geo.transform=function(n){return{stream:function(t){var e=new Ke(t);for(var r in n)e[r]=n[r];return e}}},Ke.prototype={point:function(n,t){this.stream.point(n,t)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}},Zo.geo.projection=nr,Zo.geo.projectionMutator=tr,(Zo.geo.equirectangular=function(){return nr(rr)}).raw=rr.invert=rr,Zo.geo.rotation=function(n){function t(t){return t=n(t[0]*Aa,t[1]*Aa),t[0]*=Ca,t[1]*=Ca,t}return n=ir(n[0]%360*Aa,n[1]*Aa,n.length>2?n[2]*Aa:0),t.invert=function(t){return t=n.invert(t[0]*Aa,t[1]*Aa),t[0]*=Ca,t[1]*=Ca,t},t},ur.invert=rr,Zo.geo.circle=function(){function n(){var n="function"==typeof r?r.apply(this,arguments):r,t=ir(-n[0]*Aa,-n[1]*Aa,0).invert,u=[];return e(null,null,1,{point:function(n,e){u.push(n=t(n,e)),n[0]*=Ca,n[1]*=Ca}}),{type:"Polygon",coordinates:[u]}}var t,e,r=[0,0],u=6;return n.origin=function(t){return arguments.length?(r=t,n):r},n.angle=function(r){return arguments.length?(e=sr((t=+r)*Aa,u*Aa),n):t},n.precision=function(r){return arguments.length?(e=sr(t*Aa,(u=+r)*Aa),n):u},n.angle(90)},Zo.geo.distance=function(n,t){var e,r=(t[0]-n[0])*Aa,u=n[1]*Aa,i=t[1]*Aa,o=Math.sin(r),a=Math.cos(r),c=Math.sin(u),s=Math.cos(u),l=Math.sin(i),f=Math.cos(i);return Math.atan2(Math.sqrt((e=f*o)*e+(e=s*l-c*f*a)*e),c*l+s*f*a)},Zo.geo.graticule=function(){function n(){return{type:"MultiLineString",coordinates:t()}}function t(){return Zo.range(Math.ceil(i/d)*d,u,d).map(h).concat(Zo.range(Math.ceil(s/m)*m,c,m).map(g)).concat(Zo.range(Math.ceil(r/p)*p,e,p).filter(function(n){return ua(n%d)>ka}).map(l)).concat(Zo.range(Math.ceil(a/v)*v,o,v).filter(function(n){return ua(n%m)>ka}).map(f))}var e,r,u,i,o,a,c,s,l,f,h,g,p=10,v=p,d=90,m=360,y=2.5;return n.lines=function(){return t().map(function(n){return{type:"LineString",coordinates:n}})},n.outline=function(){return{type:"Polygon",coordinates:[h(i).concat(g(c).slice(1),h(u).reverse().slice(1),g(s).reverse().slice(1))]}},n.extent=function(t){return arguments.length?n.majorExtent(t).minorExtent(t):n.minorExtent()},n.majorExtent=function(t){return arguments.length?(i=+t[0][0],u=+t[1][0],s=+t[0][1],c=+t[1][1],i>u&&(t=i,i=u,u=t),s>c&&(t=s,s=c,c=t),n.precision(y)):[[i,s],[u,c]]},n.minorExtent=function(t){return arguments.length?(r=+t[0][0],e=+t[1][0],a=+t[0][1],o=+t[1][1],r>e&&(t=r,r=e,e=t),a>o&&(t=a,a=o,o=t),n.precision(y)):[[r,a],[e,o]]},n.step=function(t){return arguments.length?n.majorStep(t).minorStep(t):n.minorStep()},n.majorStep=function(t){return arguments.length?(d=+t[0],m=+t[1],n):[d,m]},n.minorStep=function(t){return arguments.length?(p=+t[0],v=+t[1],n):[p,v]},n.precision=function(t){return arguments.length?(y=+t,l=fr(a,o,90),f=hr(r,e,y),h=fr(s,c,90),g=hr(i,u,y),n):y},n.majorExtent([[-180,-90+ka],[180,90-ka]]).minorExtent([[-180,-80-ka],[180,80+ka]])},Zo.geo.greatArc=function(){function n(){return{type:"LineString",coordinates:[t||r.apply(this,arguments),e||u.apply(this,arguments)]}}var t,e,r=gr,u=pr;return n.distance=function(){return Zo.geo.distance(t||r.apply(this,arguments),e||u.apply(this,arguments))},n.source=function(e){return arguments.length?(r=e,t="function"==typeof e?null:e,n):r},n.target=function(t){return arguments.length?(u=t,e="function"==typeof t?null:t,n):u},n.precision=function(){return arguments.length?n:0},n},Zo.geo.interpolate=function(n,t){return vr(n[0]*Aa,n[1]*Aa,t[0]*Aa,t[1]*Aa)},Zo.geo.length=function(n){return Dc=0,Zo.geo.stream(n,Pc),Dc};var Dc,Pc={sphere:v,point:v,lineStart:dr,lineEnd:v,polygonStart:v,polygonEnd:v},Uc=mr(function(n){return Math.sqrt(2/(1+n))},function(n){return 2*Math.asin(n/2)});(Zo.geo.azimuthalEqualArea=function(){return nr(Uc)}).raw=Uc;var jc=mr(function(n){var t=Math.acos(n);return t&&t/Math.sin(t)},wt);(Zo.geo.azimuthalEquidistant=function(){return nr(jc)}).raw=jc,(Zo.geo.conicConformal=function(){return He(yr)}).raw=yr,(Zo.geo.conicEquidistant=function(){return He(xr)}).raw=xr;var Hc=mr(function(n){return 1/n},Math.atan);(Zo.geo.gnomonic=function(){return nr(Hc)}).raw=Hc,Mr.invert=function(n,t){return[n,2*Math.atan(Math.exp(t))-Sa]},(Zo.geo.mercator=function(){return _r(Mr)}).raw=Mr;var Fc=mr(function(){return 1},Math.asin);(Zo.geo.orthographic=function(){return nr(Fc)}).raw=Fc;var Oc=mr(function(n){return 1/(1+n)},function(n){return 2*Math.atan(n)});(Zo.geo.stereographic=function(){return nr(Oc)}).raw=Oc,br.invert=function(n,t){return[-t,2*Math.atan(Math.exp(n))-Sa]},(Zo.geo.transverseMercator=function(){var n=_r(br),t=n.center,e=n.rotate;return n.center=function(n){return n?t([-n[1],n[0]]):(n=t(),[n[1],-n[0]])},n.rotate=function(n){return n?e([n[0],n[1],n.length>2?n[2]+90:90]):(n=e(),[n[0],n[1],n[2]-90])},e([0,0,90])}).raw=br,Zo.geom={},Zo.geom.hull=function(n){function t(n){if(n.length<3)return[];var t,u=bt(e),i=bt(r),o=n.length,a=[],c=[];for(t=0;o>t;t++)a.push([+u.call(this,n[t],t),+i.call(this,n[t],t),t]);for(a.sort(Er),t=0;o>t;t++)c.push([a[t][0],-a[t][1]]);var s=kr(a),l=kr(c),f=l[0]===s[0],h=l[l.length-1]===s[s.length-1],g=[];for(t=s.length-1;t>=0;--t)g.push(n[a[s[t]][2]]);for(t=+f;t<l.length-h;++t)g.push(n[a[l[t]][2]]);return g}var e=wr,r=Sr;return arguments.length?t(n):(t.x=function(n){return arguments.length?(e=n,t):e},t.y=function(n){return arguments.length?(r=n,t):r},t)},Zo.geom.polygon=function(n){return sa(n,Yc),n};var Yc=Zo.geom.polygon.prototype=[];Yc.area=function(){for(var n,t=-1,e=this.length,r=this[e-1],u=0;++t<e;)n=r,r=this[t],u+=n[1]*r[0]-n[0]*r[1];return.5*u},Yc.centroid=function(n){var t,e,r=-1,u=this.length,i=0,o=0,a=this[u-1];for(arguments.length||(n=-1/(6*this.area()));++r<u;)t=a,a=this[r],e=t[0]*a[1]-a[0]*t[1],i+=(t[0]+a[0])*e,o+=(t[1]+a[1])*e;return[i*n,o*n]},Yc.clip=function(n){for(var t,e,r,u,i,o,a=Nr(n),c=-1,s=this.length-Nr(this),l=this[s-1];++c<s;){for(t=n.slice(),n.length=0,u=this[c],i=t[(r=t.length-a)-1],e=-1;++e<r;)o=t[e],Ar(o,l,u)?(Ar(i,l,u)||n.push(Cr(i,o,l,u)),n.push(o)):Ar(i,l,u)&&n.push(Cr(i,o,l,u)),i=o;a&&n.push(n[0]),l=u}return n};var Ic,Zc,Vc,Xc,$c,Bc=[],Wc=[];Ur.prototype.prepare=function(){for(var n,t=this.edges,e=t.length;e--;)n=t[e].edge,n.b&&n.a||t.splice(e,1);return t.sort(Hr),t.length},Wr.prototype={start:function(){return this.edge.l===this.site?this.edge.a:this.edge.b},end:function(){return this.edge.l===this.site?this.edge.b:this.edge.a}},Jr.prototype={insert:function(n,t){var e,r,u;if(n){if(t.P=n,t.N=n.N,n.N&&(n.N.P=t),n.N=t,n.R){for(n=n.R;n.L;)n=n.L;n.L=t}else n.R=t;e=n}else this._?(n=nu(this._),t.P=null,t.N=n,n.P=n.L=t,e=n):(t.P=t.N=null,this._=t,e=null);for(t.L=t.R=null,t.U=e,t.C=!0,n=t;e&&e.C;)r=e.U,e===r.L?(u=r.R,u&&u.C?(e.C=u.C=!1,r.C=!0,n=r):(n===e.R&&(Kr(this,e),n=e,e=n.U),e.C=!1,r.C=!0,Qr(this,r))):(u=r.L,u&&u.C?(e.C=u.C=!1,r.C=!0,n=r):(n===e.L&&(Qr(this,e),n=e,e=n.U),e.C=!1,r.C=!0,Kr(this,r))),e=n.U;this._.C=!1},remove:function(n){n.N&&(n.N.P=n.P),n.P&&(n.P.N=n.N),n.N=n.P=null;var t,e,r,u=n.U,i=n.L,o=n.R;if(e=i?o?nu(o):i:o,u?u.L===n?u.L=e:u.R=e:this._=e,i&&o?(r=e.C,e.C=n.C,e.L=i,i.U=e,e!==o?(u=e.U,e.U=n.U,n=e.R,u.L=n,e.R=o,o.U=e):(e.U=u,u=e,n=e.R)):(r=n.C,n=e),n&&(n.U=u),!r){if(n&&n.C)return n.C=!1,void 0;do{if(n===this._)break;if(n===u.L){if(t=u.R,t.C&&(t.C=!1,u.C=!0,Kr(this,u),t=u.R),t.L&&t.L.C||t.R&&t.R.C){t.R&&t.R.C||(t.L.C=!1,t.C=!0,Qr(this,t),t=u.R),t.C=u.C,u.C=t.R.C=!1,Kr(this,u),n=this._;break}}else if(t=u.L,t.C&&(t.C=!1,u.C=!0,Qr(this,u),t=u.L),t.L&&t.L.C||t.R&&t.R.C){t.L&&t.L.C||(t.R.C=!1,t.C=!0,Kr(this,t),t=u.L),t.C=u.C,u.C=t.L.C=!1,Qr(this,u),n=this._;break}t.C=!0,n=u,u=u.U}while(!n.C);n&&(n.C=!1)}}},Zo.geom.voronoi=function(n){function t(n){var t=new Array(n.length),r=a[0][0],u=a[0][1],i=a[1][0],o=a[1][1];return tu(e(n),a).cells.forEach(function(e,a){var c=e.edges,s=e.site,l=t[a]=c.length?c.map(function(n){var t=n.start();return[t.x,t.y]}):s.x>=r&&s.x<=i&&s.y>=u&&s.y<=o?[[r,o],[i,o],[i,u],[r,u]]:[];l.point=n[a]}),t}function e(n){return n.map(function(n,t){return{x:Math.round(i(n,t)/ka)*ka,y:Math.round(o(n,t)/ka)*ka,i:t}})}var r=wr,u=Sr,i=r,o=u,a=Jc;return n?t(n):(t.links=function(n){return tu(e(n)).edges.filter(function(n){return n.l&&n.r}).map(function(t){return{source:n[t.l.i],target:n[t.r.i]}})},t.triangles=function(n){var t=[];return tu(e(n)).cells.forEach(function(e,r){for(var u,i,o=e.site,a=e.edges.sort(Hr),c=-1,s=a.length,l=a[s-1].edge,f=l.l===o?l.r:l.l;++c<s;)u=l,i=f,l=a[c].edge,f=l.l===o?l.r:l.l,r<i.i&&r<f.i&&ru(o,i,f)<0&&t.push([n[r],n[i.i],n[f.i]])}),t},t.x=function(n){return arguments.length?(i=bt(r=n),t):r},t.y=function(n){return arguments.length?(o=bt(u=n),t):u},t.clipExtent=function(n){return arguments.length?(a=null==n?Jc:n,t):a===Jc?null:a},t.size=function(n){return arguments.length?t.clipExtent(n&&[[0,0],n]):a===Jc?null:a&&a[1]},t)};var Jc=[[-1e6,-1e6],[1e6,1e6]];Zo.geom.delaunay=function(n){return Zo.geom.voronoi().triangles(n)},Zo.geom.quadtree=function(n,t,e,r,u){function i(n){function i(n,t,e,r,u,i,o,a){if(!isNaN(e)&&!isNaN(r))if(n.leaf){var c=n.x,l=n.y;if(null!=c)if(ua(c-e)+ua(l-r)<.01)s(n,t,e,r,u,i,o,a);else{var f=n.point;n.x=n.y=n.point=null,s(n,f,c,l,u,i,o,a),s(n,t,e,r,u,i,o,a)}else n.x=e,n.y=r,n.point=t}else s(n,t,e,r,u,i,o,a)}function s(n,t,e,r,u,o,a,c){var s=.5*(u+a),l=.5*(o+c),f=e>=s,h=r>=l,g=(h<<1)+f;n.leaf=!1,n=n.nodes[g]||(n.nodes[g]=ou()),f?u=s:a=s,h?o=l:c=l,i(n,t,e,r,u,o,a,c)}var l,f,h,g,p,v,d,m,y,x=bt(a),M=bt(c);if(null!=t)v=t,d=e,m=r,y=u;else if(m=y=-(v=d=1/0),f=[],h=[],p=n.length,o)for(g=0;p>g;++g)l=n[g],l.x<v&&(v=l.x),l.y<d&&(d=l.y),l.x>m&&(m=l.x),l.y>y&&(y=l.y),f.push(l.x),h.push(l.y);else for(g=0;p>g;++g){var _=+x(l=n[g],g),b=+M(l,g);v>_&&(v=_),d>b&&(d=b),_>m&&(m=_),b>y&&(y=b),f.push(_),h.push(b)}var w=m-v,S=y-d;w>S?y=d+w:m=v+S;var k=ou();if(k.add=function(n){i(k,n,+x(n,++g),+M(n,g),v,d,m,y)},k.visit=function(n){au(n,k,v,d,m,y)},g=-1,null==t){for(;++g<p;)i(k,n[g],f[g],h[g],v,d,m,y);--g}else n.forEach(k.add);return f=h=n=l=null,k}var o,a=wr,c=Sr;return(o=arguments.length)?(a=uu,c=iu,3===o&&(u=e,r=t,e=t=0),i(n)):(i.x=function(n){return arguments.length?(a=n,i):a},i.y=function(n){return arguments.length?(c=n,i):c},i.extent=function(n){return arguments.length?(null==n?t=e=r=u=null:(t=+n[0][0],e=+n[0][1],r=+n[1][0],u=+n[1][1]),i):null==t?null:[[t,e],[r,u]]},i.size=function(n){return arguments.length?(null==n?t=e=r=u=null:(t=e=0,r=+n[0],u=+n[1]),i):null==t?null:[r-t,u-e]},i)},Zo.interpolateRgb=cu,Zo.interpolateObject=su,Zo.interpolateNumber=lu,Zo.interpolateString=fu;var Gc=/[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g,Kc=new RegExp(Gc.source,"g");Zo.interpolate=hu,Zo.interpolators=[function(n,t){var e=typeof t;return("string"===e?Ia.has(t)||/^(#|rgb\(|hsl\()/.test(t)?cu:fu:t instanceof et?cu:Array.isArray(t)?gu:"object"===e&&isNaN(t)?su:lu)(n,t)}],Zo.interpolateArray=gu;var Qc=function(){return wt},ns=Zo.map({linear:Qc,poly:Mu,quad:function(){return mu},cubic:function(){return yu},sin:function(){return _u},exp:function(){return bu},circle:function(){return wu},elastic:Su,back:ku,bounce:function(){return Eu}}),ts=Zo.map({"in":wt,out:vu,"in-out":du,"out-in":function(n){return du(vu(n))}});Zo.ease=function(n){var t=n.indexOf("-"),e=t>=0?n.substring(0,t):n,r=t>=0?n.substring(t+1):"in";return e=ns.get(e)||Qc,r=ts.get(r)||wt,pu(r(e.apply(null,Vo.call(arguments,1))))},Zo.interpolateHcl=Au,Zo.interpolateHsl=Cu,Zo.interpolateLab=Nu,Zo.interpolateRound=zu,Zo.transform=function(n){var t=$o.createElementNS(Zo.ns.prefix.svg,"g");return(Zo.transform=function(n){if(null!=n){t.setAttribute("transform",n);var e=t.transform.baseVal.consolidate()}return new Lu(e?e.matrix:es)})(n)},Lu.prototype.toString=function(){return"translate("+this.translate+")rotate("+this.rotate+")skewX("+this.skew+")scale("+this.scale+")"};var es={a:1,b:0,c:0,d:1,e:0,f:0};Zo.interpolateTransform=Du,Zo.layout={},Zo.layout.bundle=function(){return function(n){for(var t=[],e=-1,r=n.length;++e<r;)t.push(ju(n[e]));return t}},Zo.layout.chord=function(){function n(){var n,s,f,h,g,p={},v=[],d=Zo.range(i),m=[];for(e=[],r=[],n=0,h=-1;++h<i;){for(s=0,g=-1;++g<i;)s+=u[h][g];v.push(s),m.push(Zo.range(i)),n+=s}for(o&&d.sort(function(n,t){return o(v[n],v[t])}),a&&m.forEach(function(n,t){n.sort(function(n,e){return a(u[t][n],u[t][e])})}),n=(wa-l*i)/n,s=0,h=-1;++h<i;){for(f=s,g=-1;++g<i;){var y=d[h],x=m[y][g],M=u[y][x],_=s,b=s+=M*n;p[y+"-"+x]={index:y,subindex:x,startAngle:_,endAngle:b,value:M}}r[y]={index:y,startAngle:f,endAngle:s,value:(s-f)/n},s+=l}for(h=-1;++h<i;)for(g=h-1;++g<i;){var w=p[h+"-"+g],S=p[g+"-"+h];(w.value||S.value)&&e.push(w.value<S.value?{source:S,target:w}:{source:w,target:S})}c&&t()}function t(){e.sort(function(n,t){return c((n.source.value+n.target.value)/2,(t.source.value+t.target.value)/2)})}var e,r,u,i,o,a,c,s={},l=0;return s.matrix=function(n){return arguments.length?(i=(u=n)&&u.length,e=r=null,s):u},s.padding=function(n){return arguments.length?(l=n,e=r=null,s):l},s.sortGroups=function(n){return arguments.length?(o=n,e=r=null,s):o},s.sortSubgroups=function(n){return arguments.length?(a=n,e=null,s):a},s.sortChords=function(n){return arguments.length?(c=n,e&&t(),s):c},s.chords=function(){return e||n(),e},s.groups=function(){return r||n(),r},s},Zo.layout.force=function(){function n(n){return function(t,e,r,u){if(t.point!==n){var i=t.cx-n.x,o=t.cy-n.y,a=u-e,c=i*i+o*o;if(c>a*a/d){if(p>c){var s=t.charge/c;n.px-=i*s,n.py-=o*s}return!0}if(t.point&&c&&p>c){var s=t.pointCharge/c;n.px-=i*s,n.py-=o*s}}return!t.charge}}function t(n){n.px=Zo.event.x,n.py=Zo.event.y,a.resume()}var e,r,u,i,o,a={},c=Zo.dispatch("start","tick","end"),s=[1,1],l=.9,f=rs,h=us,g=-30,p=is,v=.1,d=.64,m=[],y=[];return a.tick=function(){if((r*=.99)<.005)return c.end({type:"end",alpha:r=0}),!0;var t,e,a,f,h,p,d,x,M,_=m.length,b=y.length;for(e=0;b>e;++e)a=y[e],f=a.source,h=a.target,x=h.x-f.x,M=h.y-f.y,(p=x*x+M*M)&&(p=r*i[e]*((p=Math.sqrt(p))-u[e])/p,x*=p,M*=p,h.x-=x*(d=f.weight/(h.weight+f.weight)),h.y-=M*d,f.x+=x*(d=1-d),f.y+=M*d);if((d=r*v)&&(x=s[0]/2,M=s[1]/2,e=-1,d))for(;++e<_;)a=m[e],a.x+=(x-a.x)*d,a.y+=(M-a.y)*d;if(g)for(Vu(t=Zo.geom.quadtree(m),r,o),e=-1;++e<_;)(a=m[e]).fixed||t.visit(n(a));for(e=-1;++e<_;)a=m[e],a.fixed?(a.x=a.px,a.y=a.py):(a.x-=(a.px-(a.px=a.x))*l,a.y-=(a.py-(a.py=a.y))*l);c.tick({type:"tick",alpha:r})},a.nodes=function(n){return arguments.length?(m=n,a):m},a.links=function(n){return arguments.length?(y=n,a):y},a.size=function(n){return arguments.length?(s=n,a):s},a.linkDistance=function(n){return arguments.length?(f="function"==typeof n?n:+n,a):f},a.distance=a.linkDistance,a.linkStrength=function(n){return arguments.length?(h="function"==typeof n?n:+n,a):h},a.friction=function(n){return arguments.length?(l=+n,a):l},a.charge=function(n){return arguments.length?(g="function"==typeof n?n:+n,a):g},a.chargeDistance=function(n){return arguments.length?(p=n*n,a):Math.sqrt(p)},a.gravity=function(n){return arguments.length?(v=+n,a):v},a.theta=function(n){return arguments.length?(d=n*n,a):Math.sqrt(d)},a.alpha=function(n){return arguments.length?(n=+n,r?r=n>0?n:0:n>0&&(c.start({type:"start",alpha:r=n}),Zo.timer(a.tick)),a):r},a.start=function(){function n(n,r){if(!e){for(e=new Array(c),a=0;c>a;++a)e[a]=[];for(a=0;s>a;++a){var u=y[a];e[u.source.index].push(u.target),e[u.target.index].push(u.source)}}for(var i,o=e[t],a=-1,s=o.length;++a<s;)if(!isNaN(i=o[a][n]))return i;return Math.random()*r}var t,e,r,c=m.length,l=y.length,p=s[0],v=s[1];for(t=0;c>t;++t)(r=m[t]).index=t,r.weight=0;for(t=0;l>t;++t)r=y[t],"number"==typeof r.source&&(r.source=m[r.source]),"number"==typeof r.target&&(r.target=m[r.target]),++r.source.weight,++r.target.weight;for(t=0;c>t;++t)r=m[t],isNaN(r.x)&&(r.x=n("x",p)),isNaN(r.y)&&(r.y=n("y",v)),isNaN(r.px)&&(r.px=r.x),isNaN(r.py)&&(r.py=r.y);if(u=[],"function"==typeof f)for(t=0;l>t;++t)u[t]=+f.call(this,y[t],t);else for(t=0;l>t;++t)u[t]=f;if(i=[],"function"==typeof h)for(t=0;l>t;++t)i[t]=+h.call(this,y[t],t);else for(t=0;l>t;++t)i[t]=h;if(o=[],"function"==typeof g)for(t=0;c>t;++t)o[t]=+g.call(this,m[t],t);else for(t=0;c>t;++t)o[t]=g;return a.resume()},a.resume=function(){return a.alpha(.1)},a.stop=function(){return a.alpha(0)},a.drag=function(){return e||(e=Zo.behavior.drag().origin(wt).on("dragstart.force",Ou).on("drag.force",t).on("dragend.force",Yu)),arguments.length?(this.on("mouseover.force",Iu).on("mouseout.force",Zu).call(e),void 0):e},Zo.rebind(a,c,"on")};var rs=20,us=1,is=1/0;Zo.layout.hierarchy=function(){function n(u){var i,o=[u],a=[];for(u.depth=0;null!=(i=o.pop());)if(a.push(i),(s=e.call(n,i,i.depth))&&(c=s.length)){for(var c,s,l;--c>=0;)o.push(l=s[c]),l.parent=i,l.depth=i.depth+1;r&&(i.value=0),i.children=s}else r&&(i.value=+r.call(n,i,i.depth)||0),delete i.children;return Bu(u,function(n){var e,u;t&&(e=n.children)&&e.sort(t),r&&(u=n.parent)&&(u.value+=n.value)}),a}var t=Gu,e=Wu,r=Ju;return n.sort=function(e){return arguments.length?(t=e,n):t},n.children=function(t){return arguments.length?(e=t,n):e},n.value=function(t){return arguments.length?(r=t,n):r},n.revalue=function(t){return r&&($u(t,function(n){n.children&&(n.value=0)}),Bu(t,function(t){var e;t.children||(t.value=+r.call(n,t,t.depth)||0),(e=t.parent)&&(e.value+=t.value)})),t},n},Zo.layout.partition=function(){function n(t,e,r,u){var i=t.children;if(t.x=e,t.y=t.depth*u,t.dx=r,t.dy=u,i&&(o=i.length)){var o,a,c,s=-1;for(r=t.value?r/t.value:0;++s<o;)n(a=i[s],e,c=a.value*r,u),e+=c}}function t(n){var e=n.children,r=0;if(e&&(u=e.length))for(var u,i=-1;++i<u;)r=Math.max(r,t(e[i]));return 1+r}function e(e,i){var o=r.call(this,e,i);return n(o[0],0,u[0],u[1]/t(o[0])),o}var r=Zo.layout.hierarchy(),u=[1,1];return e.size=function(n){return arguments.length?(u=n,e):u},Xu(e,r)},Zo.layout.pie=function(){function n(i){var o=i.map(function(e,r){return+t.call(n,e,r)}),a=+("function"==typeof r?r.apply(this,arguments):r),c=(("function"==typeof u?u.apply(this,arguments):u)-a)/Zo.sum(o),s=Zo.range(i.length);null!=e&&s.sort(e===os?function(n,t){return o[t]-o[n]}:function(n,t){return e(i[n],i[t])});var l=[];return s.forEach(function(n){var t;l[n]={data:i[n],value:t=o[n],startAngle:a,endAngle:a+=t*c}}),l}var t=Number,e=os,r=0,u=wa;return n.value=function(e){return arguments.length?(t=e,n):t},n.sort=function(t){return arguments.length?(e=t,n):e},n.startAngle=function(t){return arguments.length?(r=t,n):r},n.endAngle=function(t){return arguments.length?(u=t,n):u},n};var os={};Zo.layout.stack=function(){function n(a,c){var s=a.map(function(e,r){return t.call(n,e,r)}),l=s.map(function(t){return t.map(function(t,e){return[i.call(n,t,e),o.call(n,t,e)]})}),f=e.call(n,l,c);s=Zo.permute(s,f),l=Zo.permute(l,f);var h,g,p,v=r.call(n,l,c),d=s.length,m=s[0].length;for(g=0;m>g;++g)for(u.call(n,s[0][g],p=v[g],l[0][g][1]),h=1;d>h;++h)u.call(n,s[h][g],p+=l[h-1][g][1],l[h][g][1]);return a}var t=wt,e=ei,r=ri,u=ti,i=Qu,o=ni;return n.values=function(e){return arguments.length?(t=e,n):t},n.order=function(t){return arguments.length?(e="function"==typeof t?t:as.get(t)||ei,n):e},n.offset=function(t){return arguments.length?(r="function"==typeof t?t:cs.get(t)||ri,n):r},n.x=function(t){return arguments.length?(i=t,n):i},n.y=function(t){return arguments.length?(o=t,n):o},n.out=function(t){return arguments.length?(u=t,n):u},n};var as=Zo.map({"inside-out":function(n){var t,e,r=n.length,u=n.map(ui),i=n.map(ii),o=Zo.range(r).sort(function(n,t){return u[n]-u[t]}),a=0,c=0,s=[],l=[];for(t=0;r>t;++t)e=o[t],c>a?(a+=i[e],s.push(e)):(c+=i[e],l.push(e));return l.reverse().concat(s)},reverse:function(n){return Zo.range(n.length).reverse()},"default":ei}),cs=Zo.map({silhouette:function(n){var t,e,r,u=n.length,i=n[0].length,o=[],a=0,c=[];for(e=0;i>e;++e){for(t=0,r=0;u>t;t++)r+=n[t][e][1];r>a&&(a=r),o.push(r)}for(e=0;i>e;++e)c[e]=(a-o[e])/2;return c},wiggle:function(n){var t,e,r,u,i,o,a,c,s,l=n.length,f=n[0],h=f.length,g=[];for(g[0]=c=s=0,e=1;h>e;++e){for(t=0,u=0;l>t;++t)u+=n[t][e][1];for(t=0,i=0,a=f[e][0]-f[e-1][0];l>t;++t){for(r=0,o=(n[t][e][1]-n[t][e-1][1])/(2*a);t>r;++r)o+=(n[r][e][1]-n[r][e-1][1])/a;i+=o*n[t][e][1]}g[e]=c-=u?i/u*a:0,s>c&&(s=c)}for(e=0;h>e;++e)g[e]-=s;return g},expand:function(n){var t,e,r,u=n.length,i=n[0].length,o=1/u,a=[];for(e=0;i>e;++e){for(t=0,r=0;u>t;t++)r+=n[t][e][1];if(r)for(t=0;u>t;t++)n[t][e][1]/=r;else for(t=0;u>t;t++)n[t][e][1]=o}for(e=0;i>e;++e)a[e]=0;return a},zero:ri});Zo.layout.histogram=function(){function n(n,i){for(var o,a,c=[],s=n.map(e,this),l=r.call(this,s,i),f=u.call(this,l,s,i),i=-1,h=s.length,g=f.length-1,p=t?1:1/h;++i<g;)o=c[i]=[],o.dx=f[i+1]-(o.x=f[i]),o.y=0;if(g>0)for(i=-1;++i<h;)a=s[i],a>=l[0]&&a<=l[1]&&(o=c[Zo.bisect(f,a,1,g)-1],o.y+=p,o.push(n[i]));return c}var t=!0,e=Number,r=si,u=ai;return n.value=function(t){return arguments.length?(e=t,n):e},n.range=function(t){return arguments.length?(r=bt(t),n):r},n.bins=function(t){return arguments.length?(u="number"==typeof t?function(n){return ci(n,t)}:bt(t),n):u},n.frequency=function(e){return arguments.length?(t=!!e,n):t},n},Zo.layout.pack=function(){function n(n,i){var o=e.call(this,n,i),a=o[0],c=u[0],s=u[1],l=null==t?Math.sqrt:"function"==typeof t?t:function(){return t};if(a.x=a.y=0,Bu(a,function(n){n.r=+l(n.value)}),Bu(a,pi),r){var f=r*(t?1:Math.max(2*a.r/c,2*a.r/s))/2;Bu(a,function(n){n.r+=f}),Bu(a,pi),Bu(a,function(n){n.r-=f})}return mi(a,c/2,s/2,t?1:1/Math.max(2*a.r/c,2*a.r/s)),o}var t,e=Zo.layout.hierarchy().sort(li),r=0,u=[1,1];return n.size=function(t){return arguments.length?(u=t,n):u},n.radius=function(e){return arguments.length?(t=null==e||"function"==typeof e?e:+e,n):t},n.padding=function(t){return arguments.length?(r=+t,n):r},Xu(n,e)},Zo.layout.tree=function(){function n(n,u){var l=o.call(this,n,u),f=l[0],h=t(f);if(Bu(h,e),h.parent.m=-h.z,$u(h,r),s)$u(f,i);else{var g=f,p=f,v=f;$u(f,function(n){n.x<g.x&&(g=n),n.x>p.x&&(p=n),n.depth>v.depth&&(v=n)});var d=a(g,p)/2-g.x,m=c[0]/(p.x+a(p,g)/2+d),y=c[1]/(v.depth||1);$u(f,function(n){n.x=(n.x+d)*m,n.y=n.depth*y})}return l}function t(n){for(var t,e={A:null,children:[n]},r=[e];null!=(t=r.pop());)for(var u,i=t.children,o=0,a=i.length;a>o;++o)r.push((i[o]=u={_:i[o],parent:t,children:(u=i[o].children)&&u.slice()||[],A:null,a:null,z:0,m:0,c:0,s:0,t:null,i:o}).a=u);return e.children[0]}function e(n){var t=n.children,e=n.parent.children,r=n.i?e[n.i-1]:null;if(t.length){wi(n);var i=(t[0].z+t[t.length-1].z)/2;r?(n.z=r.z+a(n._,r._),n.m=n.z-i):n.z=i}else r&&(n.z=r.z+a(n._,r._));n.parent.A=u(n,r,n.parent.A||e[0])}function r(n){n._.x=n.z+n.parent.m,n.m+=n.parent.m}function u(n,t,e){if(t){for(var r,u=n,i=n,o=t,c=u.parent.children[0],s=u.m,l=i.m,f=o.m,h=c.m;o=_i(o),u=Mi(u),o&&u;)c=Mi(c),i=_i(i),i.a=n,r=o.z+f-u.z-s+a(o._,u._),r>0&&(bi(Si(o,n,e),n,r),s+=r,l+=r),f+=o.m,s+=u.m,h+=c.m,l+=i.m;o&&!_i(i)&&(i.t=o,i.m+=f-l),u&&!Mi(c)&&(c.t=u,c.m+=s-h,e=n)}return e}function i(n){n.x*=c[0],n.y=n.depth*c[1]}var o=Zo.layout.hierarchy().sort(null).value(null),a=xi,c=[1,1],s=null;return n.separation=function(t){return arguments.length?(a=t,n):a},n.size=function(t){return arguments.length?(s=null==(c=t)?i:null,n):s?null:c},n.nodeSize=function(t){return arguments.length?(s=null==(c=t)?null:i,n):s?c:null},Xu(n,o)},Zo.layout.cluster=function(){function n(n,i){var o,a=t.call(this,n,i),c=a[0],s=0;Bu(c,function(n){var t=n.children;t&&t.length?(n.x=Ei(t),n.y=ki(t)):(n.x=o?s+=e(n,o):0,n.y=0,o=n)});var l=Ai(c),f=Ci(c),h=l.x-e(l,f)/2,g=f.x+e(f,l)/2;return Bu(c,u?function(n){n.x=(n.x-c.x)*r[0],n.y=(c.y-n.y)*r[1]}:function(n){n.x=(n.x-h)/(g-h)*r[0],n.y=(1-(c.y?n.y/c.y:1))*r[1]}),a}var t=Zo.layout.hierarchy().sort(null).value(null),e=xi,r=[1,1],u=!1;return n.separation=function(t){return arguments.length?(e=t,n):e},n.size=function(t){return arguments.length?(u=null==(r=t),n):u?null:r},n.nodeSize=function(t){return arguments.length?(u=null!=(r=t),n):u?r:null},Xu(n,t)},Zo.layout.treemap=function(){function n(n,t){for(var e,r,u=-1,i=n.length;++u<i;)r=(e=n[u]).value*(0>t?0:t),e.area=isNaN(r)||0>=r?0:r}function t(e){var i=e.children;if(i&&i.length){var o,a,c,s=f(e),l=[],h=i.slice(),p=1/0,v="slice"===g?s.dx:"dice"===g?s.dy:"slice-dice"===g?1&e.depth?s.dy:s.dx:Math.min(s.dx,s.dy);for(n(h,s.dx*s.dy/e.value),l.area=0;(c=h.length)>0;)l.push(o=h[c-1]),l.area+=o.area,"squarify"!==g||(a=r(l,v))<=p?(h.pop(),p=a):(l.area-=l.pop().area,u(l,v,s,!1),v=Math.min(s.dx,s.dy),l.length=l.area=0,p=1/0);l.length&&(u(l,v,s,!0),l.length=l.area=0),i.forEach(t)}}function e(t){var r=t.children;if(r&&r.length){var i,o=f(t),a=r.slice(),c=[];for(n(a,o.dx*o.dy/t.value),c.area=0;i=a.pop();)c.push(i),c.area+=i.area,null!=i.z&&(u(c,i.z?o.dx:o.dy,o,!a.length),c.length=c.area=0);r.forEach(e)}}function r(n,t){for(var e,r=n.area,u=0,i=1/0,o=-1,a=n.length;++o<a;)(e=n[o].area)&&(i>e&&(i=e),e>u&&(u=e));return r*=r,t*=t,r?Math.max(t*u*p/r,r/(t*i*p)):1/0}function u(n,t,e,r){var u,i=-1,o=n.length,a=e.x,s=e.y,l=t?c(n.area/t):0;if(t==e.dx){for((r||l>e.dy)&&(l=e.dy);++i<o;)u=n[i],u.x=a,u.y=s,u.dy=l,a+=u.dx=Math.min(e.x+e.dx-a,l?c(u.area/l):0);u.z=!0,u.dx+=e.x+e.dx-a,e.y+=l,e.dy-=l}else{for((r||l>e.dx)&&(l=e.dx);++i<o;)u=n[i],u.x=a,u.y=s,u.dx=l,s+=u.dy=Math.min(e.y+e.dy-s,l?c(u.area/l):0);u.z=!1,u.dy+=e.y+e.dy-s,e.x+=l,e.dx-=l}}function i(r){var u=o||a(r),i=u[0];return i.x=0,i.y=0,i.dx=s[0],i.dy=s[1],o&&a.revalue(i),n([i],i.dx*i.dy/i.value),(o?e:t)(i),h&&(o=u),u}var o,a=Zo.layout.hierarchy(),c=Math.round,s=[1,1],l=null,f=Ni,h=!1,g="squarify",p=.5*(1+Math.sqrt(5));return i.size=function(n){return arguments.length?(s=n,i):s},i.padding=function(n){function t(t){var e=n.call(i,t,t.depth);return null==e?Ni(t):zi(t,"number"==typeof e?[e,e,e,e]:e)}function e(t){return zi(t,n)}if(!arguments.length)return l;var r;return f=null==(l=n)?Ni:"function"==(r=typeof n)?t:"number"===r?(n=[n,n,n,n],e):e,i},i.round=function(n){return arguments.length?(c=n?Math.round:Number,i):c!=Number},i.sticky=function(n){return arguments.length?(h=n,o=null,i):h},i.ratio=function(n){return arguments.length?(p=n,i):p},i.mode=function(n){return arguments.length?(g=n+"",i):g},Xu(i,a)},Zo.random={normal:function(n,t){var e=arguments.length;return 2>e&&(t=1),1>e&&(n=0),function(){var e,r,u;do e=2*Math.random()-1,r=2*Math.random()-1,u=e*e+r*r;while(!u||u>1);return n+t*e*Math.sqrt(-2*Math.log(u)/u)}},logNormal:function(){var n=Zo.random.normal.apply(Zo,arguments);return function(){return Math.exp(n())}},bates:function(n){var t=Zo.random.irwinHall(n);return function(){return t()/n}},irwinHall:function(n){return function(){for(var t=0,e=0;n>e;e++)t+=Math.random();return t}}},Zo.scale={};var ss={floor:wt,ceil:wt};Zo.scale.linear=function(){return Ui([0,1],[0,1],hu,!1)};var ls={s:1,g:1,p:1,r:1,e:1};Zo.scale.log=function(){return Vi(Zo.scale.linear().domain([0,1]),10,!0,[1,10])};var fs=Zo.format(".0e"),hs={floor:function(n){return-Math.ceil(-n)},ceil:function(n){return-Math.floor(-n)}};Zo.scale.pow=function(){return Xi(Zo.scale.linear(),1,[0,1])},Zo.scale.sqrt=function(){return Zo.scale.pow().exponent(.5)},Zo.scale.ordinal=function(){return Bi([],{t:"range",a:[[]]})},Zo.scale.category10=function(){return Zo.scale.ordinal().range(gs)},Zo.scale.category20=function(){return Zo.scale.ordinal().range(ps)},Zo.scale.category20b=function(){return Zo.scale.ordinal().range(vs)},Zo.scale.category20c=function(){return Zo.scale.ordinal().range(ds)};var gs=[2062260,16744206,2924588,14034728,9725885,9197131,14907330,8355711,12369186,1556175].map(vt),ps=[2062260,11454440,16744206,16759672,2924588,10018698,14034728,16750742,9725885,12955861,9197131,12885140,14907330,16234194,8355711,13092807,12369186,14408589,1556175,10410725].map(vt),vs=[3750777,5395619,7040719,10264286,6519097,9216594,11915115,13556636,9202993,12426809,15186514,15190932,8666169,11356490,14049643,15177372,8077683,10834324,13528509,14589654].map(vt),ds=[3244733,7057110,10406625,13032431,15095053,16616764,16625259,16634018,3253076,7652470,10607003,13101504,7695281,10394312,12369372,14342891,6513507,9868950,12434877,14277081].map(vt);Zo.scale.quantile=function(){return Wi([],[])},Zo.scale.quantize=function(){return Ji(0,1,[0,1])},Zo.scale.threshold=function(){return Gi([.5],[0,1])},Zo.scale.identity=function(){return Ki([0,1])},Zo.svg={},Zo.svg.arc=function(){function n(){var n=t.apply(this,arguments),i=e.apply(this,arguments),o=r.apply(this,arguments)+ms,a=u.apply(this,arguments)+ms,c=(o>a&&(c=o,o=a,a=c),a-o),s=ba>c?"0":"1",l=Math.cos(o),f=Math.sin(o),h=Math.cos(a),g=Math.sin(a); +return c>=ys?n?"M0,"+i+"A"+i+","+i+" 0 1,1 0,"+-i+"A"+i+","+i+" 0 1,1 0,"+i+"M0,"+n+"A"+n+","+n+" 0 1,0 0,"+-n+"A"+n+","+n+" 0 1,0 0,"+n+"Z":"M0,"+i+"A"+i+","+i+" 0 1,1 0,"+-i+"A"+i+","+i+" 0 1,1 0,"+i+"Z":n?"M"+i*l+","+i*f+"A"+i+","+i+" 0 "+s+",1 "+i*h+","+i*g+"L"+n*h+","+n*g+"A"+n+","+n+" 0 "+s+",0 "+n*l+","+n*f+"Z":"M"+i*l+","+i*f+"A"+i+","+i+" 0 "+s+",1 "+i*h+","+i*g+"L0,0"+"Z"}var t=Qi,e=no,r=to,u=eo;return n.innerRadius=function(e){return arguments.length?(t=bt(e),n):t},n.outerRadius=function(t){return arguments.length?(e=bt(t),n):e},n.startAngle=function(t){return arguments.length?(r=bt(t),n):r},n.endAngle=function(t){return arguments.length?(u=bt(t),n):u},n.centroid=function(){var n=(t.apply(this,arguments)+e.apply(this,arguments))/2,i=(r.apply(this,arguments)+u.apply(this,arguments))/2+ms;return[Math.cos(i)*n,Math.sin(i)*n]},n};var ms=-Sa,ys=wa-ka;Zo.svg.line=function(){return ro(wt)};var xs=Zo.map({linear:uo,"linear-closed":io,step:oo,"step-before":ao,"step-after":co,basis:po,"basis-open":vo,"basis-closed":mo,bundle:yo,cardinal:fo,"cardinal-open":so,"cardinal-closed":lo,monotone:So});xs.forEach(function(n,t){t.key=n,t.closed=/-closed$/.test(n)});var Ms=[0,2/3,1/3,0],_s=[0,1/3,2/3,0],bs=[0,1/6,2/3,1/6];Zo.svg.line.radial=function(){var n=ro(ko);return n.radius=n.x,delete n.x,n.angle=n.y,delete n.y,n},ao.reverse=co,co.reverse=ao,Zo.svg.area=function(){return Eo(wt)},Zo.svg.area.radial=function(){var n=Eo(ko);return n.radius=n.x,delete n.x,n.innerRadius=n.x0,delete n.x0,n.outerRadius=n.x1,delete n.x1,n.angle=n.y,delete n.y,n.startAngle=n.y0,delete n.y0,n.endAngle=n.y1,delete n.y1,n},Zo.svg.chord=function(){function n(n,a){var c=t(this,i,n,a),s=t(this,o,n,a);return"M"+c.p0+r(c.r,c.p1,c.a1-c.a0)+(e(c,s)?u(c.r,c.p1,c.r,c.p0):u(c.r,c.p1,s.r,s.p0)+r(s.r,s.p1,s.a1-s.a0)+u(s.r,s.p1,c.r,c.p0))+"Z"}function t(n,t,e,r){var u=t.call(n,e,r),i=a.call(n,u,r),o=c.call(n,u,r)+ms,l=s.call(n,u,r)+ms;return{r:i,a0:o,a1:l,p0:[i*Math.cos(o),i*Math.sin(o)],p1:[i*Math.cos(l),i*Math.sin(l)]}}function e(n,t){return n.a0==t.a0&&n.a1==t.a1}function r(n,t,e){return"A"+n+","+n+" 0 "+ +(e>ba)+",1 "+t}function u(n,t,e,r){return"Q 0,0 "+r}var i=gr,o=pr,a=Ao,c=to,s=eo;return n.radius=function(t){return arguments.length?(a=bt(t),n):a},n.source=function(t){return arguments.length?(i=bt(t),n):i},n.target=function(t){return arguments.length?(o=bt(t),n):o},n.startAngle=function(t){return arguments.length?(c=bt(t),n):c},n.endAngle=function(t){return arguments.length?(s=bt(t),n):s},n},Zo.svg.diagonal=function(){function n(n,u){var i=t.call(this,n,u),o=e.call(this,n,u),a=(i.y+o.y)/2,c=[i,{x:i.x,y:a},{x:o.x,y:a},o];return c=c.map(r),"M"+c[0]+"C"+c[1]+" "+c[2]+" "+c[3]}var t=gr,e=pr,r=Co;return n.source=function(e){return arguments.length?(t=bt(e),n):t},n.target=function(t){return arguments.length?(e=bt(t),n):e},n.projection=function(t){return arguments.length?(r=t,n):r},n},Zo.svg.diagonal.radial=function(){var n=Zo.svg.diagonal(),t=Co,e=n.projection;return n.projection=function(n){return arguments.length?e(No(t=n)):t},n},Zo.svg.symbol=function(){function n(n,r){return(ws.get(t.call(this,n,r))||To)(e.call(this,n,r))}var t=Lo,e=zo;return n.type=function(e){return arguments.length?(t=bt(e),n):t},n.size=function(t){return arguments.length?(e=bt(t),n):e},n};var ws=Zo.map({circle:To,cross:function(n){var t=Math.sqrt(n/5)/2;return"M"+-3*t+","+-t+"H"+-t+"V"+-3*t+"H"+t+"V"+-t+"H"+3*t+"V"+t+"H"+t+"V"+3*t+"H"+-t+"V"+t+"H"+-3*t+"Z"},diamond:function(n){var t=Math.sqrt(n/(2*As)),e=t*As;return"M0,"+-t+"L"+e+",0"+" 0,"+t+" "+-e+",0"+"Z"},square:function(n){var t=Math.sqrt(n)/2;return"M"+-t+","+-t+"L"+t+","+-t+" "+t+","+t+" "+-t+","+t+"Z"},"triangle-down":function(n){var t=Math.sqrt(n/Es),e=t*Es/2;return"M0,"+e+"L"+t+","+-e+" "+-t+","+-e+"Z"},"triangle-up":function(n){var t=Math.sqrt(n/Es),e=t*Es/2;return"M0,"+-e+"L"+t+","+e+" "+-t+","+e+"Z"}});Zo.svg.symbolTypes=ws.keys();var Ss,ks,Es=Math.sqrt(3),As=Math.tan(30*Aa),Cs=[],Ns=0;Cs.call=pa.call,Cs.empty=pa.empty,Cs.node=pa.node,Cs.size=pa.size,Zo.transition=function(n){return arguments.length?Ss?n.transition():n:ma.transition()},Zo.transition.prototype=Cs,Cs.select=function(n){var t,e,r,u=this.id,i=[];n=b(n);for(var o=-1,a=this.length;++o<a;){i.push(t=[]);for(var c=this[o],s=-1,l=c.length;++s<l;)(r=c[s])&&(e=n.call(r,r.__data__,s,o))?("__data__"in r&&(e.__data__=r.__data__),Po(e,s,u,r.__transition__[u]),t.push(e)):t.push(null)}return qo(i,u)},Cs.selectAll=function(n){var t,e,r,u,i,o=this.id,a=[];n=w(n);for(var c=-1,s=this.length;++c<s;)for(var l=this[c],f=-1,h=l.length;++f<h;)if(r=l[f]){i=r.__transition__[o],e=n.call(r,r.__data__,f,c),a.push(t=[]);for(var g=-1,p=e.length;++g<p;)(u=e[g])&&Po(u,g,o,i),t.push(u)}return qo(a,o)},Cs.filter=function(n){var t,e,r,u=[];"function"!=typeof n&&(n=R(n));for(var i=0,o=this.length;o>i;i++){u.push(t=[]);for(var e=this[i],a=0,c=e.length;c>a;a++)(r=e[a])&&n.call(r,r.__data__,a,i)&&t.push(r)}return qo(u,this.id)},Cs.tween=function(n,t){var e=this.id;return arguments.length<2?this.node().__transition__[e].tween.get(n):P(this,null==t?function(t){t.__transition__[e].tween.remove(n)}:function(r){r.__transition__[e].tween.set(n,t)})},Cs.attr=function(n,t){function e(){this.removeAttribute(a)}function r(){this.removeAttributeNS(a.space,a.local)}function u(n){return null==n?e:(n+="",function(){var t,e=this.getAttribute(a);return e!==n&&(t=o(e,n),function(n){this.setAttribute(a,t(n))})})}function i(n){return null==n?r:(n+="",function(){var t,e=this.getAttributeNS(a.space,a.local);return e!==n&&(t=o(e,n),function(n){this.setAttributeNS(a.space,a.local,t(n))})})}if(arguments.length<2){for(t in n)this.attr(t,n[t]);return this}var o="transform"==n?Du:hu,a=Zo.ns.qualify(n);return Ro(this,"attr."+n,t,a.local?i:u)},Cs.attrTween=function(n,t){function e(n,e){var r=t.call(this,n,e,this.getAttribute(u));return r&&function(n){this.setAttribute(u,r(n))}}function r(n,e){var r=t.call(this,n,e,this.getAttributeNS(u.space,u.local));return r&&function(n){this.setAttributeNS(u.space,u.local,r(n))}}var u=Zo.ns.qualify(n);return this.tween("attr."+n,u.local?r:e)},Cs.style=function(n,t,e){function r(){this.style.removeProperty(n)}function u(t){return null==t?r:(t+="",function(){var r,u=Wo.getComputedStyle(this,null).getPropertyValue(n);return u!==t&&(r=hu(u,t),function(t){this.style.setProperty(n,r(t),e)})})}var i=arguments.length;if(3>i){if("string"!=typeof n){2>i&&(t="");for(e in n)this.style(e,n[e],t);return this}e=""}return Ro(this,"style."+n,t,u)},Cs.styleTween=function(n,t,e){function r(r,u){var i=t.call(this,r,u,Wo.getComputedStyle(this,null).getPropertyValue(n));return i&&function(t){this.style.setProperty(n,i(t),e)}}return arguments.length<3&&(e=""),this.tween("style."+n,r)},Cs.text=function(n){return Ro(this,"text",n,Do)},Cs.remove=function(){return this.each("end.transition",function(){var n;this.__transition__.count<2&&(n=this.parentNode)&&n.removeChild(this)})},Cs.ease=function(n){var t=this.id;return arguments.length<1?this.node().__transition__[t].ease:("function"!=typeof n&&(n=Zo.ease.apply(Zo,arguments)),P(this,function(e){e.__transition__[t].ease=n}))},Cs.delay=function(n){var t=this.id;return arguments.length<1?this.node().__transition__[t].delay:P(this,"function"==typeof n?function(e,r,u){e.__transition__[t].delay=+n.call(e,e.__data__,r,u)}:(n=+n,function(e){e.__transition__[t].delay=n}))},Cs.duration=function(n){var t=this.id;return arguments.length<1?this.node().__transition__[t].duration:P(this,"function"==typeof n?function(e,r,u){e.__transition__[t].duration=Math.max(1,n.call(e,e.__data__,r,u))}:(n=Math.max(1,n),function(e){e.__transition__[t].duration=n}))},Cs.each=function(n,t){var e=this.id;if(arguments.length<2){var r=ks,u=Ss;Ss=e,P(this,function(t,r,u){ks=t.__transition__[e],n.call(t,t.__data__,r,u)}),ks=r,Ss=u}else P(this,function(r){var u=r.__transition__[e];(u.event||(u.event=Zo.dispatch("start","end"))).on(n,t)});return this},Cs.transition=function(){for(var n,t,e,r,u=this.id,i=++Ns,o=[],a=0,c=this.length;c>a;a++){o.push(n=[]);for(var t=this[a],s=0,l=t.length;l>s;s++)(e=t[s])&&(r=Object.create(e.__transition__[u]),r.delay+=r.duration,Po(e,s,i,r)),n.push(e)}return qo(o,i)},Zo.svg.axis=function(){function n(n){n.each(function(){var n,s=Zo.select(this),l=this.__chart__||e,f=this.__chart__=e.copy(),h=null==c?f.ticks?f.ticks.apply(f,a):f.domain():c,g=null==t?f.tickFormat?f.tickFormat.apply(f,a):wt:t,p=s.selectAll(".tick").data(h,f),v=p.enter().insert("g",".domain").attr("class","tick").style("opacity",ka),d=Zo.transition(p.exit()).style("opacity",ka).remove(),m=Zo.transition(p.order()).style("opacity",1),y=Ti(f),x=s.selectAll(".domain").data([0]),M=(x.enter().append("path").attr("class","domain"),Zo.transition(x));v.append("line"),v.append("text");var _=v.select("line"),b=m.select("line"),w=p.select("text").text(g),S=v.select("text"),k=m.select("text");switch(r){case"bottom":n=Uo,_.attr("y2",u),S.attr("y",Math.max(u,0)+o),b.attr("x2",0).attr("y2",u),k.attr("x",0).attr("y",Math.max(u,0)+o),w.attr("dy",".71em").style("text-anchor","middle"),M.attr("d","M"+y[0]+","+i+"V0H"+y[1]+"V"+i);break;case"top":n=Uo,_.attr("y2",-u),S.attr("y",-(Math.max(u,0)+o)),b.attr("x2",0).attr("y2",-u),k.attr("x",0).attr("y",-(Math.max(u,0)+o)),w.attr("dy","0em").style("text-anchor","middle"),M.attr("d","M"+y[0]+","+-i+"V0H"+y[1]+"V"+-i);break;case"left":n=jo,_.attr("x2",-u),S.attr("x",-(Math.max(u,0)+o)),b.attr("x2",-u).attr("y2",0),k.attr("x",-(Math.max(u,0)+o)).attr("y",0),w.attr("dy",".32em").style("text-anchor","end"),M.attr("d","M"+-i+","+y[0]+"H0V"+y[1]+"H"+-i);break;case"right":n=jo,_.attr("x2",u),S.attr("x",Math.max(u,0)+o),b.attr("x2",u).attr("y2",0),k.attr("x",Math.max(u,0)+o).attr("y",0),w.attr("dy",".32em").style("text-anchor","start"),M.attr("d","M"+i+","+y[0]+"H0V"+y[1]+"H"+i)}if(f.rangeBand){var E=f,A=E.rangeBand()/2;l=f=function(n){return E(n)+A}}else l.rangeBand?l=f:d.call(n,f);v.call(n,l),m.call(n,f)})}var t,e=Zo.scale.linear(),r=zs,u=6,i=6,o=3,a=[10],c=null;return n.scale=function(t){return arguments.length?(e=t,n):e},n.orient=function(t){return arguments.length?(r=t in Ls?t+"":zs,n):r},n.ticks=function(){return arguments.length?(a=arguments,n):a},n.tickValues=function(t){return arguments.length?(c=t,n):c},n.tickFormat=function(e){return arguments.length?(t=e,n):t},n.tickSize=function(t){var e=arguments.length;return e?(u=+t,i=+arguments[e-1],n):u},n.innerTickSize=function(t){return arguments.length?(u=+t,n):u},n.outerTickSize=function(t){return arguments.length?(i=+t,n):i},n.tickPadding=function(t){return arguments.length?(o=+t,n):o},n.tickSubdivide=function(){return arguments.length&&n},n};var zs="bottom",Ls={top:1,right:1,bottom:1,left:1};Zo.svg.brush=function(){function n(i){i.each(function(){var i=Zo.select(this).style("pointer-events","all").style("-webkit-tap-highlight-color","rgba(0,0,0,0)").on("mousedown.brush",u).on("touchstart.brush",u),o=i.selectAll(".background").data([0]);o.enter().append("rect").attr("class","background").style("visibility","hidden").style("cursor","crosshair"),i.selectAll(".extent").data([0]).enter().append("rect").attr("class","extent").style("cursor","move");var a=i.selectAll(".resize").data(p,wt);a.exit().remove(),a.enter().append("g").attr("class",function(n){return"resize "+n}).style("cursor",function(n){return Ts[n]}).append("rect").attr("x",function(n){return/[ew]$/.test(n)?-3:null}).attr("y",function(n){return/^[ns]/.test(n)?-3:null}).attr("width",6).attr("height",6).style("visibility","hidden"),a.style("display",n.empty()?"none":null);var l,f=Zo.transition(i),h=Zo.transition(o);c&&(l=Ti(c),h.attr("x",l[0]).attr("width",l[1]-l[0]),e(f)),s&&(l=Ti(s),h.attr("y",l[0]).attr("height",l[1]-l[0]),r(f)),t(f)})}function t(n){n.selectAll(".resize").attr("transform",function(n){return"translate("+l[+/e$/.test(n)]+","+f[+/^s/.test(n)]+")"})}function e(n){n.select(".extent").attr("x",l[0]),n.selectAll(".extent,.n>rect,.s>rect").attr("width",l[1]-l[0])}function r(n){n.select(".extent").attr("y",f[0]),n.selectAll(".extent,.e>rect,.w>rect").attr("height",f[1]-f[0])}function u(){function u(){32==Zo.event.keyCode&&(C||(x=null,z[0]-=l[1],z[1]-=f[1],C=2),y())}function p(){32==Zo.event.keyCode&&2==C&&(z[0]+=l[1],z[1]+=f[1],C=0,y())}function v(){var n=Zo.mouse(_),u=!1;M&&(n[0]+=M[0],n[1]+=M[1]),C||(Zo.event.altKey?(x||(x=[(l[0]+l[1])/2,(f[0]+f[1])/2]),z[0]=l[+(n[0]<x[0])],z[1]=f[+(n[1]<x[1])]):x=null),E&&d(n,c,0)&&(e(S),u=!0),A&&d(n,s,1)&&(r(S),u=!0),u&&(t(S),w({type:"brush",mode:C?"move":"resize"}))}function d(n,t,e){var r,u,a=Ti(t),c=a[0],s=a[1],p=z[e],v=e?f:l,d=v[1]-v[0];return C&&(c-=p,s-=d+p),r=(e?g:h)?Math.max(c,Math.min(s,n[e])):n[e],C?u=(r+=p)+d:(x&&(p=Math.max(c,Math.min(s,2*x[e]-r))),r>p?(u=r,r=p):u=p),v[0]!=r||v[1]!=u?(e?o=null:i=null,v[0]=r,v[1]=u,!0):void 0}function m(){v(),S.style("pointer-events","all").selectAll(".resize").style("display",n.empty()?"none":null),Zo.select("body").style("cursor",null),L.on("mousemove.brush",null).on("mouseup.brush",null).on("touchmove.brush",null).on("touchend.brush",null).on("keydown.brush",null).on("keyup.brush",null),N(),w({type:"brushend"})}var x,M,_=this,b=Zo.select(Zo.event.target),w=a.of(_,arguments),S=Zo.select(_),k=b.datum(),E=!/^(n|s)$/.test(k)&&c,A=!/^(e|w)$/.test(k)&&s,C=b.classed("extent"),N=I(),z=Zo.mouse(_),L=Zo.select(Wo).on("keydown.brush",u).on("keyup.brush",p);if(Zo.event.changedTouches?L.on("touchmove.brush",v).on("touchend.brush",m):L.on("mousemove.brush",v).on("mouseup.brush",m),S.interrupt().selectAll("*").interrupt(),C)z[0]=l[0]-z[0],z[1]=f[0]-z[1];else if(k){var T=+/w$/.test(k),q=+/^n/.test(k);M=[l[1-T]-z[0],f[1-q]-z[1]],z[0]=l[T],z[1]=f[q]}else Zo.event.altKey&&(x=z.slice());S.style("pointer-events","none").selectAll(".resize").style("display",null),Zo.select("body").style("cursor",b.style("cursor")),w({type:"brushstart"}),v()}var i,o,a=M(n,"brushstart","brush","brushend"),c=null,s=null,l=[0,0],f=[0,0],h=!0,g=!0,p=qs[0];return n.event=function(n){n.each(function(){var n=a.of(this,arguments),t={x:l,y:f,i:i,j:o},e=this.__chart__||t;this.__chart__=t,Ss?Zo.select(this).transition().each("start.brush",function(){i=e.i,o=e.j,l=e.x,f=e.y,n({type:"brushstart"})}).tween("brush:brush",function(){var e=gu(l,t.x),r=gu(f,t.y);return i=o=null,function(u){l=t.x=e(u),f=t.y=r(u),n({type:"brush",mode:"resize"})}}).each("end.brush",function(){i=t.i,o=t.j,n({type:"brush",mode:"resize"}),n({type:"brushend"})}):(n({type:"brushstart"}),n({type:"brush",mode:"resize"}),n({type:"brushend"}))})},n.x=function(t){return arguments.length?(c=t,p=qs[!c<<1|!s],n):c},n.y=function(t){return arguments.length?(s=t,p=qs[!c<<1|!s],n):s},n.clamp=function(t){return arguments.length?(c&&s?(h=!!t[0],g=!!t[1]):c?h=!!t:s&&(g=!!t),n):c&&s?[h,g]:c?h:s?g:null},n.extent=function(t){var e,r,u,a,h;return arguments.length?(c&&(e=t[0],r=t[1],s&&(e=e[0],r=r[0]),i=[e,r],c.invert&&(e=c(e),r=c(r)),e>r&&(h=e,e=r,r=h),(e!=l[0]||r!=l[1])&&(l=[e,r])),s&&(u=t[0],a=t[1],c&&(u=u[1],a=a[1]),o=[u,a],s.invert&&(u=s(u),a=s(a)),u>a&&(h=u,u=a,a=h),(u!=f[0]||a!=f[1])&&(f=[u,a])),n):(c&&(i?(e=i[0],r=i[1]):(e=l[0],r=l[1],c.invert&&(e=c.invert(e),r=c.invert(r)),e>r&&(h=e,e=r,r=h))),s&&(o?(u=o[0],a=o[1]):(u=f[0],a=f[1],s.invert&&(u=s.invert(u),a=s.invert(a)),u>a&&(h=u,u=a,a=h))),c&&s?[[e,u],[r,a]]:c?[e,r]:s&&[u,a])},n.clear=function(){return n.empty()||(l=[0,0],f=[0,0],i=o=null),n},n.empty=function(){return!!c&&l[0]==l[1]||!!s&&f[0]==f[1]},Zo.rebind(n,a,"on")};var Ts={n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},qs=[["n","e","s","w","nw","ne","se","sw"],["e","w"],["n","s"],[]],Rs=Qa.format=ic.timeFormat,Ds=Rs.utc,Ps=Ds("%Y-%m-%dT%H:%M:%S.%LZ");Rs.iso=Date.prototype.toISOString&&+new Date("2000-01-01T00:00:00.000Z")?Ho:Ps,Ho.parse=function(n){var t=new Date(n);return isNaN(t)?null:t},Ho.toString=Ps.toString,Qa.second=Dt(function(n){return new nc(1e3*Math.floor(n/1e3))},function(n,t){n.setTime(n.getTime()+1e3*Math.floor(t))},function(n){return n.getSeconds()}),Qa.seconds=Qa.second.range,Qa.seconds.utc=Qa.second.utc.range,Qa.minute=Dt(function(n){return new nc(6e4*Math.floor(n/6e4))},function(n,t){n.setTime(n.getTime()+6e4*Math.floor(t))},function(n){return n.getMinutes()}),Qa.minutes=Qa.minute.range,Qa.minutes.utc=Qa.minute.utc.range,Qa.hour=Dt(function(n){var t=n.getTimezoneOffset()/60;return new nc(36e5*(Math.floor(n/36e5-t)+t))},function(n,t){n.setTime(n.getTime()+36e5*Math.floor(t))},function(n){return n.getHours()}),Qa.hours=Qa.hour.range,Qa.hours.utc=Qa.hour.utc.range,Qa.month=Dt(function(n){return n=Qa.day(n),n.setDate(1),n},function(n,t){n.setMonth(n.getMonth()+t)},function(n){return n.getMonth()}),Qa.months=Qa.month.range,Qa.months.utc=Qa.month.utc.range;var Us=[1e3,5e3,15e3,3e4,6e4,3e5,9e5,18e5,36e5,108e5,216e5,432e5,864e5,1728e5,6048e5,2592e6,7776e6,31536e6],js=[[Qa.second,1],[Qa.second,5],[Qa.second,15],[Qa.second,30],[Qa.minute,1],[Qa.minute,5],[Qa.minute,15],[Qa.minute,30],[Qa.hour,1],[Qa.hour,3],[Qa.hour,6],[Qa.hour,12],[Qa.day,1],[Qa.day,2],[Qa.week,1],[Qa.month,1],[Qa.month,3],[Qa.year,1]],Hs=Rs.multi([[".%L",function(n){return n.getMilliseconds()}],[":%S",function(n){return n.getSeconds()}],["%I:%M",function(n){return n.getMinutes()}],["%I %p",function(n){return n.getHours()}],["%a %d",function(n){return n.getDay()&&1!=n.getDate()}],["%b %d",function(n){return 1!=n.getDate()}],["%B",function(n){return n.getMonth()}],["%Y",we]]),Fs={range:function(n,t,e){return Zo.range(Math.ceil(n/e)*e,+t,e).map(Oo)},floor:wt,ceil:wt};js.year=Qa.year,Qa.scale=function(){return Fo(Zo.scale.linear(),js,Hs)};var Os=js.map(function(n){return[n[0].utc,n[1]]}),Ys=Ds.multi([[".%L",function(n){return n.getUTCMilliseconds()}],[":%S",function(n){return n.getUTCSeconds()}],["%I:%M",function(n){return n.getUTCMinutes()}],["%I %p",function(n){return n.getUTCHours()}],["%a %d",function(n){return n.getUTCDay()&&1!=n.getUTCDate()}],["%b %d",function(n){return 1!=n.getUTCDate()}],["%B",function(n){return n.getUTCMonth()}],["%Y",we]]);Os.year=Qa.year.utc,Qa.scale.utc=function(){return Fo(Zo.scale.linear(),Os,Ys)},Zo.text=St(function(n){return n.responseText}),Zo.json=function(n,t){return kt(n,"application/json",Yo,t)},Zo.html=function(n,t){return kt(n,"text/html",Io,t)},Zo.xml=St(function(n){return n.responseXML}),"function"==typeof define&&define.amd?define(Zo):"object"==typeof module&&module.exports&&(module.exports=Zo),this.d3=Zo}(); \ No newline at end of file diff --git a/snf-admin-app/synnefo_admin/admin/static/js/details.js b/snf-admin-app/synnefo_admin/admin/static/js/details.js new file mode 100644 index 0000000000000000000000000000000000000000..d889a71a950206735d8a6738e51c88ce29d000a4 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/js/details.js @@ -0,0 +1,167 @@ +$(document).ready(function(){ + + var navsHeight = $('.main-nav').height() + $('.sub-nav').height(); + $('.sub-nav .link-to-anchor').click(function(e) { + e.preventDefault(); + var pos = $($.attr(this, 'href')).offset().top - navsHeight; + $('html, body').animate({ + scrollTop: pos + }, 500) + }) + $('.object-details h4 .arrow').click(function(){ + var $expandBtn = $(this); + var hasNotClass = !$expandBtn.closest('h4').hasClass('expanded'); + $expandBtn.closest('h4').toggleClass('expanded'); + + if(hasNotClass) { + $expandBtn.removeClass('snf-angle-down').addClass('snf-angle-up'); + $expandBtn.closest('h4').siblings('.object-details-content').stop().slideDown('slow'); + } + else { + $expandBtn.removeClass('snf-angle-up').addClass('snf-angle-down') + $expandBtn.closest('h4').siblings('.object-details-content').stop().slideUp('slow'); + } + + var $areas = $expandBtn.closest('.info-block.object-details') // *** add another class + + var allSameClass = true; + + ($areas.find('.object-details')).each(function() { + if(hasNotClass){ + allSameClass = allSameClass && $(this).find('h4').hasClass('expanded'); + } + else{ + allSameClass = allSameClass && !$(this).find('h4').hasClass('expanded'); + } + + if(!allSameClass){ + return false; + } + }); + var $toggleAllBtn = $expandBtn.closest('.info-block.object-details').find('.show-hide-all'); + if(allSameClass){ + if($expandBtn.closest('h4').hasClass('expanded')){ + $toggleAllBtn.addClass('open'); + $toggleAllBtn.find('.txt').text(txt_all[1]); + } + else { + $toggleAllBtn.removeClass('open'); + $toggleAllBtn.find('.txt').text(txt_all[0]); + } + } + else { + $toggleAllBtn.removeClass('open'); + $toggleAllBtn.find('.txt').text(txt_all[0]); + } + }); + + // hide/show expand/collapse + + + var txt_all = ['Expand all','Collapse all']; + + + $('.show-hide-all span.txt').text(txt_all[0]); + + + $('.show-hide-all').click(function(e){ + e.preventDefault(); + $(this).toggleClass('open'); + var tabs = $(this).parent('.info-block').find('.object-details-content'); + + if ($(this).hasClass('open')){ + $(this).find('span.txt').text( txt_all[1]); + tabs.each(function() { + $(this).stop().slideDown('slow'); + $(this).siblings('h4').addClass('expanded'); + $(this).siblings('h4').find('.arrow').removeClass('snf-angle-down').addClass('snf-angle-up') + }); + + + } else { + $(this).find('span.txt').text( txt_all[0]); + tabs.each(function() { + $(this).stop().slideUp('slow'); + $(this).siblings('h4').removeClass('expanded'); + $(this).siblings('h4').find('.arrow').removeClass('snf-angle-up').addClass('snf-angle-down') + }); + } + }); + +$('.main .object-details h4 .arrow').trigger('click') + + /* Modals */ + + $('.actions-per-item .custom-btn').click(function() { + var itemID = $(this).closest('.object-details').data('id'); + var itemName = $(this).closest('.object-details').find('h4 .title').text(); + var modalID = $(this).data('target'); + drawModalSingleItem(modalID, itemName, itemID); + }); + + function resetItemInfo(modal) { + var $modal = $(modal); + $modal.find('.summary .info-list').remove(); + } + + function drawModalSingleItem(modalID, itemName, itemID) { + var $summary = $(modalID).find('.modal-body .summary'); + var $actionBtn = $(modalID).find('.apply-action'); + var html = _.template(snf.modals.html.singleItemInfo); + + $actionBtn.attr('data-ids','['+itemID+']'); + $summary.append(html({name: itemName, id: itemID})); + }; + + + $('.modal').find('.cancel').click(function() { + $modal =$(this).closest('.modal'); + snf.modals.resetInputs($modal); + snf.modals.resetErrors($modal); + resetItemInfo($modal); + $('[data-toggle="popover"]').popover('hide'); + + }); + + var $notificationArea = $('.notify'); + var countAction = 0; + $('.modal .apply-action').click(function(e) { + var $modal = $(this).closest('.modal'); + var noError = true; + if($modal.attr('data-type') === 'contact') { + noError = snf.modals.validateContactForm($modal); + } + if(!noError) { + e.preventDefault(); + e.stopPropagation(); + } + else { + snf.modals.performAction($modal, $notificationArea, snf.modals.html.notifyRefreshPage, 0, countAction); + snf.modals.resetInputs($modal); + snf.modals.resetErrors($modal); + resetItemInfo($modal); + $('[data-toggle="popover"]').popover('hide'); + countAction++; + } + }); + + setDropdownHeight(); +}); + +$(window).resize(function(){ + setDropdownHeight(); +}) + +function setDropdownHeight() { + var mainNavH = $('.navbar-default').height(); + var subNavH = $('.sub-nav').height(); + var windowH = $(window).height(); + // 20 is the distance from the bottom of the page so that + // the dropdown does not collapse with the window + var res = windowH - (mainNavH + subNavH) - 20; + $('.dropdown-menu').each(function(){ + $(this).css('max-height', res); + }); +} + + diff --git a/snf-admin-app/synnefo_admin/admin/static/js/filters.js b/snf-admin-app/synnefo_admin/admin/static/js/filters.js new file mode 100644 index 0000000000000000000000000000000000000000..173f10e76c48190b821864c3ad4c889a953e0fbf --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/js/filters.js @@ -0,0 +1,592 @@ +$(document).ready(function() { + + var tableDomID = '#table-items-total'; + + var filtersInfo = {}; // stores the type of each filter + var tempFilters = {}; // use for filtering from compact view + var filtersResetValue = {}; // the values are stored in upper case + var filtersValidValues = {}; // the values are stored in upper case + var enabledFilters = []; // visible filters on standard view + var cookie_name = 'filters_'+ $('.table-items').attr('data-content'); + + /* Extract type and valid keys and values for filtersInfo, filtersResetValue, filtersValidValues */ + $('.filters').find('.filter').each(function(index) { + if(!$(this).hasClass('compact-filter') && !$(this).hasClass('filters-list')) { + var key = $(this).attr('data-filter'); + var type; // possible values: 'singe-choice', 'multi-choice', 'text' + var resetValue; + if($(this).hasClass('filter-dropdown')) { + type = ($(this).hasClass('filter-boolean')? 'single-choice' : 'multi-choice'); + resetValue = $(this).find('li.reset').text().toUpperCase(); + filtersResetValue[key] = resetValue; + filtersValidValues[key] = []; + $(this).find('li:not(.divider)').each(function() { + filtersValidValues[key].push($(this).text().toUpperCase()); + }); + } + else { + type = 'text'; + } + filtersInfo[key] = type; + } + }); + + + /* Standard View Functionality */ + + function dropdownSelect(filterElem) { + var $dropdownList = $(filterElem).find('.choices'); + $dropdownList.find('li a').click(function(e) { + e.preventDefault(); + e.stopPropagation(); + var $currentFilter = $(this).closest('.filter'); + var $li = $(this).closest('li'); + + if($(this).closest('.filter-dropdown').hasClass('filter-boolean')) { + if(!$li.hasClass('active')) { + $li.addClass('active'); + $li.siblings('.active').removeClass('active'); + } + } + // multichoice filter + else { + if($li.hasClass('reset')) { + $li.addClass('active'); + $li.siblings('.active').removeClass('active'); + } + else { + $li.toggleClass('active'); + if($li.hasClass('active')) { + $li.siblings('.reset').removeClass('active'); + } + // deselect a choice + else { + //if there is no other selected value the reset value is checked + if($li.siblings('.active').length === 0) { + $li.siblings('li.reset').addClass('active'); + } + } + } + } + execChoiceFiltering($currentFilter); + }); + }; + + function execChoiceFiltering($filter) { + var $filterSelections = $filter.find('ul li.active'); + var key = $filter.attr('data-filter'); + var value = []; + snf.filters[key] = []; + if($filterSelections.length === 1) { + if($filterSelections.hasClass('reset')) { + delete snf.filters[key] + } + else if($filter.hasClass('filter-boolean')) { + snf.filters[key] = $filterSelections.text(); + } + else { + value.push($filterSelections.text()); + snf.filters[key] = snf.filters[key].concat(value); + } + } + else { + $filterSelections.each(function() { + value.push($(this).text()); + }); + snf.filters[key] = snf.filters[key].concat(value); + } + $(tableDomID).dataTable().api().ajax.reload(); + }; + + + function textFilter(extraSearch) { + snf.timer = 0; + var $input = $(extraSearch).find('input'); + + $input.keyup(function(e) { + // if enter or space is pressed do nothing + if(e.which !== 32 && e.which !== 13) { + var key, value, pastValue, valuHasChanged; + key = $(this).data('filter'); + value = $.trim($(this).val()); + if(snf.filters[key]) { + pastValue = snf.filters[key]; + } + else { + pastValue = undefined; + } + snf.filters[key] = value; + if (snf.filters[key] === '') { + delete snf.filters[key]; + } + valueHasChanged = snf.filters[key] !== pastValue; + if(valueHasChanged) { + if(snf.timer === 0) { + snf.timer = 1; + setTimeout(function() { + $(tableDomID).dataTable().api().ajax.reload(); + snf.timer = 0; + }, snf.ajaxdelay) + } + } + } + }) + }; + + textFilter('.filter-text'); + dropdownSelect('.filters .filter-dropdown'); // every dropdown filter (.filters-list not included) + + /* Choose which filters will be visible */ + + /* Each click should display the filter, add it to enabledFilters array and add to the corresponding cookie */ + + $('.filters-list').on('click', '.choices li a', function(e) { + var $li = $(this).closest('li'); + if($li.hasClass('reset') && $li.find('span').hasClass('snf-checkbox-unchecked')) { + $li.addClass('active'); + $li.siblings('li:not(.reset):not(.divider)').removeClass('active'); + showAllFilters(); + } + else if(!$li.hasClass('reset')) { + $li.toggleClass('active'); + if($li.hasClass('active')) { + if($li.siblings('.reset').hasClass('active')) { + $li.siblings('.reset').removeClass('active'); + hideAllFilters(); + } + showFilter($li.attr('data-filter-name')); + } + else { + hideFilter($li.attr('data-filter-name')); + } + } + }); + + /* show the selected values of a choice-filter */ + $('.filters .filter .dropdown').on('hide.bs.dropdown', function() { + showSelections($(this).closest('.filter')); + }); + + /* Every time the list of available filters appears the proper li elements are checked */ + $('.filters .filters-list #select-filters').on('shown.bs.popover', function() { + showSelectedFilters(); + }); + + function showSelections($filter) { + var selectedFilterstext = ''; + var selectedFiltersLabels = []; + $filter.find('.choices .active').each(function() { + selectedFiltersLabels.push($(this).text()); + }); + selectedFilterstext = selectedFiltersLabels.toString().replace(/,/g, ', '); + $filter.find('.selected-value').text(selectedFilterstext); + }; + + /* Display filters onload */ + (function showFiltersOnLoad() { + var enabledFiltersNum; + if($.cookie(cookie_name)) { + enabledFilters = $.cookie(cookie_name).split(','); + enabledFiltersNum = enabledFilters.length; + if(enabledFilters[0] === 'all-filters') { + $('.filters .filter:not(.compact-filter)').addClass('visible-filter selected'); + } + else { + for(var i=0; i<enabledFiltersNum; i++) { + $('.filters').find('.filter[data-filter='+enabledFilters[i]+']').addClass('visible-filter selected'); + } + } + + } + else { + /* by default the first 2 filters get enabled */ + $('.filters .filter:not(.filters-list)').each(function() { + var show = false; + var filter; + if($(this).index('.filter:not(.filters-list)') === 0 || $(this).index('.filter:not(.filters-list)') === 1) { + show = true; + filter = $(this).attr('data-filter'); + } + if(show) { + showFilter(filter); + } + }); + } + })(); + + function showSelectedFilters() { + var filtersNum = enabledFilters.length; + $('.filters-list .choices li').removeClass('active'); + for(var i=0; i<filtersNum; i++) { + $('.filters-list').find('[data-filter-name='+enabledFilters[i]+']').addClass('active'); + } + }; + + function resetFilterTextView($filter) { + $filter.find('input').val(''); + }; + + function resetFilterChoiceView($filter) { + var resetLabel = $filter.find('.reset').text(); + $filter.find('.active').removeClass('active'); + $filter.find('.reset').addClass('active'); + $filter.find('.selected-value').text(resetLabel); + }; + + function showAllFilters() { + $('.filters .filter:not(.compact-filter)').addClass('visible-filter selected'); + enabledFilters = []; + enabledFilters.push('all-filters'); + $.cookie(cookie_name, enabledFilters.toString()); + }; + + function hideAllFilters() { + $('.filters .filter:not(.filters-list)').attr('style', ''); + $('.filters .filter:not(.filters-list)').removeClass('visible-filter visible-filter-fade selected'); + enabledFilters = []; + $.cookie(cookie_name, ''); + }; + + function showFilter(attrFilter) { + $('.filters').find('.filter[data-filter='+attrFilter+']').addClass('visible-filter selected'); + enabledFilters.push(attrFilter) + $.cookie(cookie_name, enabledFilters.toString()); + }; + + function hideFilter(attrFilter) { + var index = enabledFilters.indexOf(attrFilter); + var $currentFilter = $('.filters').find('.filter[data-filter='+attrFilter+']'); + $currentFilter.removeClass('visible-filter visible-filter-fade selected'); + if(filtersInfo[attrFilter] === 'text') { + resetFilterTextView($currentFilter); + } + else { + if(!$currentFilter.find('.reset').hasClass('active')) { + resetFilterChoiceView($currentFilter); + } + + } + enabledFilters.splice(index, 1); + $.cookie(cookie_name, enabledFilters.toString()); + + delete snf.filters[attrFilter]; + $(tableDomID).dataTable().api().ajax.reload(); + }; + + /* Change Filters' View */ + + $('.search-mode input').click(function(e) { + e.stopPropagation(); + var $compact = $('.compact-filter'); + var $standard = $('.filter.selected, .filters-list'); + if($compact.is(':visible')) { + $compact.removeClass('visible-filter visible-filter-fade'); + $standard.addClass('visible-filter-fade'); + $standard.each(function() { + if(!$(this).hasClass('filters-list')) { + var filter = $(this).attr('data-filter'); + var $filterOption = $('.filters .filters-list li[data-filter-name='+filter+']'); + + if(!$filterOption.hasClass('active')) { + $filterOption.trigger('click'); + showSelections($filterOption.closest('.filter')); + } + } + }); + $standard.each(function() { + if($(this).hasClass('filter-text') && $(this).hasClass('visible-filter-fade')) { + $(this).find('input').focus(); + return false; } + }) + $.cookie('search_mode', 'standard'); + } + else { + $standard.removeClass('visible-filter visible-filter-fade'); + $compact.addClass('visible-filter-fade'); + standardToCompact(); + $.cookie('search_mode', 'compact'); + $('.compact-filter.visible-filter-fade').find('input').focus(); + } + + }); + + if(!$.cookie('search_mode')) { + $.cookie('search_mode', 'standard'); + } + else { + if($.cookie('search_mode') !== 'standard') { + $('.search-mode input').trigger('click'); + } + } + + + /* Tranfer the search terms of standard view to compact view */ + + function standardToCompact() { + var $advFilt = $('.filters').find('input[data-filter=compact]'); + var updated = true; + hideFilterError(); + $advFilt.val(filtersToString()); + }; + + function filtersToString() { + var text = ''; + var newTerm; + for(var prop in snf.filters) { + if(filtersInfo[prop] === 'text') { + newTerm = prop + ': ' + snf.filters[prop]; + if(text.length == 0) { + text = newTerm; + } + else { + text = text + ' ' + newTerm; + } + } + else { + newTerm = prop + ': ' + snf.filters[prop].toString(); + if(text.length === 0) { + text = newTerm; + } + else { + text = text + ' ' + newTerm; + } + } + } + + return text; + }; + + /* Compact View Functionality */ + + $('.filters .compact-filter input').keyup(function(e) { + if(e.which === 13) { + $('.exec-search').trigger('click'); + } + }); + + $('.filters .toggle-instructions').click(function (e) { + e.preventDefault(); + var that = this; + $(this).toggleClass('open'); + $(this).siblings('.content').stop().slideToggle(function() { + if($(that).hasClass('open') && $(this).css('display') === 'none') { + $(that).removeClass('open'); + } + }); + }); + + $('.exec-search').click(function(e) { + e.preventDefault(); + tempFilters = {}; + var text = $(this).siblings('.form-group').find('input').val().trim(); + hideFilterError(); + if(text.length > 0) { + var terms = text.split(' '); + var key = 'unknown', value; + var termsL = terms.length; + var keyIndex; + var lastkey; + var filterType; + var isKey = false; + for(var i=0; i<termsL; i++) { + terms[i] = terms[i].trim(); + for(var prop in filtersInfo) { + if(terms[i].substring(0, prop.length+1).toUpperCase() === prop.toUpperCase() + ':') { + key = prop; + value = terms[i].substring(prop.length + 1).trim(); + isKey = true; + break; + } + } + if(!isKey) { + value = terms[i]; + } + + if(!tempFilters[key]) { + tempFilters[key] = value; + } + else if(value.length > 0) { + tempFilters[key] = tempFilters[key] + ' ' + value; + } + isKey = false; + } + } + + if(!_.isEmpty(tempFilters)) { + for(var filter in tempFilters) { + for(var prop in filtersInfo) { + if(prop === filter && (filtersInfo[prop] === 'single-choice' || filtersInfo[prop] === 'multi-choice')) { + tempFilters[filter] = tempFilters[filter].replace(/\s*,\s*/g ,',').split(','); + break; + } + } + } + for(var prop in snf.filters) { + if(!_.has(tempFilters, prop) && !tempFilters['unknown']) { + delete snf.filters[prop]; + $(tableDomID).dataTable().api().ajax.reload(); + } + } + } + compactToStandard(); + }); + + function compactToStandard() { + var $choicesLi; + var valuesL; + var validValues = []; + var valid = true; + var temp; + if(_.isEmpty(tempFilters) && !_.isEmpty(snf.filters)) { + snf.filters = {}; + $(tableDomID).dataTable().api().ajax.reload(); + } + else { + if(tempFilters['unknown']) { + showFilterError(tempFilters['unknown']); + valid = false; + } + for(var prop in tempFilters) { + if(prop !== 'unknown') { + temp = checkValues(prop); + if(valid) { + valid = temp; + } + } + } + } + + // execution + if(valid) { + resetStandardFiltersView(); + triggerFiltering(); + showSelectedFilters(); + } + }; + + function triggerFiltering() { + var $choicesLi, valuesL; + var $filters = $('.filters') + for(var prop in tempFilters) { + if(prop !== 'unknown') { + $filters.find('.filter[data-filter="' + prop + '"]').addClass('selected'); + if(enabledFilters.indexOf('all-filters') === -1 && enabledFilters.indexOf(prop) === -1) { + enabledFilters.push(prop); + $.cookie(cookie_name, enabledFilters.toString()); + } + if(filtersInfo[prop] === 'text'){ + $filters.find('input[data-filter="' + prop + '"]').val(tempFilters[prop]); + $filters.find('input[data-filter="' + prop + '"]').trigger('keyup'); + } + else { + $choicesLi = $filters.find('.filter[data-filter="' + prop + '"] .choices').find('li'); + valuesL = tempFilters[prop].length; + for(var i=0; i<valuesL; i++) { // for each filter + $choicesLi.each(function() { + if(tempFilters[prop][i].toUpperCase() === $(this).text().toUpperCase()) { + if(!$(this).hasClass('active') || ($(this).hasClass('active')&& $(this).hasClass('reset'))) { + $(this).find('a').trigger('click'); + } + } + }); + showSelections($filters.find('.filter[data-filter="' + prop + '"]')); + } + } + } + } + }; + + function checkValues(key) { + var wrongTerm; + var isWrong = false; + if(filtersInfo[key] === 'text') { + if(tempFilters[key] === '') { + isWrong = true; + } + } + else if(!isWrong) { + var valuesUpperCased = $.map(tempFilters[key], function(item, index) { + return item.toUpperCase(); + }); + var valuesL = valuesUpperCased.length; + for(var i=0; i<valuesL; i++) { + if(filtersValidValues[key].indexOf(valuesUpperCased[i]) === -1) { + isWrong = true; + break; + } + } + if(!isWrong) { + if(valuesUpperCased.indexOf(filtersResetValue[key])!==-1 && tempFilters[key].length>1) { + isWrong = true; + } + else if(filtersInfo[key] === 'single-choice' && tempFilters[key].length > 1) { + isWrong = true; + } + } + } + if(isWrong) { + wrongTerm = key + ': ' + tempFilters[key].toString(); + showFilterError(wrongTerm); + delete tempFilters[key]; + } + return !isWrong; + }; + + function showFilterError(wrongTerm) { + var msg, addition, prevMsg; + $errorDescr = $('.compact-filter').find('.error-description'); + $errorSign = $('.compact-filter').find('.error-sign'); + if($errorDescr.text() === '') { + msg = 'Invalid search: "' + wrongTerm + '" is not valid.'; + } + else { + prevMsg = $errorDescr.text(); + addition = ', "' + wrongTerm + '" are not valid.'; + msg = prevMsg.replace('term:', 'terms:'); + msg = msg.replace(' are not valid.', addition); + msg = msg.replace(' is not valid.', addition); + } + $errorDescr.text(msg); + $errorSign.css('opacity', 1) + }; + + function hideFilterError() { + $('.compact-filter').find('.error-sign').css('opacity', 0); + $('.compact-filter').find('.error-description').text(''); + }; + + function resetStandardFiltersView() { + $('.filters .filter-dropdown').each(function() { + $(this).find('li.reset').each(function() { + if(!$(this).hasClass('active')) { + $(this).addClass('active'); + } + }); + $(this).find('li:not(.reset)').each(function() { + if($(this).hasClass('active')) { + $(this).removeClass('active'); + } + }); + showSelections($(this).closest('.filter')); + }); + + $('.filters .filter-text').find('input').each(function() { + if($(this).val().length !== 0) { + $(this).val(''); + } + }); + }; + $('.filter-text.visible-filter').first().find('input').focus(); + $('.compact-filter.visible-filter-fade').find('input').focus(); + + var filtersListHTML = $('#select-filters').attr('popover-content'); + + $('#select-filters').popover({ + trigger: 'click', + html: true, + content: filtersListHTML, + placement: 'bottom', + }); + + $('#select-filters').attr('title', $('#select-filters').attr('link-title')); +}); diff --git a/snf-admin-app/synnefo_admin/admin/static/js/ie7.js b/snf-admin-app/synnefo_admin/admin/static/js/ie7.js new file mode 100644 index 0000000000000000000000000000000000000000..0936ff73e24b73a5693430cd4192dfb73f31bb4a --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/js/ie7.js @@ -0,0 +1,38 @@ +/* To avoid CSS expressions while still supporting IE 7 and IE 6, use this script */ +/* The script tag referring to this file must be placed before the ending body tag. */ + +/* Use conditional comments in order to target IE 7 and older: + <!--[if lt IE 8]><!--> + <script src="ie7/ie7.js"></script> + <!--<![endif]--> +*/ + +(function() { + function addIcon(el, entity) { + var html = el.innerHTML; + el.innerHTML = '<span style="font-family: \'font-icons\'">' + entity + '</span>' + html; + } + var icons = { + 'snf-envelope': 'c', + 'snf-ok': 'a', + 'snf-remove': 'b', + 'snf-exclamation-sign': 'g', + 'snf-envelope-alt': 'd', + 'snf-angle-up': 'e', + 'snf-angle-down': 'f', + '0': 0 + }, + els = document.getElementsByTagName('*'), + i, c, el; + for (i = 0; ; i += 1) { + el = els[i]; + if(!el) { + break; + } + c = el.className; + c = c.match(/snf-[^\s'"]+/); + if (c && icons[c[0]]) { + addIcon(el, icons[c[0]]); + } + } +}()); diff --git a/snf-admin-app/synnefo_admin/admin/static/js/jquery.cookie.js b/snf-admin-app/synnefo_admin/admin/static/js/jquery.cookie.js new file mode 100644 index 0000000000000000000000000000000000000000..feb62e92561a63eae96ff42e50ed873944382a5d --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/js/jquery.cookie.js @@ -0,0 +1,117 @@ +/*! + * jQuery Cookie Plugin v1.4.1 + * https://github.com/carhartl/jquery-cookie + * + * Copyright 2006, 2014 Klaus Hartl + * Released under the MIT license + */ +(function (factory) { + if (typeof define === 'function' && define.amd) { + // AMD + define(['jquery'], factory); + } else if (typeof exports === 'object') { + // CommonJS + factory(require('jquery')); + } else { + // Browser globals + factory(jQuery); + } +}(function ($) { + + var pluses = /\+/g; + + function encode(s) { + return config.raw ? s : encodeURIComponent(s); + } + + function decode(s) { + return config.raw ? s : decodeURIComponent(s); + } + + function stringifyCookieValue(value) { + return encode(config.json ? JSON.stringify(value) : String(value)); + } + + function parseCookieValue(s) { + if (s.indexOf('"') === 0) { + // This is a quoted cookie as according to RFC2068, unescape... + s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\'); + } + + try { + // Replace server-side written pluses with spaces. + // If we can't decode the cookie, ignore it, it's unusable. + // If we can't parse the cookie, ignore it, it's unusable. + s = decodeURIComponent(s.replace(pluses, ' ')); + return config.json ? JSON.parse(s) : s; + } catch(e) {} + } + + function read(s, converter) { + var value = config.raw ? s : parseCookieValue(s); + return $.isFunction(converter) ? converter(value) : value; + } + + var config = $.cookie = function (key, value, options) { + + // Write + + if (arguments.length > 1 && !$.isFunction(value)) { + options = $.extend({}, config.defaults, options); + + if (typeof options.expires === 'number') { + var days = options.expires, t = options.expires = new Date(); + t.setTime(+t + days * 864e+5); + } + + return (document.cookie = [ + encode(key), '=', stringifyCookieValue(value), + options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE + options.path ? '; path=' + options.path : '', + options.domain ? '; domain=' + options.domain : '', + options.secure ? '; secure' : '' + ].join('')); + } + + // Read + + var result = key ? undefined : {}; + + // To prevent the for loop in the first place assign an empty array + // in case there are no cookies at all. Also prevents odd result when + // calling $.cookie(). + var cookies = document.cookie ? document.cookie.split('; ') : []; + + for (var i = 0, l = cookies.length; i < l; i++) { + var parts = cookies[i].split('='); + var name = decode(parts.shift()); + var cookie = parts.join('='); + + if (key && key === name) { + // If second argument (value) is a function it's a converter... + result = read(cookie, value); + break; + } + + // Prevent storing a cookie that we couldn't decode. + if (!key && (cookie = read(cookie)) !== undefined) { + result[name] = cookie; + } + } + + return result; + }; + + config.defaults = {}; + + $.removeCookie = function (key, options) { + if ($.cookie(key) === undefined) { + return false; + } + + // Must not alter options, thus extending a fresh object... + $.cookie(key, '', $.extend({}, options, { expires: -1 })); + return !$.cookie(key); + }; + +})); diff --git a/snf-admin-app/synnefo_admin/admin/static/js/jquery.dataTables.js b/snf-admin-app/synnefo_admin/admin/static/js/jquery.dataTables.js new file mode 100644 index 0000000000000000000000000000000000000000..ff0620da0f2a506ed3ff739ec5301b61513542fc --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/js/jquery.dataTables.js @@ -0,0 +1,14383 @@ +/*! DataTables 1.10.0 + * ©2008-2014 SpryMedia Ltd - datatables.net/license + */ + +/** + * @summary DataTables + * @description Paginate, search and order HTML tables + * @version 1.10.0 + * @file jquery.dataTables.js + * @author SpryMedia Ltd (www.sprymedia.co.uk) + * @contact www.sprymedia.co.uk/contact + * @copyright Copyright 2008-2014 SpryMedia Ltd. + * + * This source file is free software, available under the following license: + * MIT license - http://datatables.net/license + * + * This source file 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 license files for details. + * + * For details please refer to: http://www.datatables.net + */ + +/*jslint evil: true, undef: true, browser: true */ +/*globals $,require,jQuery,define,_selector_run,_selector_opts,_selector_first,_selector_row_indexes,_ext,_Api,_api_register,_api_registerPlural,_re_new_lines,_re_html,_re_formatted_numeric,_re_escape_regex,_empty,_intVal,_numToDecimal,_isNumber,_isHtml,_htmlNumeric,_pluck,_pluck_order,_range,_stripHtml,_unique,_fnBuildAjax,_fnAjaxUpdate,_fnAjaxParameters,_fnAjaxUpdateDraw,_fnAjaxDataSrc,_fnAddColumn,_fnColumnOptions,_fnAdjustColumnSizing,_fnVisibleToColumnIndex,_fnColumnIndexToVisible,_fnVisbleColumns,_fnGetColumns,_fnColumnTypes,_fnApplyColumnDefs,_fnHungarianMap,_fnCamelToHungarian,_fnLanguageCompat,_fnBrowserDetect,_fnAddData,_fnAddTr,_fnNodeToDataIndex,_fnNodeToColumnIndex,_fnGetCellData,_fnSetCellData,_fnSplitObjNotation,_fnGetObjectDataFn,_fnSetObjectDataFn,_fnGetDataMaster,_fnClearTable,_fnDeleteIndex,_fnInvalidateRow,_fnGetRowElements,_fnCreateTr,_fnBuildHead,_fnDrawHead,_fnDraw,_fnReDraw,_fnAddOptionsHtml,_fnDetectHeader,_fnGetUniqueThs,_fnFeatureHtmlFilter,_fnFilterComplete,_fnFilterCustom,_fnFilterColumn,_fnFilter,_fnFilterCreateSearch,_fnEscapeRegex,_fnFilterData,_fnFeatureHtmlInfo,_fnUpdateInfo,_fnInfoMacros,_fnInitialise,_fnInitComplete,_fnLengthChange,_fnFeatureHtmlLength,_fnFeatureHtmlPaginate,_fnPageChange,_fnFeatureHtmlProcessing,_fnProcessingDisplay,_fnFeatureHtmlTable,_fnScrollDraw,_fnApplyToChildren,_fnCalculateColumnWidths,_fnThrottle,_fnConvertToWidth,_fnScrollingWidthAdjust,_fnGetWidestNode,_fnGetMaxLenString,_fnStringToCss,_fnScrollBarWidth,_fnSortFlatten,_fnSort,_fnSortAria,_fnSortListener,_fnSortAttachListener,_fnSortingClasses,_fnSortData,_fnSaveState,_fnLoadState,_fnSettingsFromNode,_fnLog,_fnMap,_fnBindAction,_fnCallbackReg,_fnCallbackFire,_fnLengthOverflow,_fnRenderer,_fnDataSource,_fnRowAttributes*/ + +(/** @lends <global> */function( window, document, undefined ) { + +(function( factory ) { + "use strict"; + + if ( typeof define === 'function' && define.amd ) { + // Define as an AMD module if possible + define( 'datatables', ['jquery'], factory ); + } + else if ( typeof exports === 'object' ) { + // Node/CommonJS + factory( require( 'jquery' ) ); + } + else if ( jQuery && !jQuery.fn.dataTable ) { + // Define using browser globals otherwise + // Prevent multiple instantiations if the script is loaded twice + factory( jQuery ); + } +} +(/** @lends <global> */function( $ ) { + "use strict"; + + /** + * DataTables is a plug-in for the jQuery Javascript library. It is a highly + * flexible tool, based upon the foundations of progressive enhancement, + * which will add advanced interaction controls to any HTML table. For a + * full list of features please refer to + * [DataTables.net](href="http://datatables.net). + * + * Note that the `DataTable` object is not a global variable but is aliased + * to `jQuery.fn.DataTable` and `jQuery.fn.dataTable` through which it may + * be accessed. + * + * @class + * @param {object} [init={}] Configuration object for DataTables. Options + * are defined by {@link DataTable.defaults} + * @requires jQuery 1.7+ + * + * @example + * // Basic initialisation + * $(document).ready( function { + * $('#example').dataTable(); + * } ); + * + * @example + * // Initialisation with configuration options - in this case, disable + * // pagination and sorting. + * $(document).ready( function { + * $('#example').dataTable( { + * "paginate": false, + * "sort": false + * } ); + * } ); + */ + var DataTable; + + + /* + * It is useful to have variables which are scoped locally so only the + * DataTables functions can access them and they don't leak into global space. + * At the same time these functions are often useful over multiple files in the + * core and API, so we list, or at least document, all variables which are used + * by DataTables as private variables here. This also ensures that there is no + * clashing of variable names and that they can easily referenced for reuse. + */ + + + // Defined else where + // _selector_run + // _selector_opts + // _selector_first + // _selector_row_indexes + + var _ext; // DataTable.ext + var _Api; // DataTable.Api + var _api_register; // DataTable.Api.register + var _api_registerPlural; // DataTable.Api.registerPlural + + var _re_dic = {}; + var _re_new_lines = /[\r\n]/g; + var _re_html = /<.*?>/g; + var _re_date_start = /^[\d\+\-a-zA-Z]/; + + // Escape regular expression special characters + var _re_escape_regex = new RegExp( '(\\' + [ '/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\', '$', '^', '-' ].join('|\\') + ')', 'g' ); + + // U+2009 is thin space and U+202F is narrow no-break space, both used in many + // standards as thousands separators + var _re_formatted_numeric = /[',$£€¥%\u2009\u202F]/g; + + + var _empty = function ( d ) { + return !d || d === '-' ? true : false; + }; + + + var _intVal = function ( s ) { + var integer = parseInt( s, 10 ); + return !isNaN(integer) && isFinite(s) ? integer : null; + }; + + // Convert from a formatted number with characters other than `.` as the + // decimal place, to a Javascript number + var _numToDecimal = function ( num, decimalPoint ) { + // Cache created regular expressions for speed as this function is called often + if ( ! _re_dic[ decimalPoint ] ) { + _re_dic[ decimalPoint ] = new RegExp( _fnEscapeRegex( decimalPoint ), 'g' ); + } + return typeof num === 'string' ? + num.replace( /\./g, '' ).replace( _re_dic[ decimalPoint ], '.' ) : + num; + }; + + + var _isNumber = function ( d, decimalPoint, formatted ) { + var strType = typeof d === 'string'; + + if ( decimalPoint && strType ) { + d = _numToDecimal( d, decimalPoint ); + } + + if ( formatted && strType ) { + d = d.replace( _re_formatted_numeric, '' ); + } + + return !d || d==='-' || (!isNaN( parseFloat(d) ) && isFinite( d )); + }; + + + // A string without HTML in it can be considered to be HTML still + var _isHtml = function ( d ) { + return !d || typeof d === 'string'; + }; + + + var _htmlNumeric = function ( d, decimalPoint, formatted ) { + if ( _empty( d ) ) { + return true; + } + + var html = _isHtml( d ); + return ! html ? + null : + _isNumber( _stripHtml( d ), decimalPoint, formatted ) ? + true : + null; + }; + + + var _pluck = function ( a, prop, prop2 ) { + var out = []; + var i=0, ien=a.length; + + // Could have the test in the loop for slightly smaller code, but speed + // is essential here + if ( prop2 !== undefined ) { + for ( ; i<ien ; i++ ) { + if ( a[i] && a[i][ prop ] ) { + out.push( a[i][ prop ][ prop2 ] ); + } + } + } + else { + for ( ; i<ien ; i++ ) { + if ( a[i] ) { + out.push( a[i][ prop ] ); + } + } + } + + return out; + }; + + + // Basically the same as _pluck, but rather than looping over `a` we use `order` + // as the indexes to pick from `a` + var _pluck_order = function ( a, order, prop, prop2 ) + { + var out = []; + var i=0, ien=order.length; + + // Could have the test in the loop for slightly smaller code, but speed + // is essential here + if ( prop2 !== undefined ) { + for ( ; i<ien ; i++ ) { + out.push( a[ order[i] ][ prop ][ prop2 ] ); + } + } + else { + for ( ; i<ien ; i++ ) { + out.push( a[ order[i] ][ prop ] ); + } + } + + return out; + }; + + + var _range = function ( len, start ) + { + var out = []; + var end; + + if ( start === undefined ) { + start = 0; + end = len; + } + else { + end = start; + start = len; + } + + for ( var i=start ; i<end ; i++ ) { + out.push( i ); + } + + return out; + }; + + + var _stripHtml = function ( d ) { + return d.replace( _re_html, '' ); + }; + + + /** + * Find the unique elements in a source array. + * + * @param {array} src Source array + * @return {array} Array of unique items + * @ignore + */ + var _unique = function ( src ) + { + // A faster unique method is to use object keys to identify used values, + // but this doesn't work with arrays or objects, which we must also + // consider. See jsperf.com/compare-array-unique-versions/4 for more + // information. + var + out = [], + val, + i, ien=src.length, + j, k=0; + + again: for ( i=0 ; i<ien ; i++ ) { + val = src[i]; + + for ( j=0 ; j<k ; j++ ) { + if ( out[j] === val ) { + continue again; + } + } + + out.push( val ); + k++; + } + + return out; + }; + + + + /** + * Create a mapping object that allows camel case parameters to be looked up + * for their Hungarian counterparts. The mapping is stored in a private + * parameter called `_hungarianMap` which can be accessed on the source object. + * @param {object} o + * @memberof DataTable#oApi + */ + function _fnHungarianMap ( o ) + { + var + hungarian = 'a aa ai ao as b fn i m o s ', + match, + newKey, + map = {}; + + $.each( o, function (key, val) { + match = key.match(/^([^A-Z]+?)([A-Z])/); + + if ( match && hungarian.indexOf(match[1]+' ') !== -1 ) + { + newKey = key.replace( match[0], match[2].toLowerCase() ); + map[ newKey ] = key; + + //console.log( key, match ); + if ( match[1] === 'o' ) + { + _fnHungarianMap( o[key] ); + } + } + } ); + + o._hungarianMap = map; + } + + + /** + * Convert from camel case parameters to Hungarian, based on a Hungarian map + * created by _fnHungarianMap. + * @param {object} src The model object which holds all parameters that can be + * mapped. + * @param {object} user The object to convert from camel case to Hungarian. + * @param {boolean} force When set to `true`, properties which already have a + * Hungarian value in the `user` object will be overwritten. Otherwise they + * won't be. + * @memberof DataTable#oApi + */ + function _fnCamelToHungarian ( src, user, force ) + { + if ( ! src._hungarianMap ) { + _fnHungarianMap( src ); + } + + var hungarianKey; + + $.each( user, function (key, val) { + hungarianKey = src._hungarianMap[ key ]; + + if ( hungarianKey !== undefined && (force || user[hungarianKey] === undefined) ) + { + // For objects, we need to buzz down into the object to copy parameters + if ( hungarianKey.charAt(0) === 'o' ) + { + // Copy the camelCase options over to the hungarian + if ( ! user[ hungarianKey ] ) { + user[ hungarianKey ] = {}; + } + $.extend( true, user[hungarianKey], user[key] ); + + _fnCamelToHungarian( src[hungarianKey], user[hungarianKey], force ); + } + else { + user[hungarianKey] = user[ key ]; + } + } + } ); + } + + + /** + * Language compatibility - when certain options are given, and others aren't, we + * need to duplicate the values over, in order to provide backwards compatibility + * with older language files. + * @param {object} oSettings dataTables settings object + * @memberof DataTable#oApi + */ + function _fnLanguageCompat( lang ) + { + var defaults = DataTable.defaults.oLanguage; + var zeroRecords = lang.sZeroRecords; + + /* Backwards compatibility - if there is no sEmptyTable given, then use the same as + * sZeroRecords - assuming that is given. + */ + if ( ! lang.sEmptyTable && zeroRecords && + defaults.sEmptyTable === "No data available in table" ) + { + _fnMap( lang, lang, 'sZeroRecords', 'sEmptyTable' ); + } + + /* Likewise with loading records */ + if ( ! lang.sLoadingRecords && zeroRecords && + defaults.sLoadingRecords === "Loading..." ) + { + _fnMap( lang, lang, 'sZeroRecords', 'sLoadingRecords' ); + } + + // Old parameter name of the thousands separator mapped onto the new + if ( lang.sInfoThousands ) { + lang.sThousands = lang.sInfoThousands; + } + + var decimal = lang.sDecimal; + if ( decimal ) { + _addNumericSort( decimal ); + } + } + + + /** + * Map one parameter onto another + * @param {object} o Object to map + * @param {*} knew The new parameter name + * @param {*} old The old parameter name + */ + var _fnCompatMap = function ( o, knew, old ) { + if ( o[ knew ] !== undefined ) { + o[ old ] = o[ knew ]; + } + }; + + + /** + * Provide backwards compatibility for the main DT options. Note that the new + * options are mapped onto the old parameters, so this is an external interface + * change only. + * @param {object} init Object to map + */ + function _fnCompatOpts ( init ) + { + _fnCompatMap( init, 'ordering', 'bSort' ); + _fnCompatMap( init, 'orderMulti', 'bSortMulti' ); + _fnCompatMap( init, 'orderClasses', 'bSortClasses' ); + _fnCompatMap( init, 'orderCellsTop', 'bSortCellsTop' ); + _fnCompatMap( init, 'order', 'aaSorting' ); + _fnCompatMap( init, 'orderFixed', 'aaSortingFixed' ); + _fnCompatMap( init, 'paging', 'bPaginate' ); + _fnCompatMap( init, 'pagingType', 'sPaginationType' ); + _fnCompatMap( init, 'pageLength', 'iDisplayLength' ); + _fnCompatMap( init, 'searching', 'bFilter' ); + } + + + /** + * Provide backwards compatibility for column options. Note that the new options + * are mapped onto the old parameters, so this is an external interface change + * only. + * @param {object} init Object to map + */ + function _fnCompatCols ( init ) + { + _fnCompatMap( init, 'orderable', 'bSortable' ); + _fnCompatMap( init, 'orderData', 'aDataSort' ); + _fnCompatMap( init, 'orderSequence', 'asSorting' ); + _fnCompatMap( init, 'orderDataType', 'sortDataType' ); + } + + + /** + * Browser feature detection for capabilities, quirks + * @param {object} settings dataTables settings object + * @memberof DataTable#oApi + */ + function _fnBrowserDetect( settings ) + { + var browser = settings.oBrowser; + + // Scrolling feature / quirks detection + var n = $('<div/>') + .css( { + position: 'absolute', + top: 0, + left: 0, + height: 1, + width: 1, + overflow: 'hidden' + } ) + .append( + $('<div/>') + .css( { + position: 'absolute', + top: 1, + left: 1, + width: 100, + overflow: 'scroll' + } ) + .append( + $('<div class="test"/>') + .css( { + width: '100%', + height: 10 + } ) + ) + ) + .appendTo( 'body' ); + + var test = n.find('.test'); + + // IE6/7 will oversize a width 100% element inside a scrolling element, to + // include the width of the scrollbar, while other browsers ensure the inner + // element is contained without forcing scrolling + browser.bScrollOversize = test[0].offsetWidth === 100; + + // In rtl text layout, some browsers (most, but not all) will place the + // scrollbar on the left, rather than the right. + browser.bScrollbarLeft = test.offset().left !== 1; + + n.remove(); + } + + + /** + * Array.prototype reduce[Right] method, used for browsers which don't support + * JS 1.6. Done this way to reduce code size, since we iterate either way + * @param {object} settings dataTables settings object + * @memberof DataTable#oApi + */ + function _fnReduce ( that, fn, init, start, end, inc ) + { + var + i = start, + value, + isSet = false; + + if ( init !== undefined ) { + value = init; + isSet = true; + } + + while ( i !== end ) { + if ( ! that.hasOwnProperty(i) ) { + continue; + } + + value = isSet ? + fn( value, that[i], i, that ) : + that[i]; + + isSet = true; + i += inc; + } + + return value; + } + + /** + * Add a column to the list used for the table with default values + * @param {object} oSettings dataTables settings object + * @param {node} nTh The th element for this column + * @memberof DataTable#oApi + */ + function _fnAddColumn( oSettings, nTh ) + { + // Add column to aoColumns array + var oDefaults = DataTable.defaults.column; + var iCol = oSettings.aoColumns.length; + var oCol = $.extend( {}, DataTable.models.oColumn, oDefaults, { + "nTh": nTh ? nTh : document.createElement('th'), + "sTitle": oDefaults.sTitle ? oDefaults.sTitle : nTh ? nTh.innerHTML : '', + "aDataSort": oDefaults.aDataSort ? oDefaults.aDataSort : [iCol], + "mData": oDefaults.mData ? oDefaults.mData : iCol, + idx: iCol + } ); + oSettings.aoColumns.push( oCol ); + + // Add search object for column specific search. Note that the `searchCols[ iCol ]` + // passed into extend can be undefined. This allows the user to give a default + // with only some of the parameters defined, and also not give a default + var searchCols = oSettings.aoPreSearchCols; + searchCols[ iCol ] = $.extend( {}, DataTable.models.oSearch, searchCols[ iCol ] ); + + // Use the default column options function to initialise classes etc + _fnColumnOptions( oSettings, iCol, null ); + } + + + /** + * Apply options for a column + * @param {object} oSettings dataTables settings object + * @param {int} iCol column index to consider + * @param {object} oOptions object with sType, bVisible and bSearchable etc + * @memberof DataTable#oApi + */ + function _fnColumnOptions( oSettings, iCol, oOptions ) + { + var oCol = oSettings.aoColumns[ iCol ]; + var oClasses = oSettings.oClasses; + var th = $(oCol.nTh); + + // Try to get width information from the DOM. We can't get it from CSS + // as we'd need to parse the CSS stylesheet. `width` option can override + if ( ! oCol.sWidthOrig ) { + // Width attribute + oCol.sWidthOrig = th.attr('width') || null; + + // Style attribute + var t = (th.attr('style') || '').match(/width:\s*(\d+[pxem%])/); + if ( t ) { + oCol.sWidthOrig = t[1]; + } + } + + /* User specified column options */ + if ( oOptions !== undefined && oOptions !== null ) + { + // Backwards compatibility + _fnCompatCols( oOptions ); + + // Map camel case parameters to their Hungarian counterparts + _fnCamelToHungarian( DataTable.defaults.column, oOptions ); + + /* Backwards compatibility for mDataProp */ + if ( oOptions.mDataProp !== undefined && !oOptions.mData ) + { + oOptions.mData = oOptions.mDataProp; + } + + if ( oOptions.sType ) + { + oCol._sManualType = oOptions.sType; + } + + // `class` is a reserved word in Javascript, so we need to provide + // the ability to use a valid name for the camel case input + if ( oOptions.className && ! oOptions.sClass ) + { + oOptions.sClass = oOptions.className; + } + + $.extend( oCol, oOptions ); + _fnMap( oCol, oOptions, "sWidth", "sWidthOrig" ); + + /* iDataSort to be applied (backwards compatibility), but aDataSort will take + * priority if defined + */ + if ( typeof oOptions.iDataSort === 'number' ) + { + oCol.aDataSort = [ oOptions.iDataSort ]; + } + _fnMap( oCol, oOptions, "aDataSort" ); + } + + /* Cache the data get and set functions for speed */ + var mDataSrc = oCol.mData; + var mData = _fnGetObjectDataFn( mDataSrc ); + var mRender = oCol.mRender ? _fnGetObjectDataFn( oCol.mRender ) : null; + + var attrTest = function( src ) { + return typeof src === 'string' && src.indexOf('@') !== -1; + }; + oCol._bAttrSrc = $.isPlainObject( mDataSrc ) && ( + attrTest(mDataSrc.sort) || attrTest(mDataSrc.type) || attrTest(mDataSrc.filter) + ); + + oCol.fnGetData = function (oData, sSpecific) { + var innerData = mData( oData, sSpecific ); + + if ( oCol.mRender && (sSpecific && sSpecific !== '') ) + { + return mRender( innerData, sSpecific, oData ); + } + return innerData; + }; + oCol.fnSetData = _fnSetObjectDataFn( mDataSrc ); + + /* Feature sorting overrides column specific when off */ + if ( !oSettings.oFeatures.bSort ) + { + oCol.bSortable = false; + th.addClass( oClasses.sSortableNone ); // Have to add class here as order event isn't called + } + + /* Check that the class assignment is correct for sorting */ + var bAsc = $.inArray('asc', oCol.asSorting) !== -1; + var bDesc = $.inArray('desc', oCol.asSorting) !== -1; + if ( !oCol.bSortable || (!bAsc && !bDesc) ) + { + oCol.sSortingClass = oClasses.sSortableNone; + oCol.sSortingClassJUI = ""; + } + else if ( bAsc && !bDesc ) + { + oCol.sSortingClass = oClasses.sSortableAsc; + oCol.sSortingClassJUI = oClasses.sSortJUIAscAllowed; + } + else if ( !bAsc && bDesc ) + { + oCol.sSortingClass = oClasses.sSortableDesc; + oCol.sSortingClassJUI = oClasses.sSortJUIDescAllowed; + } + else + { + oCol.sSortingClass = oClasses.sSortable; + oCol.sSortingClassJUI = oClasses.sSortJUI; + } + } + + + /** + * Adjust the table column widths for new data. Note: you would probably want to + * do a redraw after calling this function! + * @param {object} settings dataTables settings object + * @memberof DataTable#oApi + */ + function _fnAdjustColumnSizing ( settings ) + { + /* Not interested in doing column width calculation if auto-width is disabled */ + if ( settings.oFeatures.bAutoWidth !== false ) + { + var columns = settings.aoColumns; + + _fnCalculateColumnWidths( settings ); + for ( var i=0 , iLen=columns.length ; i<iLen ; i++ ) + { + columns[i].nTh.style.width = columns[i].sWidth; + } + } + + var scroll = settings.oScroll; + if ( scroll.sY !== '' || scroll.sX !== '') + { + _fnScrollDraw( settings ); + } + + _fnCallbackFire( settings, null, 'column-sizing', [settings] ); + } + + + /** + * Covert the index of a visible column to the index in the data array (take account + * of hidden columns) + * @param {object} oSettings dataTables settings object + * @param {int} iMatch Visible column index to lookup + * @returns {int} i the data index + * @memberof DataTable#oApi + */ + function _fnVisibleToColumnIndex( oSettings, iMatch ) + { + var aiVis = _fnGetColumns( oSettings, 'bVisible' ); + + return typeof aiVis[iMatch] === 'number' ? + aiVis[iMatch] : + null; + } + + + /** + * Covert the index of an index in the data array and convert it to the visible + * column index (take account of hidden columns) + * @param {int} iMatch Column index to lookup + * @param {object} oSettings dataTables settings object + * @returns {int} i the data index + * @memberof DataTable#oApi + */ + function _fnColumnIndexToVisible( oSettings, iMatch ) + { + var aiVis = _fnGetColumns( oSettings, 'bVisible' ); + var iPos = $.inArray( iMatch, aiVis ); + + return iPos !== -1 ? iPos : null; + } + + + /** + * Get the number of visible columns + * @param {object} oSettings dataTables settings object + * @returns {int} i the number of visible columns + * @memberof DataTable#oApi + */ + function _fnVisbleColumns( oSettings ) + { + return _fnGetColumns( oSettings, 'bVisible' ).length; + } + + + /** + * Get an array of column indexes that match a given property + * @param {object} oSettings dataTables settings object + * @param {string} sParam Parameter in aoColumns to look for - typically + * bVisible or bSearchable + * @returns {array} Array of indexes with matched properties + * @memberof DataTable#oApi + */ + function _fnGetColumns( oSettings, sParam ) + { + var a = []; + + $.map( oSettings.aoColumns, function(val, i) { + if ( val[sParam] ) { + a.push( i ); + } + } ); + + return a; + } + + + /** + * Calculate the 'type' of a column + * @param {object} settings dataTables settings object + * @memberof DataTable#oApi + */ + function _fnColumnTypes ( settings ) + { + var columns = settings.aoColumns; + var data = settings.aoData; + var types = DataTable.ext.type.detect; + var i, ien, j, jen, k, ken; + var col, cell, detectedType, cache; + + // For each column, spin over the + for ( i=0, ien=columns.length ; i<ien ; i++ ) { + col = columns[i]; + cache = []; + + if ( ! col.sType && col._sManualType ) { + col.sType = col._sManualType; + } + else if ( ! col.sType ) { + for ( j=0, jen=types.length ; j<jen ; j++ ) { + for ( k=0, ken=data.length ; k<ken ; k++ ) { + // Use a cache array so we only need to get the type data + // from the formatter once (when using multiple detectors) + if ( cache[k] === undefined ) { + cache[k] = _fnGetCellData( settings, k, i, 'type' ); + } + + detectedType = types[j]( cache[k], settings ); + + // Doesn't match, so break early, since this type can't + // apply to this column. Also, HTML is a special case since + // it is so similar to `string`. Just a single match is + // needed for a column to be html type + if ( ! detectedType || detectedType === 'html' ) { + break; + } + } + + // Type is valid for all data points in the column - use this + // type + if ( detectedType ) { + col.sType = detectedType; + break; + } + } + + // Fall back - if no type was detected, always use string + if ( ! col.sType ) { + col.sType = 'string'; + } + } + } + } + + + /** + * Take the column definitions and static columns arrays and calculate how + * they relate to column indexes. The callback function will then apply the + * definition found for a column to a suitable configuration object. + * @param {object} oSettings dataTables settings object + * @param {array} aoColDefs The aoColumnDefs array that is to be applied + * @param {array} aoCols The aoColumns array that defines columns individually + * @param {function} fn Callback function - takes two parameters, the calculated + * column index and the definition for that column. + * @memberof DataTable#oApi + */ + function _fnApplyColumnDefs( oSettings, aoColDefs, aoCols, fn ) + { + var i, iLen, j, jLen, k, kLen, def; + var columns = oSettings.aoColumns; + + // Column definitions with aTargets + if ( aoColDefs ) + { + /* Loop over the definitions array - loop in reverse so first instance has priority */ + for ( i=aoColDefs.length-1 ; i>=0 ; i-- ) + { + def = aoColDefs[i]; + + /* Each definition can target multiple columns, as it is an array */ + var aTargets = def.targets !== undefined ? + def.targets : + def.aTargets; + + if ( ! $.isArray( aTargets ) ) + { + aTargets = [ aTargets ]; + } + + for ( j=0, jLen=aTargets.length ; j<jLen ; j++ ) + { + if ( typeof aTargets[j] === 'number' && aTargets[j] >= 0 ) + { + /* Add columns that we don't yet know about */ + while( columns.length <= aTargets[j] ) + { + _fnAddColumn( oSettings ); + } + + /* Integer, basic index */ + fn( aTargets[j], def ); + } + else if ( typeof aTargets[j] === 'number' && aTargets[j] < 0 ) + { + /* Negative integer, right to left column counting */ + fn( columns.length+aTargets[j], def ); + } + else if ( typeof aTargets[j] === 'string' ) + { + /* Class name matching on TH element */ + for ( k=0, kLen=columns.length ; k<kLen ; k++ ) + { + if ( aTargets[j] == "_all" || + $(columns[k].nTh).hasClass( aTargets[j] ) ) + { + fn( k, def ); + } + } + } + } + } + } + + // Statically defined columns array + if ( aoCols ) + { + for ( i=0, iLen=aoCols.length ; i<iLen ; i++ ) + { + fn( i, aoCols[i] ); + } + } + } + + /** + * Add a data array to the table, creating DOM node etc. This is the parallel to + * _fnGatherData, but for adding rows from a Javascript source, rather than a + * DOM source. + * @param {object} oSettings dataTables settings object + * @param {array} aData data array to be added + * @param {node} [nTr] TR element to add to the table - optional. If not given, + * DataTables will create a row automatically + * @param {array} [anTds] Array of TD|TH elements for the row - must be given + * if nTr is. + * @returns {int} >=0 if successful (index of new aoData entry), -1 if failed + * @memberof DataTable#oApi + */ + function _fnAddData ( oSettings, aDataIn, nTr, anTds ) + { + /* Create the object for storing information about this new row */ + var iRow = oSettings.aoData.length; + var oData = $.extend( true, {}, DataTable.models.oRow, { + src: nTr ? 'dom' : 'data' + } ); + + oData._aData = aDataIn; + oSettings.aoData.push( oData ); + + /* Create the cells */ + var nTd, sThisType; + var columns = oSettings.aoColumns; + for ( var i=0, iLen=columns.length ; i<iLen ; i++ ) + { + // When working with a row, the data source object must be populated. In + // all other cases, the data source object is already populated, so we + // don't overwrite it, which might break bindings etc + if ( nTr ) { + _fnSetCellData( oSettings, iRow, i, _fnGetCellData( oSettings, iRow, i ) ); + } + columns[i].sType = null; + } + + /* Add to the display array */ + oSettings.aiDisplayMaster.push( iRow ); + + /* Create the DOM information */ + if ( !oSettings.oFeatures.bDeferRender ) + { + _fnCreateTr( oSettings, iRow, nTr, anTds ); + } + + return iRow; + } + + + /** + * Add one or more TR elements to the table. Generally we'd expect to + * use this for reading data from a DOM sourced table, but it could be + * used for an TR element. Note that if a TR is given, it is used (i.e. + * it is not cloned). + * @param {object} settings dataTables settings object + * @param {array|node|jQuery} trs The TR element(s) to add to the table + * @returns {array} Array of indexes for the added rows + * @memberof DataTable#oApi + */ + function _fnAddTr( settings, trs ) + { + var row; + + // Allow an individual node to be passed in + if ( ! (trs instanceof $) ) { + trs = $(trs); + } + + return trs.map( function (i, el) { + row = _fnGetRowElements( settings, el ); + return _fnAddData( settings, row.data, el, row.cells ); + } ); + } + + + /** + * Take a TR element and convert it to an index in aoData + * @param {object} oSettings dataTables settings object + * @param {node} n the TR element to find + * @returns {int} index if the node is found, null if not + * @memberof DataTable#oApi + */ + function _fnNodeToDataIndex( oSettings, n ) + { + return (n._DT_RowIndex!==undefined) ? n._DT_RowIndex : null; + } + + + /** + * Take a TD element and convert it into a column data index (not the visible index) + * @param {object} oSettings dataTables settings object + * @param {int} iRow The row number the TD/TH can be found in + * @param {node} n The TD/TH element to find + * @returns {int} index if the node is found, -1 if not + * @memberof DataTable#oApi + */ + function _fnNodeToColumnIndex( oSettings, iRow, n ) + { + return $.inArray( n, oSettings.aoData[ iRow ].anCells ); + } + + + /** + * Get the data for a given cell from the internal cache, taking into account data mapping + * @param {object} oSettings dataTables settings object + * @param {int} iRow aoData row id + * @param {int} iCol Column index + * @param {string} sSpecific data get type ('display', 'type' 'filter' 'sort') + * @returns {*} Cell data + * @memberof DataTable#oApi + */ + function _fnGetCellData( oSettings, iRow, iCol, sSpecific ) + { + var oCol = oSettings.aoColumns[iCol]; + var oData = oSettings.aoData[iRow]._aData; + var sData = oCol.fnGetData( oData, sSpecific ); + + if ( sData === undefined ) + { + if ( oSettings.iDrawError != oSettings.iDraw && oCol.sDefaultContent === null ) + { + _fnLog( oSettings, 0, "Requested unknown parameter "+ + (typeof oCol.mData=='function' ? '{function}' : "'"+oCol.mData+"'")+ + " for row "+iRow, 4 ); + oSettings.iDrawError = oSettings.iDraw; + } + return oCol.sDefaultContent; + } + + /* When the data source is null, we can use default column data */ + if ( (sData === oData || sData === null) && oCol.sDefaultContent !== null ) + { + sData = oCol.sDefaultContent; + } + else if ( typeof sData === 'function' ) + { + // If the data source is a function, then we run it and use the return + return sData(); + } + + if ( sData === null && sSpecific == 'display' ) + { + return ''; + } + return sData; + } + + + /** + * Set the value for a specific cell, into the internal data cache + * @param {object} oSettings dataTables settings object + * @param {int} iRow aoData row id + * @param {int} iCol Column index + * @param {*} val Value to set + * @memberof DataTable#oApi + */ + function _fnSetCellData( oSettings, iRow, iCol, val ) + { + var oCol = oSettings.aoColumns[iCol]; + var oData = oSettings.aoData[iRow]._aData; + + oCol.fnSetData( oData, val ); + } + + + // Private variable that is used to match action syntax in the data property object + var __reArray = /\[.*?\]$/; + var __reFn = /\(\)$/; + + /** + * Split string on periods, taking into account escaped periods + * @param {string} str String to split + * @return {array} Split string + */ + function _fnSplitObjNotation( str ) + { + return $.map( str.match(/(\\.|[^\.])+/g), function ( s ) { + return s.replace('\\.', '.'); + } ); + } + + + /** + * Return a function that can be used to get data from a source object, taking + * into account the ability to use nested objects as a source + * @param {string|int|function} mSource The data source for the object + * @returns {function} Data get function + * @memberof DataTable#oApi + */ + function _fnGetObjectDataFn( mSource ) + { + if ( $.isPlainObject( mSource ) ) + { + /* Build an object of get functions, and wrap them in a single call */ + var o = {}; + $.each( mSource, function (key, val) { + if ( val ) { + o[key] = _fnGetObjectDataFn( val ); + } + } ); + + return function (data, type, extra) { + var t = o[type] || o._; + return t !== undefined ? + t(data, type, extra) : + data; + }; + } + else if ( mSource === null ) + { + /* Give an empty string for rendering / sorting etc */ + return function (data, type) { + return data; + }; + } + else if ( typeof mSource === 'function' ) + { + return function (data, type, extra) { + return mSource( data, type, extra ); + }; + } + else if ( typeof mSource === 'string' && (mSource.indexOf('.') !== -1 || + mSource.indexOf('[') !== -1 || mSource.indexOf('(') !== -1) ) + { + /* If there is a . in the source string then the data source is in a + * nested object so we loop over the data for each level to get the next + * level down. On each loop we test for undefined, and if found immediately + * return. This allows entire objects to be missing and sDefaultContent to + * be used if defined, rather than throwing an error + */ + var fetchData = function (data, type, src) { + var arrayNotation, funcNotation, out, innerSrc; + + if ( src !== "" ) + { + var a = _fnSplitObjNotation( src ); + + for ( var i=0, iLen=a.length ; i<iLen ; i++ ) + { + // Check if we are dealing with special notation + arrayNotation = a[i].match(__reArray); + funcNotation = a[i].match(__reFn); + + if ( arrayNotation ) + { + // Array notation + a[i] = a[i].replace(__reArray, ''); + + // Condition allows simply [] to be passed in + if ( a[i] !== "" ) { + data = data[ a[i] ]; + } + out = []; + + // Get the remainder of the nested object to get + a.splice( 0, i+1 ); + innerSrc = a.join('.'); + + // Traverse each entry in the array getting the properties requested + for ( var j=0, jLen=data.length ; j<jLen ; j++ ) { + out.push( fetchData( data[j], type, innerSrc ) ); + } + + // If a string is given in between the array notation indicators, that + // is used to join the strings together, otherwise an array is returned + var join = arrayNotation[0].substring(1, arrayNotation[0].length-1); + data = (join==="") ? out : out.join(join); + + // The inner call to fetchData has already traversed through the remainder + // of the source requested, so we exit from the loop + break; + } + else if ( funcNotation ) + { + // Function call + a[i] = a[i].replace(__reFn, ''); + data = data[ a[i] ](); + continue; + } + + if ( data === null || data[ a[i] ] === undefined ) + { + return undefined; + } + data = data[ a[i] ]; + } + } + + return data; + }; + + return function (data, type) { + return fetchData( data, type, mSource ); + }; + } + else + { + /* Array or flat object mapping */ + return function (data, type) { + return data[mSource]; + }; + } + } + + + /** + * Return a function that can be used to set data from a source object, taking + * into account the ability to use nested objects as a source + * @param {string|int|function} mSource The data source for the object + * @returns {function} Data set function + * @memberof DataTable#oApi + */ + function _fnSetObjectDataFn( mSource ) + { + if ( $.isPlainObject( mSource ) ) + { + /* Unlike get, only the underscore (global) option is used for for + * setting data since we don't know the type here. This is why an object + * option is not documented for `mData` (which is read/write), but it is + * for `mRender` which is read only. + */ + return _fnSetObjectDataFn( mSource._ ); + } + else if ( mSource === null ) + { + /* Nothing to do when the data source is null */ + return function (data, val) {}; + } + else if ( typeof mSource === 'function' ) + { + return function (data, val) { + mSource( data, 'set', val ); + }; + } + else if ( typeof mSource === 'string' && (mSource.indexOf('.') !== -1 || + mSource.indexOf('[') !== -1 || mSource.indexOf('(') !== -1) ) + { + /* Like the get, we need to get data from a nested object */ + var setData = function (data, val, src) { + var a = _fnSplitObjNotation( src ), b; + var aLast = a[a.length-1]; + var arrayNotation, funcNotation, o, innerSrc; + + for ( var i=0, iLen=a.length-1 ; i<iLen ; i++ ) + { + // Check if we are dealing with an array notation request + arrayNotation = a[i].match(__reArray); + funcNotation = a[i].match(__reFn); + + if ( arrayNotation ) + { + a[i] = a[i].replace(__reArray, ''); + data[ a[i] ] = []; + + // Get the remainder of the nested object to set so we can recurse + b = a.slice(); + b.splice( 0, i+1 ); + innerSrc = b.join('.'); + + // Traverse each entry in the array setting the properties requested + for ( var j=0, jLen=val.length ; j<jLen ; j++ ) + { + o = {}; + setData( o, val[j], innerSrc ); + data[ a[i] ].push( o ); + } + + // The inner call to setData has already traversed through the remainder + // of the source and has set the data, thus we can exit here + return; + } + else if ( funcNotation ) + { + // Function call + a[i] = a[i].replace(__reFn, ''); + data = data[ a[i] ]( val ); + } + + // If the nested object doesn't currently exist - since we are + // trying to set the value - create it + if ( data[ a[i] ] === null || data[ a[i] ] === undefined ) + { + data[ a[i] ] = {}; + } + data = data[ a[i] ]; + } + + // Last item in the input - i.e, the actual set + if ( aLast.match(__reFn ) ) + { + // Function call + data = data[ aLast.replace(__reFn, '') ]( val ); + } + else + { + // If array notation is used, we just want to strip it and use the property name + // and assign the value. If it isn't used, then we get the result we want anyway + data[ aLast.replace(__reArray, '') ] = val; + } + }; + + return function (data, val) { + return setData( data, val, mSource ); + }; + } + else + { + /* Array or flat object mapping */ + return function (data, val) { + data[mSource] = val; + }; + } + } + + + /** + * Return an array with the full table data + * @param {object} oSettings dataTables settings object + * @returns array {array} aData Master data array + * @memberof DataTable#oApi + */ + function _fnGetDataMaster ( settings ) + { + return _pluck( settings.aoData, '_aData' ); + } + + + /** + * Nuke the table + * @param {object} oSettings dataTables settings object + * @memberof DataTable#oApi + */ + function _fnClearTable( settings ) + { + settings.aoData.length = 0; + settings.aiDisplayMaster.length = 0; + settings.aiDisplay.length = 0; + } + + + /** + * Take an array of integers (index array) and remove a target integer (value - not + * the key!) + * @param {array} a Index array to target + * @param {int} iTarget value to find + * @memberof DataTable#oApi + */ + function _fnDeleteIndex( a, iTarget, splice ) + { + var iTargetIndex = -1; + + for ( var i=0, iLen=a.length ; i<iLen ; i++ ) + { + if ( a[i] == iTarget ) + { + iTargetIndex = i; + } + else if ( a[i] > iTarget ) + { + a[i]--; + } + } + + if ( iTargetIndex != -1 && splice === undefined ) + { + a.splice( iTargetIndex, 1 ); + } + } + + + /** + * Mark cached data as invalid such that a re-read of the data will occur when + * the cached data is next requested. Also update from the data source object. + * + * @param {object} settings DataTables settings object + * @param {int} rowIdx Row index to invalidate + * @memberof DataTable#oApi + * + * @todo For the modularisation of v1.11 this will need to become a callback, so + * the sort and filter methods can subscribe to it. That will required + * initialisation options for sorting, which is why it is not already baked in + */ + function _fnInvalidateRow( settings, rowIdx, src, column ) + { + var row = settings.aoData[ rowIdx ]; + var i, ien; + + // Are we reading last data from DOM or the data object? + if ( src === 'dom' || ((! src || src === 'auto') && row.src === 'dom') ) { + // Read the data from the DOM + row._aData = _fnGetRowElements( settings, row ).data; + } + else { + // Reading from data object, update the DOM + var cells = row.anCells; + + if ( cells ) { + for ( i=0, ien=cells.length ; i<ien ; i++ ) { + cells[i].innerHTML = _fnGetCellData( settings, rowIdx, i, 'display' ); + } + } + } + + row._aSortData = null; + row._aFilterData = null; + + // Invalidate the type for a specific column (if given) or all columns since + // the data might have changed + var cols = settings.aoColumns; + if ( column !== undefined ) { + cols[ column ].sType = null; + } + else { + for ( i=0, ien=cols.length ; i<ien ; i++ ) { + cols[i].sType = null; + } + } + + // Update DataTables special `DT_*` attributes for the row + _fnRowAttributes( row ); + } + + + /** + * Build a data source object from an HTML row, reading the contents of the + * cells that are in the row. + * + * @param {object} settings DataTables settings object + * @param {node|object} TR element from which to read data or existing row + * object from which to re-read the data from the cells + * @returns {object} Object with two parameters: `data` the data read, in + * document order, and `cells` and array of nodes (they can be useful to the + * caller, so rather than needing a second traversal to get them, just return + * them from here). + * @memberof DataTable#oApi + */ + function _fnGetRowElements( settings, row ) + { + var + d = [], + tds = [], + td = row.firstChild, + name, col, o, i=0, contents, + columns = settings.aoColumns; + + var attr = function ( str, data, td ) { + if ( typeof str === 'string' ) { + var idx = str.indexOf('@'); + + if ( idx !== -1 ) { + var src = str.substring( idx+1 ); + o[ '@'+src ] = td.getAttribute( src ); + } + } + }; + + var cellProcess = function ( cell ) { + col = columns[i]; + contents = $.trim(cell.innerHTML); + + if ( col && col._bAttrSrc ) { + o = { + display: contents + }; + + attr( col.mData.sort, o, cell ); + attr( col.mData.type, o, cell ); + attr( col.mData.filter, o, cell ); + + d.push( o ); + } + else { + d.push( contents ); + } + + tds.push( cell ); + i++; + }; + + if ( td ) { + // `tr` element passed in + while ( td ) { + name = td.nodeName.toUpperCase(); + + if ( name == "TD" || name == "TH" ) { + cellProcess( td ); + } + + td = td.nextSibling; + } + } + else { + // Existing row object passed in + tds = row.anCells; + + for ( var j=0, jen=tds.length ; j<jen ; j++ ) { + cellProcess( tds[j] ); + } + } + + return { + data: d, + cells: tds + }; + } + /** + * Create a new TR element (and it's TD children) for a row + * @param {object} oSettings dataTables settings object + * @param {int} iRow Row to consider + * @param {node} [nTrIn] TR element to add to the table - optional. If not given, + * DataTables will create a row automatically + * @param {array} [anTds] Array of TD|TH elements for the row - must be given + * if nTr is. + * @memberof DataTable#oApi + */ + function _fnCreateTr ( oSettings, iRow, nTrIn, anTds ) + { + var + row = oSettings.aoData[iRow], + rowData = row._aData, + cells = [], + nTr, nTd, oCol, + i, iLen; + + if ( row.nTr === null ) + { + nTr = nTrIn || document.createElement('tr'); + + row.nTr = nTr; + row.anCells = cells; + + /* Use a private property on the node to allow reserve mapping from the node + * to the aoData array for fast look up + */ + nTr._DT_RowIndex = iRow; + + /* Special parameters can be given by the data source to be used on the row */ + _fnRowAttributes( row ); + + /* Process each column */ + for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ ) + { + oCol = oSettings.aoColumns[i]; + + nTd = nTrIn ? anTds[i] : document.createElement( oCol.sCellType ); + cells.push( nTd ); + + // Need to create the HTML if new, or if a rendering function is defined + if ( !nTrIn || oCol.mRender || oCol.mData !== i ) + { + nTd.innerHTML = _fnGetCellData( oSettings, iRow, i, 'display' ); + } + + /* Add user defined class */ + if ( oCol.sClass ) + { + nTd.className += ' '+oCol.sClass; + } + + // Visibility - add or remove as required + if ( oCol.bVisible && ! nTrIn ) + { + nTr.appendChild( nTd ); + } + else if ( ! oCol.bVisible && nTrIn ) + { + nTd.parentNode.removeChild( nTd ); + } + + if ( oCol.fnCreatedCell ) + { + oCol.fnCreatedCell.call( oSettings.oInstance, + nTd, _fnGetCellData( oSettings, iRow, i, 'display' ), rowData, iRow, i + ); + } + } + + _fnCallbackFire( oSettings, 'aoRowCreatedCallback', null, [nTr, rowData, iRow] ); + } + + // Remove once webkit bug 131819 and Chromium bug 365619 have been resolved + // and deployed + row.nTr.setAttribute( 'role', 'row' ); + } + + + /** + * Add attributes to a row based on the special `DT_*` parameters in a data + * source object. + * @param {object} DataTables row object for the row to be modified + * @memberof DataTable#oApi + */ + function _fnRowAttributes( row ) + { + var tr = row.nTr; + var data = row._aData; + + if ( tr ) { + if ( data.DT_RowId ) { + tr.id = data.DT_RowId; + } + + if ( data.DT_RowClass ) { + // Remove any classes added by DT_RowClass before + var a = data.DT_RowClass.split(' '); + row.__rowc = row.__rowc ? + _unique( row.__rowc.concat( a ) ) : + a; + + $(tr) + .removeClass( row.__rowc.join(' ') ) + .addClass( data.DT_RowClass ); + } + + if ( data.DT_RowData ) { + $(tr).data( data.DT_RowData ); + } + } + } + + + /** + * Create the HTML header for the table + * @param {object} oSettings dataTables settings object + * @memberof DataTable#oApi + */ + function _fnBuildHead( oSettings ) + { + var i, ien, cell, row, column; + var thead = oSettings.nTHead; + var tfoot = oSettings.nTFoot; + var createHeader = $('th, td', thead).length === 0; + var classes = oSettings.oClasses; + var columns = oSettings.aoColumns; + + if ( createHeader ) { + row = $('<tr/>').appendTo( thead ); + } + + for ( i=0, ien=columns.length ; i<ien ; i++ ) { + column = columns[i]; + cell = $( column.nTh ).addClass( column.sClass ); + + if ( createHeader ) { + cell.appendTo( row ); + } + + // 1.11 move into sorting + if ( oSettings.oFeatures.bSort ) { + cell.addClass( column.sSortingClass ); + + if ( column.bSortable !== false ) { + cell + .attr( 'tabindex', oSettings.iTabIndex ) + .attr( 'aria-controls', oSettings.sTableId ); + + _fnSortAttachListener( oSettings, column.nTh, i ); + } + } + + if ( column.sTitle != cell.html() ) { + cell.html( column.sTitle ); + } + + _fnRenderer( oSettings, 'header' )( + oSettings, cell, column, classes + ); + } + + if ( createHeader ) { + _fnDetectHeader( oSettings.aoHeader, thead ); + } + + /* ARIA role for the rows */ + $(thead).find('>tr').attr('role', 'row'); + + /* Deal with the footer - add classes if required */ + $(thead).find('>tr>th, >tr>td').addClass( classes.sHeaderTH ); + $(tfoot).find('>tr>th, >tr>td').addClass( classes.sFooterTH ); + + // Cache the footer cells. Note that we only take the cells from the first + // row in the footer. If there is more than one row the user wants to + // interact with, they need to use the table().foot() method. Note also this + // allows cells to be used for multiple columns using colspan + if ( tfoot !== null ) { + var cells = oSettings.aoFooter[0]; + + for ( i=0, ien=cells.length ; i<ien ; i++ ) { + column = columns[i]; + column.nTf = cells[i].cell; + + if ( column.sClass ) { + $(column.nTf).addClass( column.sClass ); + } + } + } + } + + + /** + * Draw the header (or footer) element based on the column visibility states. The + * methodology here is to use the layout array from _fnDetectHeader, modified for + * the instantaneous column visibility, to construct the new layout. The grid is + * traversed over cell at a time in a rows x columns grid fashion, although each + * cell insert can cover multiple elements in the grid - which is tracks using the + * aApplied array. Cell inserts in the grid will only occur where there isn't + * already a cell in that position. + * @param {object} oSettings dataTables settings object + * @param array {objects} aoSource Layout array from _fnDetectHeader + * @param {boolean} [bIncludeHidden=false] If true then include the hidden columns in the calc, + * @memberof DataTable#oApi + */ + function _fnDrawHead( oSettings, aoSource, bIncludeHidden ) + { + var i, iLen, j, jLen, k, kLen, n, nLocalTr; + var aoLocal = []; + var aApplied = []; + var iColumns = oSettings.aoColumns.length; + var iRowspan, iColspan; + + if ( ! aoSource ) + { + return; + } + + if ( bIncludeHidden === undefined ) + { + bIncludeHidden = false; + } + + /* Make a copy of the master layout array, but without the visible columns in it */ + for ( i=0, iLen=aoSource.length ; i<iLen ; i++ ) + { + aoLocal[i] = aoSource[i].slice(); + aoLocal[i].nTr = aoSource[i].nTr; + + /* Remove any columns which are currently hidden */ + for ( j=iColumns-1 ; j>=0 ; j-- ) + { + if ( !oSettings.aoColumns[j].bVisible && !bIncludeHidden ) + { + aoLocal[i].splice( j, 1 ); + } + } + + /* Prep the applied array - it needs an element for each row */ + aApplied.push( [] ); + } + + for ( i=0, iLen=aoLocal.length ; i<iLen ; i++ ) + { + nLocalTr = aoLocal[i].nTr; + + /* All cells are going to be replaced, so empty out the row */ + if ( nLocalTr ) + { + while( (n = nLocalTr.firstChild) ) + { + nLocalTr.removeChild( n ); + } + } + + for ( j=0, jLen=aoLocal[i].length ; j<jLen ; j++ ) + { + iRowspan = 1; + iColspan = 1; + + /* Check to see if there is already a cell (row/colspan) covering our target + * insert point. If there is, then there is nothing to do. + */ + if ( aApplied[i][j] === undefined ) + { + nLocalTr.appendChild( aoLocal[i][j].cell ); + aApplied[i][j] = 1; + + /* Expand the cell to cover as many rows as needed */ + while ( aoLocal[i+iRowspan] !== undefined && + aoLocal[i][j].cell == aoLocal[i+iRowspan][j].cell ) + { + aApplied[i+iRowspan][j] = 1; + iRowspan++; + } + + /* Expand the cell to cover as many columns as needed */ + while ( aoLocal[i][j+iColspan] !== undefined && + aoLocal[i][j].cell == aoLocal[i][j+iColspan].cell ) + { + /* Must update the applied array over the rows for the columns */ + for ( k=0 ; k<iRowspan ; k++ ) + { + aApplied[i+k][j+iColspan] = 1; + } + iColspan++; + } + + /* Do the actual expansion in the DOM */ + $(aoLocal[i][j].cell) + .attr('rowspan', iRowspan) + .attr('colspan', iColspan); + } + } + } + } + + + /** + * Insert the required TR nodes into the table for display + * @param {object} oSettings dataTables settings object + * @memberof DataTable#oApi + */ + function _fnDraw( oSettings ) + { + /* Provide a pre-callback function which can be used to cancel the draw is false is returned */ + var aPreDraw = _fnCallbackFire( oSettings, 'aoPreDrawCallback', 'preDraw', [oSettings] ); + if ( $.inArray( false, aPreDraw ) !== -1 ) + { + _fnProcessingDisplay( oSettings, false ); + return; + } + + var i, iLen, n; + var anRows = []; + var iRowCount = 0; + var asStripeClasses = oSettings.asStripeClasses; + var iStripes = asStripeClasses.length; + var iOpenRows = oSettings.aoOpenRows.length; + var oLang = oSettings.oLanguage; + var iInitDisplayStart = oSettings.iInitDisplayStart; + var bServerSide = _fnDataSource( oSettings ) == 'ssp'; + var aiDisplay = oSettings.aiDisplay; + + oSettings.bDrawing = true; + + /* Check and see if we have an initial draw position from state saving */ + if ( iInitDisplayStart !== undefined && iInitDisplayStart !== -1 ) + { + oSettings._iDisplayStart = bServerSide ? + iInitDisplayStart : + iInitDisplayStart >= oSettings.fnRecordsDisplay() ? + 0 : + iInitDisplayStart; + + oSettings.iInitDisplayStart = -1; + } + + var iDisplayStart = oSettings._iDisplayStart; + var iDisplayEnd = oSettings.fnDisplayEnd(); + + /* Server-side processing draw intercept */ + if ( oSettings.bDeferLoading ) + { + oSettings.bDeferLoading = false; + oSettings.iDraw++; + _fnProcessingDisplay( oSettings, false ); + } + else if ( !bServerSide ) + { + oSettings.iDraw++; + } + else if ( !oSettings.bDestroying && !_fnAjaxUpdate( oSettings ) ) + { + return; + } + + if ( aiDisplay.length !== 0 ) + { + var iStart = bServerSide ? 0 : iDisplayStart; + var iEnd = bServerSide ? oSettings.aoData.length : iDisplayEnd; + + for ( var j=iStart ; j<iEnd ; j++ ) + { + var iDataIndex = aiDisplay[j]; + var aoData = oSettings.aoData[ iDataIndex ]; + if ( aoData.nTr === null ) + { + _fnCreateTr( oSettings, iDataIndex ); + } + + var nRow = aoData.nTr; + + /* Remove the old striping classes and then add the new one */ + if ( iStripes !== 0 ) + { + var sStripe = asStripeClasses[ iRowCount % iStripes ]; + if ( aoData._sRowStripe != sStripe ) + { + $(nRow).removeClass( aoData._sRowStripe ).addClass( sStripe ); + aoData._sRowStripe = sStripe; + } + } + + /* Row callback functions - might want to manipulate the row */ + _fnCallbackFire( oSettings, 'aoRowCallback', null, + [nRow, aoData._aData, iRowCount, j] ); + + anRows.push( nRow ); + iRowCount++; + } + } + else + { + /* Table is empty - create a row with an empty message in it */ + var sZero = oLang.sZeroRecords; + if ( oSettings.iDraw == 1 && _fnDataSource( oSettings ) == 'ajax' ) + { + sZero = oLang.sLoadingRecords; + } + else if ( oLang.sEmptyTable && oSettings.fnRecordsTotal() === 0 ) + { + sZero = oLang.sEmptyTable; + } + + anRows[ 0 ] = $( '<tr/>', { 'class': iStripes ? asStripeClasses[0] : '' } ) + .append( $('<td />', { + 'valign': 'top', + 'colSpan': _fnVisbleColumns( oSettings ), + 'class': oSettings.oClasses.sRowEmpty + } ).html( sZero ) )[0]; + } + + /* Header and footer callbacks */ + _fnCallbackFire( oSettings, 'aoHeaderCallback', 'header', [ $(oSettings.nTHead).children('tr')[0], + _fnGetDataMaster( oSettings ), iDisplayStart, iDisplayEnd, aiDisplay ] ); + + _fnCallbackFire( oSettings, 'aoFooterCallback', 'footer', [ $(oSettings.nTFoot).children('tr')[0], + _fnGetDataMaster( oSettings ), iDisplayStart, iDisplayEnd, aiDisplay ] ); + + var body = $(oSettings.nTBody); + + body.children().detach(); + body.append( $(anRows) ); + + /* Call all required callback functions for the end of a draw */ + _fnCallbackFire( oSettings, 'aoDrawCallback', 'draw', [oSettings] ); + + /* Draw is complete, sorting and filtering must be as well */ + oSettings.bSorted = false; + oSettings.bFiltered = false; + oSettings.bDrawing = false; + } + + + /** + * Redraw the table - taking account of the various features which are enabled + * @param {object} oSettings dataTables settings object + * @param {boolean} [holdPosition] Keep the current paging position. By default + * the paging is reset to the first page + * @memberof DataTable#oApi + */ + function _fnReDraw( settings, holdPosition ) + { + var + features = settings.oFeatures, + sort = features.bSort, + filter = features.bFilter; + + if ( sort ) { + _fnSort( settings ); + } + + if ( filter ) { + _fnFilterComplete( settings, settings.oPreviousSearch ); + } + else { + // No filtering, so we want to just use the display master + settings.aiDisplay = settings.aiDisplayMaster.slice(); + } + + if ( holdPosition !== true ) { + settings._iDisplayStart = 0; + } + + _fnDraw( settings ); + } + + + /** + * Add the options to the page HTML for the table + * @param {object} oSettings dataTables settings object + * @memberof DataTable#oApi + */ + function _fnAddOptionsHtml ( oSettings ) + { + var classes = oSettings.oClasses; + var table = $(oSettings.nTable); + var holding = $('<div/>').insertBefore( table ); // Holding element for speed + var features = oSettings.oFeatures; + + // All DataTables are wrapped in a div + var insert = $('<div/>', { + id: oSettings.sTableId+'_wrapper', + 'class': classes.sWrapper + (oSettings.nTFoot ? '' : ' '+classes.sNoFooter) + } ); + + oSettings.nHolding = holding[0]; + oSettings.nTableWrapper = insert[0]; + oSettings.nTableReinsertBefore = oSettings.nTable.nextSibling; + + /* Loop over the user set positioning and place the elements as needed */ + var aDom = oSettings.sDom.split(''); + var featureNode, cOption, nNewNode, cNext, sAttr, j; + for ( var i=0 ; i<aDom.length ; i++ ) + { + featureNode = null; + cOption = aDom[i]; + + if ( cOption == '<' ) + { + /* New container div */ + nNewNode = $('<div/>')[0]; + + /* Check to see if we should append an id and/or a class name to the container */ + cNext = aDom[i+1]; + if ( cNext == "'" || cNext == '"' ) + { + sAttr = ""; + j = 2; + while ( aDom[i+j] != cNext ) + { + sAttr += aDom[i+j]; + j++; + } + + /* Replace jQuery UI constants @todo depreciated */ + if ( sAttr == "H" ) + { + sAttr = classes.sJUIHeader; + } + else if ( sAttr == "F" ) + { + sAttr = classes.sJUIFooter; + } + + /* The attribute can be in the format of "#id.class", "#id" or "class" This logic + * breaks the string into parts and applies them as needed + */ + if ( sAttr.indexOf('.') != -1 ) + { + var aSplit = sAttr.split('.'); + nNewNode.id = aSplit[0].substr(1, aSplit[0].length-1); + nNewNode.className = aSplit[1]; + } + else if ( sAttr.charAt(0) == "#" ) + { + nNewNode.id = sAttr.substr(1, sAttr.length-1); + } + else + { + nNewNode.className = sAttr; + } + + i += j; /* Move along the position array */ + } + + insert.append( nNewNode ); + insert = $(nNewNode); + } + else if ( cOption == '>' ) + { + /* End container div */ + insert = insert.parent(); + } + // @todo Move options into their own plugins? + else if ( cOption == 'l' && features.bPaginate && features.bLengthChange ) + { + /* Length */ + featureNode = _fnFeatureHtmlLength( oSettings ); + } + else if ( cOption == 'f' && features.bFilter ) + { + /* Filter */ + featureNode = _fnFeatureHtmlFilter( oSettings ); + } + else if ( cOption == 'r' && features.bProcessing ) + { + /* pRocessing */ + featureNode = _fnFeatureHtmlProcessing( oSettings ); + } + else if ( cOption == 't' ) + { + /* Table */ + featureNode = _fnFeatureHtmlTable( oSettings ); + } + else if ( cOption == 'i' && features.bInfo ) + { + /* Info */ + featureNode = _fnFeatureHtmlInfo( oSettings ); + } + else if ( cOption == 'p' && features.bPaginate ) + { + /* Pagination */ + featureNode = _fnFeatureHtmlPaginate( oSettings ); + } + else if ( DataTable.ext.feature.length !== 0 ) + { + /* Plug-in features */ + var aoFeatures = DataTable.ext.feature; + for ( var k=0, kLen=aoFeatures.length ; k<kLen ; k++ ) + { + if ( cOption == aoFeatures[k].cFeature ) + { + featureNode = aoFeatures[k].fnInit( oSettings ); + break; + } + } + } + + /* Add to the 2D features array */ + if ( featureNode ) + { + var aanFeatures = oSettings.aanFeatures; + + if ( ! aanFeatures[cOption] ) + { + aanFeatures[cOption] = []; + } + + aanFeatures[cOption].push( featureNode ); + insert.append( featureNode ); + } + } + + /* Built our DOM structure - replace the holding div with what we want */ + holding.replaceWith( insert ); + } + + + /** + * Use the DOM source to create up an array of header cells. The idea here is to + * create a layout grid (array) of rows x columns, which contains a reference + * to the cell that that point in the grid (regardless of col/rowspan), such that + * any column / row could be removed and the new grid constructed + * @param array {object} aLayout Array to store the calculated layout in + * @param {node} nThead The header/footer element for the table + * @memberof DataTable#oApi + */ + function _fnDetectHeader ( aLayout, nThead ) + { + var nTrs = $(nThead).children('tr'); + var nTr, nCell; + var i, k, l, iLen, jLen, iColShifted, iColumn, iColspan, iRowspan; + var bUnique; + var fnShiftCol = function ( a, i, j ) { + var k = a[i]; + while ( k[j] ) { + j++; + } + return j; + }; + + aLayout.splice( 0, aLayout.length ); + + /* We know how many rows there are in the layout - so prep it */ + for ( i=0, iLen=nTrs.length ; i<iLen ; i++ ) + { + aLayout.push( [] ); + } + + /* Calculate a layout array */ + for ( i=0, iLen=nTrs.length ; i<iLen ; i++ ) + { + nTr = nTrs[i]; + iColumn = 0; + + /* For every cell in the row... */ + nCell = nTr.firstChild; + while ( nCell ) { + if ( nCell.nodeName.toUpperCase() == "TD" || + nCell.nodeName.toUpperCase() == "TH" ) + { + /* Get the col and rowspan attributes from the DOM and sanitise them */ + iColspan = nCell.getAttribute('colspan') * 1; + iRowspan = nCell.getAttribute('rowspan') * 1; + iColspan = (!iColspan || iColspan===0 || iColspan===1) ? 1 : iColspan; + iRowspan = (!iRowspan || iRowspan===0 || iRowspan===1) ? 1 : iRowspan; + + /* There might be colspan cells already in this row, so shift our target + * accordingly + */ + iColShifted = fnShiftCol( aLayout, i, iColumn ); + + /* Cache calculation for unique columns */ + bUnique = iColspan === 1 ? true : false; + + /* If there is col / rowspan, copy the information into the layout grid */ + for ( l=0 ; l<iColspan ; l++ ) + { + for ( k=0 ; k<iRowspan ; k++ ) + { + aLayout[i+k][iColShifted+l] = { + "cell": nCell, + "unique": bUnique + }; + aLayout[i+k].nTr = nTr; + } + } + } + nCell = nCell.nextSibling; + } + } + } + + + /** + * Get an array of unique th elements, one for each column + * @param {object} oSettings dataTables settings object + * @param {node} nHeader automatically detect the layout from this node - optional + * @param {array} aLayout thead/tfoot layout from _fnDetectHeader - optional + * @returns array {node} aReturn list of unique th's + * @memberof DataTable#oApi + */ + function _fnGetUniqueThs ( oSettings, nHeader, aLayout ) + { + var aReturn = []; + if ( !aLayout ) + { + aLayout = oSettings.aoHeader; + if ( nHeader ) + { + aLayout = []; + _fnDetectHeader( aLayout, nHeader ); + } + } + + for ( var i=0, iLen=aLayout.length ; i<iLen ; i++ ) + { + for ( var j=0, jLen=aLayout[i].length ; j<jLen ; j++ ) + { + if ( aLayout[i][j].unique && + (!aReturn[j] || !oSettings.bSortCellsTop) ) + { + aReturn[j] = aLayout[i][j].cell; + } + } + } + + return aReturn; + } + + + + /** + * Create an Ajax call based on the table's settings, taking into account that + * parameters can have multiple forms, and backwards compatibility. + * + * @param {object} oSettings dataTables settings object + * @param {array} data Data to send to the server, required by + * DataTables - may be augmented by developer callbacks + * @param {function} fn Callback function to run when data is obtained + */ + function _fnBuildAjax( oSettings, data, fn ) + { + // Compatibility with 1.9-, allow fnServerData and event to manipulate + _fnCallbackFire( oSettings, 'aoServerParams', 'serverParams', [data] ); + + // Convert to object based for 1.10+ if using the old array scheme which can + // come from server-side processing or serverParams + if ( data && $.isArray(data) ) { + var tmp = {}; + var rbracket = /(.*?)\[\]$/; + + $.each( data, function (key, val) { + var match = val.name.match(rbracket); + + if ( match ) { + // Support for arrays + var name = match[0]; + + if ( ! tmp[ name ] ) { + tmp[ name ] = []; + } + tmp[ name ].push( val.value ); + } + else { + tmp[val.name] = val.value; + } + } ); + data = tmp; + } + + var ajaxData; + var ajax = oSettings.ajax; + var instance = oSettings.oInstance; + + if ( $.isPlainObject( ajax ) && ajax.data ) + { + ajaxData = ajax.data; + + var newData = $.isFunction( ajaxData ) ? + ajaxData( data ) : // fn can manipulate data or return an object + ajaxData; // object or array to merge + + // If the function returned an object, use that alone + data = $.isFunction( ajaxData ) && newData ? + newData : + $.extend( true, data, newData ); + + // Remove the data property as we've resolved it already and don't want + // jQuery to do it again (it is restored at the end of the function) + delete ajax.data; + } + + var baseAjax = { + "data": data, + "success": function (json) { + var error = json.error || json.sError; + if ( error ) { + oSettings.oApi._fnLog( oSettings, 0, error ); + } + + oSettings.json = json; + _fnCallbackFire( oSettings, null, 'xhr', [oSettings, json] ); + fn( json ); + }, + "dataType": "json", + "cache": false, + "type": oSettings.sServerMethod, + "error": function (xhr, error, thrown) { + var log = oSettings.oApi._fnLog; + + if ( error == "parsererror" ) { + log( oSettings, 0, 'Invalid JSON response', 1 ); + } + else if ( xhr.readyState === 4 ) { + log( oSettings, 0, 'Ajax error', 7 ); + } + + _fnProcessingDisplay( oSettings, false ); + } + }; + + // Store the data submitted for the API + oSettings.oAjaxData = data; + + // Allow plug-ins and external processes to modify the data + _fnCallbackFire( oSettings, null, 'preXhr', [oSettings, data] ); + + if ( oSettings.fnServerData ) + { + // DataTables 1.9- compatibility + oSettings.fnServerData.call( instance, + oSettings.sAjaxSource, + $.map( data, function (val, key) { // Need to convert back to 1.9 trad format + return { name: key, value: val }; + } ), + fn, + oSettings + ); + } + else if ( oSettings.sAjaxSource || typeof ajax === 'string' ) + { + // DataTables 1.9- compatibility + oSettings.jqXHR = $.ajax( $.extend( baseAjax, { + url: ajax || oSettings.sAjaxSource + } ) ); + } + else if ( $.isFunction( ajax ) ) + { + // Is a function - let the caller define what needs to be done + oSettings.jqXHR = ajax.call( instance, data, fn, oSettings ); + } + else + { + // Object to extend the base settings + oSettings.jqXHR = $.ajax( $.extend( baseAjax, ajax ) ); + + // Restore for next time around + ajax.data = ajaxData; + } + } + + + /** + * Update the table using an Ajax call + * @param {object} oSettings dataTables settings object + * @returns {boolean} Block the table drawing or not + * @memberof DataTable#oApi + */ + function _fnAjaxUpdate( oSettings ) + { + if ( oSettings.bAjaxDataGet ) + { + oSettings.iDraw++; + _fnProcessingDisplay( oSettings, true ); + var iColumns = oSettings.aoColumns.length; + var aoData = _fnAjaxParameters( oSettings ); + + _fnBuildAjax( oSettings, aoData, function(json) { + _fnAjaxUpdateDraw( oSettings, json ); + }, oSettings ); + + return false; + } + return true; + } + + + /** + * Build up the parameters in an object needed for a server-side processing + * request. Note that this is basically done twice, is different ways - a modern + * method which is used by default in DataTables 1.10 which uses objects and + * arrays, or the 1.9- method with is name / value pairs. 1.9 method is used if + * the sAjaxSource option is used in the initialisation, or the legacyAjax + * option is set. + * @param {object} oSettings dataTables settings object + * @returns {bool} block the table drawing or not + * @memberof DataTable#oApi + */ + function _fnAjaxParameters( settings ) + { + var + columns = settings.aoColumns, + columnCount = columns.length, + features = settings.oFeatures, + preSearch = settings.oPreviousSearch, + preColSearch = settings.aoPreSearchCols, + i, data = [], dataProp, column, columnSearch, + sort = _fnSortFlatten( settings ), + displayStart = settings._iDisplayStart, + displayLength = features.bPaginate !== false ? + settings._iDisplayLength : + -1; + + var param = function ( name, value ) { + data.push( { 'name': name, 'value': value } ); + }; + + // DataTables 1.9- compatible method + param( 'sEcho', settings.iDraw ); + param( 'iColumns', columnCount ); + param( 'sColumns', _pluck( columns, 'sName' ).join(',') ); + param( 'iDisplayStart', displayStart ); + param( 'iDisplayLength', displayLength ); + + // DataTables 1.10+ method + var d = { + draw: settings.iDraw, + columns: [], + order: [], + start: displayStart, + length: displayLength, + search: { + value: preSearch.sSearch, + regex: preSearch.bRegex + } + }; + + for ( i=0 ; i<columnCount ; i++ ) { + column = columns[i]; + columnSearch = preColSearch[i]; + dataProp = typeof column.mData=="function" ? 'function' : column.mData ; + + d.columns.push( { + data: dataProp, + name: column.sName, + searchable: column.bSearchable, + orderable: column.bSortable, + search: { + value: columnSearch.sSearch, + regex: columnSearch.bRegex + } + } ); + + param( "mDataProp_"+i, dataProp ); + + if ( features.bFilter ) { + param( 'sSearch_'+i, columnSearch.sSearch ); + param( 'bRegex_'+i, columnSearch.bRegex ); + param( 'bSearchable_'+i, column.bSearchable ); + } + + if ( features.bSort ) { + param( 'bSortable_'+i, column.bSortable ); + } + } + + if ( features.bFilter ) { + param( 'sSearch', preSearch.sSearch ); + param( 'bRegex', preSearch.bRegex ); + } + + if ( features.bSort ) { + $.each( sort, function ( i, val ) { + d.order.push( { column: val.col, dir: val.dir } ); + + param( 'iSortCol_'+i, val.col ); + param( 'sSortDir_'+i, val.dir ); + } ); + + param( 'iSortingCols', sort.length ); + } + + // If the legacy.ajax parameter is null, then we automatically decide which + // form to use, based on sAjaxSource + var legacy = DataTable.ext.legacy.ajax; + if ( legacy === null ) { + return settings.sAjaxSource ? data : d; + } + + // Otherwise, if legacy has been specified then we use that to decide on the + // form + return legacy ? data : d; + } + + + /** + * Data the data from the server (nuking the old) and redraw the table + * @param {object} oSettings dataTables settings object + * @param {object} json json data return from the server. + * @param {string} json.sEcho Tracking flag for DataTables to match requests + * @param {int} json.iTotalRecords Number of records in the data set, not accounting for filtering + * @param {int} json.iTotalDisplayRecords Number of records in the data set, accounting for filtering + * @param {array} json.aaData The data to display on this page + * @param {string} [json.sColumns] Column ordering (sName, comma separated) + * @memberof DataTable#oApi + */ + function _fnAjaxUpdateDraw ( settings, json ) + { + // v1.10 uses camelCase variables, while 1.9 uses Hungarian notation. + // Support both + var compat = function ( old, modern ) { + return json[old] !== undefined ? json[old] : json[modern]; + }; + + var draw = compat( 'sEcho', 'draw' ); + var recordsTotal = compat( 'iTotalRecords', 'recordsTotal' ); + var rocordsFiltered = compat( 'iTotalDisplayRecords', 'recordsFiltered' ); + + if ( draw ) { + // Protect against out of sequence returns + if ( draw*1 < settings.iDraw ) { + return; + } + settings.iDraw = draw * 1; + } + + _fnClearTable( settings ); + settings._iRecordsTotal = parseInt(recordsTotal, 10); + settings._iRecordsDisplay = parseInt(rocordsFiltered, 10); + + var data = _fnAjaxDataSrc( settings, json ); + for ( var i=0, ien=data.length ; i<ien ; i++ ) { + _fnAddData( settings, data[i] ); + } + settings.aiDisplay = settings.aiDisplayMaster.slice(); + + settings.bAjaxDataGet = false; + _fnDraw( settings ); + + if ( ! settings._bInitComplete ) { + _fnInitComplete( settings, json ); + } + + settings.bAjaxDataGet = true; + _fnProcessingDisplay( settings, false ); + } + + + /** + * Get the data from the JSON data source to use for drawing a table. Using + * `_fnGetObjectDataFn` allows the data to be sourced from a property of the + * source object, or from a processing function. + * @param {object} oSettings dataTables settings object + * @param {object} json Data source object / array from the server + * @return {array} Array of data to use + */ + function _fnAjaxDataSrc ( oSettings, json ) + { + var dataSrc = $.isPlainObject( oSettings.ajax ) && oSettings.ajax.dataSrc !== undefined ? + oSettings.ajax.dataSrc : + oSettings.sAjaxDataProp; // Compatibility with 1.9-. + + // Compatibility with 1.9-. In order to read from aaData, check if the + // default has been changed, if not, check for aaData + if ( dataSrc === 'data' ) { + return json.aaData || json[dataSrc]; + } + + return dataSrc !== "" ? + _fnGetObjectDataFn( dataSrc )( json ) : + json; + } + + + /** + * Generate the node required for filtering text + * @returns {node} Filter control element + * @param {object} oSettings dataTables settings object + * @memberof DataTable#oApi + */ + function _fnFeatureHtmlFilter ( settings ) + { + var classes = settings.oClasses; + var tableId = settings.sTableId; + var previousSearch = settings.oPreviousSearch; + var features = settings.aanFeatures; + var input = '<input type="search" class="'+classes.sFilterInput+'"/>'; + + var str = settings.oLanguage.sSearch; + str = str.match(/_INPUT_/) ? + str.replace('_INPUT_', input) : + str+input; + + var filter = $('<div/>', { + 'id': ! features.f ? tableId+'_filter' : null, + 'class': classes.sFilter + } ) + .append( $('<label/>' ).append( str ) ); + + var searchFn = function() { + /* Update all other filter input elements for the new display */ + var n = features.f; + var val = !this.value ? "" : this.value; // mental IE8 fix :-( + + /* Now do the filter */ + if ( val != previousSearch.sSearch ) { + _fnFilterComplete( settings, { + "sSearch": val, + "bRegex": previousSearch.bRegex, + "bSmart": previousSearch.bSmart , + "bCaseInsensitive": previousSearch.bCaseInsensitive + } ); + + // Need to redraw, without resorting + settings._iDisplayStart = 0; + _fnDraw( settings ); + } + }; + var jqFilter = $('input', filter) + .val( previousSearch.sSearch.replace('"','"') ) + .bind( + 'keyup.DT search.DT input.DT paste.DT cut.DT', + _fnDataSource( settings ) === 'ssp' ? + _fnThrottle( searchFn, 400 ): + searchFn + ) + .bind( 'keypress.DT', function(e) { + /* Prevent form submission */ + if ( e.keyCode == 13 ) { + return false; + } + } ) + .attr('aria-controls', tableId); + + // Update the input elements whenever the table is filtered + $(settings.nTable).on( 'filter.DT', function () { + // IE9 throws an 'unknown error' if document.activeElement is used + // inside an iframe or frame... + try { + if ( jqFilter[0] !== document.activeElement ) { + jqFilter.val( previousSearch.sSearch ); + } + } + catch ( e ) {} + } ); + + return filter[0]; + } + + + /** + * Filter the table using both the global filter and column based filtering + * @param {object} oSettings dataTables settings object + * @param {object} oSearch search information + * @param {int} [iForce] force a research of the master array (1) or not (undefined or 0) + * @memberof DataTable#oApi + */ + function _fnFilterComplete ( oSettings, oInput, iForce ) + { + var oPrevSearch = oSettings.oPreviousSearch; + var aoPrevSearch = oSettings.aoPreSearchCols; + var fnSaveFilter = function ( oFilter ) { + /* Save the filtering values */ + oPrevSearch.sSearch = oFilter.sSearch; + oPrevSearch.bRegex = oFilter.bRegex; + oPrevSearch.bSmart = oFilter.bSmart; + oPrevSearch.bCaseInsensitive = oFilter.bCaseInsensitive; + }; + var fnRegex = function ( o ) { + // Backwards compatibility with the bEscapeRegex option + return o.bEscapeRegex !== undefined ? !o.bEscapeRegex : o.bRegex; + }; + + // Resolve any column types that are unknown due to addition or invalidation + // @todo As per sort - can this be moved into an event handler? + _fnColumnTypes( oSettings ); + + /* In server-side processing all filtering is done by the server, so no point hanging around here */ + if ( _fnDataSource( oSettings ) != 'ssp' ) + { + /* Global filter */ + _fnFilter( oSettings, oInput.sSearch, iForce, fnRegex(oInput), oInput.bSmart, oInput.bCaseInsensitive ); + fnSaveFilter( oInput ); + + /* Now do the individual column filter */ + for ( var i=0 ; i<aoPrevSearch.length ; i++ ) + { + _fnFilterColumn( oSettings, aoPrevSearch[i].sSearch, i, fnRegex(aoPrevSearch[i]), + aoPrevSearch[i].bSmart, aoPrevSearch[i].bCaseInsensitive ); + } + + /* Custom filtering */ + _fnFilterCustom( oSettings ); + } + else + { + fnSaveFilter( oInput ); + } + + /* Tell the draw function we have been filtering */ + oSettings.bFiltered = true; + _fnCallbackFire( oSettings, null, 'search', [oSettings] ); + } + + + /** + * Apply custom filtering functions + * @param {object} oSettings dataTables settings object + * @memberof DataTable#oApi + */ + function _fnFilterCustom( settings ) + { + var filters = DataTable.ext.search; + var displayRows = settings.aiDisplay; + var row, rowIdx; + + for ( var i=0, iLen=filters.length ; i<iLen ; i++ ) { + for ( var j=displayRows.length-1 ; j>=0 ; j-- ) { + rowIdx = displayRows[ j ]; + row = settings.aoData[ rowIdx ]; + + if ( ! filters[i]( settings, row._aFilterData, rowIdx, row._aData ) ) { + displayRows.splice( j, 1 ); + } + } + } + } + + + /** + * Filter the table on a per-column basis + * @param {object} oSettings dataTables settings object + * @param {string} sInput string to filter on + * @param {int} iColumn column to filter + * @param {bool} bRegex treat search string as a regular expression or not + * @param {bool} bSmart use smart filtering or not + * @param {bool} bCaseInsensitive Do case insenstive matching or not + * @memberof DataTable#oApi + */ + function _fnFilterColumn ( settings, searchStr, colIdx, regex, smart, caseInsensitive ) + { + if ( searchStr === '' ) { + return; + } + + var data; + var display = settings.aiDisplay; + var rpSearch = _fnFilterCreateSearch( searchStr, regex, smart, caseInsensitive ); + + for ( var i=display.length-1 ; i>=0 ; i-- ) { + data = settings.aoData[ display[i] ]._aFilterData[ colIdx ]; + + if ( ! rpSearch.test( data ) ) { + display.splice( i, 1 ); + } + } + } + + + /** + * Filter the data table based on user input and draw the table + * @param {object} settings dataTables settings object + * @param {string} input string to filter on + * @param {int} force optional - force a research of the master array (1) or not (undefined or 0) + * @param {bool} regex treat as a regular expression or not + * @param {bool} smart perform smart filtering or not + * @param {bool} caseInsensitive Do case insenstive matching or not + * @memberof DataTable#oApi + */ + function _fnFilter( settings, input, force, regex, smart, caseInsensitive ) + { + var rpSearch = _fnFilterCreateSearch( input, regex, smart, caseInsensitive ); + var prevSearch = settings.oPreviousSearch.sSearch; + var displayMaster = settings.aiDisplayMaster; + var display, invalidated, i; + + // Need to take account of custom filtering functions - always filter + if ( DataTable.ext.search.length !== 0 ) { + force = true; + } + + // Check if any of the rows were invalidated + invalidated = _fnFilterData( settings ); + + // If the input is blank - we just want the full data set + if ( input.length <= 0 ) { + settings.aiDisplay = displayMaster.slice(); + } + else { + // New search - start from the master array + if ( invalidated || + force || + prevSearch.length > input.length || + input.indexOf(prevSearch) !== 0 || + settings.bSorted // On resort, the display master needs to be + // re-filtered since indexes will have changed + ) { + settings.aiDisplay = displayMaster.slice(); + } + + // Search the display array + display = settings.aiDisplay; + + for ( i=display.length-1 ; i>=0 ; i-- ) { + if ( ! rpSearch.test( settings.aoData[ display[i] ]._sFilterRow ) ) { + display.splice( i, 1 ); + } + } + } + } + + + /** + * Build a regular expression object suitable for searching a table + * @param {string} sSearch string to search for + * @param {bool} bRegex treat as a regular expression or not + * @param {bool} bSmart perform smart filtering or not + * @param {bool} bCaseInsensitive Do case insensitive matching or not + * @returns {RegExp} constructed object + * @memberof DataTable#oApi + */ + function _fnFilterCreateSearch( search, regex, smart, caseInsensitive ) + { + search = regex ? + search : + _fnEscapeRegex( search ); + + if ( smart ) { + /* For smart filtering we want to allow the search to work regardless of + * word order. We also want double quoted text to be preserved, so word + * order is important - a la google. So this is what we want to + * generate: + * + * ^(?=.*?\bone\b)(?=.*?\btwo three\b)(?=.*?\bfour\b).*$ + */ + var a = $.map( search.match( /"[^"]+"|[^ ]+/g ) || '', function ( word ) { + return word.charAt(0) === '"' ? + word.match( /^"(.*)"$/ )[1] : + word; + } ); + + search = '^(?=.*?'+a.join( ')(?=.*?' )+').*$'; + } + + return new RegExp( search, caseInsensitive ? 'i' : '' ); + } + + + /** + * scape a string such that it can be used in a regular expression + * @param {string} sVal string to escape + * @returns {string} escaped string + * @memberof DataTable#oApi + */ + function _fnEscapeRegex ( sVal ) + { + return sVal.replace( _re_escape_regex, '\\$1' ); + } + + + + var __filter_div = $('<div>')[0]; + var __filter_div_textContent = __filter_div.textContent !== undefined; + + // Update the filtering data for each row if needed (by invalidation or first run) + function _fnFilterData ( settings ) + { + var columns = settings.aoColumns; + var column; + var i, j, ien, jen, filterData, cellData, row; + var fomatters = DataTable.ext.type.search; + var wasInvalidated = false; + + for ( i=0, ien=settings.aoData.length ; i<ien ; i++ ) { + row = settings.aoData[i]; + + if ( ! row._aFilterData ) { + filterData = []; + + for ( j=0, jen=columns.length ; j<jen ; j++ ) { + column = columns[j]; + + if ( column.bSearchable ) { + cellData = _fnGetCellData( settings, i, j, 'filter' ); + + cellData = fomatters[ column.sType ] ? + fomatters[ column.sType ]( cellData ) : + cellData !== null ? + cellData : + ''; + } + else { + cellData = ''; + } + + // If it looks like there is an HTML entity in the string, + // attempt to decode it so sorting works as expected. Note that + // we could use a single line of jQuery to do this, but the DOM + // method used here is much faster http://jsperf.com/html-decode + if ( cellData.indexOf && cellData.indexOf('&') !== -1 ) { + __filter_div.innerHTML = cellData; + cellData = __filter_div_textContent ? + __filter_div.textContent : + __filter_div.innerText; + } + + if ( cellData.replace ) { + cellData = cellData.replace(/[\r\n]/g, ''); + } + + filterData.push( cellData ); + } + + row._aFilterData = filterData; + row._sFilterRow = filterData.join(' '); + wasInvalidated = true; + } + } + + return wasInvalidated; + } + + /** + * Generate the node required for the info display + * @param {object} oSettings dataTables settings object + * @returns {node} Information element + * @memberof DataTable#oApi + */ + function _fnFeatureHtmlInfo ( settings ) + { + var + tid = settings.sTableId, + nodes = settings.aanFeatures.i, + n = $('<div/>', { + 'class': settings.oClasses.sInfo, + 'id': ! nodes ? tid+'_info' : null + } ); + + if ( ! nodes ) { + // Update display on each draw + settings.aoDrawCallback.push( { + "fn": _fnUpdateInfo, + "sName": "information" + } ); + + n + .attr( 'role', 'status' ) + .attr( 'aria-live', 'polite' ); + + // Table is described by our info div + $(settings.nTable).attr( 'aria-describedby', tid+'_info' ); + } + + return n[0]; + } + + + /** + * Update the information elements in the display + * @param {object} settings dataTables settings object + * @memberof DataTable#oApi + */ + function _fnUpdateInfo ( settings ) + { + /* Show information about the table */ + var nodes = settings.aanFeatures.i; + if ( nodes.length === 0 ) { + return; + } + + var + lang = settings.oLanguage, + start = settings._iDisplayStart+1, + end = settings.fnDisplayEnd(), + max = settings.fnRecordsTotal(), + total = settings.fnRecordsDisplay(), + out = total ? + lang.sInfo : + lang.sInfoEmpty; + + if ( total !== max ) { + /* Record set after filtering */ + out += ' ' + lang.sInfoFiltered; + } + + // Convert the macros + out += lang.sInfoPostFix; + out = _fnInfoMacros( settings, out ); + + var callback = lang.fnInfoCallback; + if ( callback !== null ) { + out = callback.call( settings.oInstance, + settings, start, end, max, total, out + ); + } + + $(nodes).html( out ); + } + + + function _fnInfoMacros ( settings, str ) + { + // When infinite scrolling, we are always starting at 1. _iDisplayStart is used only + // internally + var + formatter = settings.fnFormatNumber, + start = settings._iDisplayStart+1, + len = settings._iDisplayLength, + vis = settings.fnRecordsDisplay(), + all = len === -1; + + return str. + replace(/_START_/g, formatter.call( settings, start ) ). + replace(/_END_/g, formatter.call( settings, settings.fnDisplayEnd() ) ). + replace(/_MAX_/g, formatter.call( settings, settings.fnRecordsTotal() ) ). + replace(/_TOTAL_/g, formatter.call( settings, vis ) ). + replace(/_PAGE_/g, formatter.call( settings, all ? 1 : Math.ceil( start / len ) ) ). + replace(/_PAGES_/g, formatter.call( settings, all ? 1 : Math.ceil( vis / len ) ) ); + } + + + + /** + * Draw the table for the first time, adding all required features + * @param {object} settings dataTables settings object + * @memberof DataTable#oApi + */ + function _fnInitialise ( settings ) + { + var i, iLen, iAjaxStart=settings.iInitDisplayStart; + var columns = settings.aoColumns, column; + var features = settings.oFeatures; + + /* Ensure that the table data is fully initialised */ + if ( ! settings.bInitialised ) { + setTimeout( function(){ _fnInitialise( settings ); }, 200 ); + return; + } + + /* Show the display HTML options */ + _fnAddOptionsHtml( settings ); + + /* Build and draw the header / footer for the table */ + _fnBuildHead( settings ); + _fnDrawHead( settings, settings.aoHeader ); + _fnDrawHead( settings, settings.aoFooter ); + + /* Okay to show that something is going on now */ + _fnProcessingDisplay( settings, true ); + + /* Calculate sizes for columns */ + if ( features.bAutoWidth ) { + _fnCalculateColumnWidths( settings ); + } + + for ( i=0, iLen=columns.length ; i<iLen ; i++ ) { + column = columns[i]; + + if ( column.sWidth ) { + column.nTh.style.width = _fnStringToCss( column.sWidth ); + } + } + + // If there is default sorting required - let's do it. The sort function + // will do the drawing for us. Otherwise we draw the table regardless of the + // Ajax source - this allows the table to look initialised for Ajax sourcing + // data (show 'loading' message possibly) + _fnReDraw( settings ); + + // Server-side processing init complete is done by _fnAjaxUpdateDraw + var dataSrc = _fnDataSource( settings ); + if ( dataSrc != 'ssp' ) { + // if there is an ajax source load the data + if ( dataSrc == 'ajax' ) { + _fnBuildAjax( settings, [], function(json) { + var aData = _fnAjaxDataSrc( settings, json ); + + // Got the data - add it to the table + for ( i=0 ; i<aData.length ; i++ ) { + _fnAddData( settings, aData[i] ); + } + + // Reset the init display for cookie saving. We've already done + // a filter, and therefore cleared it before. So we need to make + // it appear 'fresh' + settings.iInitDisplayStart = iAjaxStart; + + _fnReDraw( settings ); + + _fnProcessingDisplay( settings, false ); + _fnInitComplete( settings, json ); + }, settings ); + } + else { + _fnProcessingDisplay( settings, false ); + _fnInitComplete( settings ); + } + } + } + + + /** + * Draw the table for the first time, adding all required features + * @param {object} oSettings dataTables settings object + * @param {object} [json] JSON from the server that completed the table, if using Ajax source + * with client-side processing (optional) + * @memberof DataTable#oApi + */ + function _fnInitComplete ( settings, json ) + { + settings._bInitComplete = true; + + // On an Ajax load we now have data and therefore want to apply the column + // sizing + if ( json ) { + _fnAdjustColumnSizing( settings ); + } + + _fnCallbackFire( settings, 'aoInitComplete', 'init', [settings, json] ); + } + + + function _fnLengthChange ( settings, val ) + { + var len = parseInt( val, 10 ); + settings._iDisplayLength = len; + + _fnLengthOverflow( settings ); + + // Fire length change event + _fnCallbackFire( settings, null, 'length', [settings, len] ); + } + + + /** + * Generate the node required for user display length changing + * @param {object} settings dataTables settings object + * @returns {node} Display length feature node + * @memberof DataTable#oApi + */ + function _fnFeatureHtmlLength ( settings ) + { + var + classes = settings.oClasses, + tableId = settings.sTableId, + menu = settings.aLengthMenu, + d2 = $.isArray( menu[0] ), + lengths = d2 ? menu[0] : menu, + language = d2 ? menu[1] : menu; + + var select = $('<select/>', { + 'name': tableId+'_length', + 'aria-controls': tableId, + 'class': classes.sLengthSelect + } ); + + for ( var i=0, ien=lengths.length ; i<ien ; i++ ) { + select[0][ i ] = new Option( language[i], lengths[i] ); + } + + var div = $('<div><label/></div>').addClass( classes.sLength ); + if ( ! settings.aanFeatures.l ) { + div[0].id = tableId+'_length'; + } + + var a = settings.oLanguage.sLengthMenu.split(/(_MENU_)/); + div.children().append( a.length > 1 ? + [ a[0], select, a[2] ] : + a[0] + ); + + // Can't use `select` variable, as user might provide their own select menu + $('select', div) + .val( settings._iDisplayLength ) + .bind( 'change.DT', function(e) { + _fnLengthChange( settings, $(this).val() ); + _fnDraw( settings ); + } ); + + // Update node value whenever anything changes the table's length + $(settings.nTable).bind( 'length.dt.DT', function (e, s, len) { + $('select', div).val( len ); + } ); + + return div[0]; + } + + + + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Note that most of the paging logic is done in + * DataTable.ext.pager + */ + + /** + * Generate the node required for default pagination + * @param {object} oSettings dataTables settings object + * @returns {node} Pagination feature node + * @memberof DataTable#oApi + */ + function _fnFeatureHtmlPaginate ( settings ) + { + var + type = settings.sPaginationType, + plugin = DataTable.ext.pager[ type ], + modern = typeof plugin === 'function', + redraw = function( settings ) { + _fnDraw( settings ); + }, + node = $('<div/>').addClass( settings.oClasses.sPaging + type )[0], + features = settings.aanFeatures; + + if ( ! modern ) { + plugin.fnInit( settings, node, redraw ); + } + + /* Add a draw callback for the pagination on first instance, to update the paging display */ + if ( ! features.p ) + { + node.id = settings.sTableId+'_paginate'; + + settings.aoDrawCallback.push( { + "fn": function( settings ) { + if ( modern ) { + var + start = settings._iDisplayStart, + len = settings._iDisplayLength, + visRecords = settings.fnRecordsDisplay(), + all = len === -1, + page = all ? 0 : Math.ceil( start / len ), + pages = all ? 1 : Math.ceil( visRecords / len ), + buttons = plugin(page, pages), + i, ien; + + for ( i=0, ien=features.p.length ; i<ien ; i++ ) { + _fnRenderer( settings, 'pageButton' )( + settings, features.p[i], i, buttons, page, pages + ); + } + } + else { + plugin.fnUpdate( settings, redraw ); + } + }, + "sName": "pagination" + } ); + } + + return node; + } + + + /** + * Alter the display settings to change the page + * @param {object} settings DataTables settings object + * @param {string|int} action Paging action to take: "first", "previous", + * "next" or "last" or page number to jump to (integer) + * @param [bool] redraw Automatically draw the update or not + * @returns {bool} true page has changed, false - no change + * @memberof DataTable#oApi + */ + function _fnPageChange ( settings, action, redraw ) + { + var + start = settings._iDisplayStart, + len = settings._iDisplayLength, + records = settings.fnRecordsDisplay(); + + if ( records === 0 || len === -1 ) + { + start = 0; + } + else if ( typeof action === "number" ) + { + start = action * len; + + if ( start > records ) + { + start = 0; + } + } + else if ( action == "first" ) + { + start = 0; + } + else if ( action == "previous" ) + { + start = len >= 0 ? + start - len : + 0; + + if ( start < 0 ) + { + start = 0; + } + } + else if ( action == "next" ) + { + if ( start + len < records ) + { + start += len; + } + } + else if ( action == "last" ) + { + start = Math.floor( (records-1) / len) * len; + } + else + { + _fnLog( settings, 0, "Unknown paging action: "+action, 5 ); + } + + var changed = settings._iDisplayStart !== start; + settings._iDisplayStart = start; + + if ( changed ) { + _fnCallbackFire( settings, null, 'page', [settings] ); + + if ( redraw ) { + _fnDraw( settings ); + } + } + + return changed; + } + + + + /** + * Generate the node required for the processing node + * @param {object} settings dataTables settings object + * @returns {node} Processing element + * @memberof DataTable#oApi + */ + function _fnFeatureHtmlProcessing ( settings ) + { + return $('<div/>', { + 'id': ! settings.aanFeatures.r ? settings.sTableId+'_processing' : null, + 'class': settings.oClasses.sProcessing + } ) + .html( settings.oLanguage.sProcessing ) + .insertBefore( settings.nTable )[0]; + } + + + /** + * Display or hide the processing indicator + * @param {object} settings dataTables settings object + * @param {bool} show Show the processing indicator (true) or not (false) + * @memberof DataTable#oApi + */ + function _fnProcessingDisplay ( settings, show ) + { + if ( settings.oFeatures.bProcessing ) { + $(settings.aanFeatures.r).css( 'display', show ? 'block' : 'none' ); + $(settings.nTable).css( 'opacity', show ? '0.4' : '1' ); + } + + _fnCallbackFire( settings, null, 'processing', [settings, show] ); + } + + /** + * Add any control elements for the table - specifically scrolling + * @param {object} settings dataTables settings object + * @returns {node} Node to add to the DOM + * @memberof DataTable#oApi + */ + function _fnFeatureHtmlTable ( settings ) + { + var table = $(settings.nTable); + + // Add the ARIA grid role to the table + table.attr( 'role', 'grid' ); + + // Scrolling from here on in + var scroll = settings.oScroll; + + if ( scroll.sX === '' && scroll.sY === '' ) { + return settings.nTable; + } + + var scrollX = scroll.sX; + var scrollY = scroll.sY; + var classes = settings.oClasses; + var caption = table.children('caption'); + var captionSide = caption.length ? caption[0]._captionSide : null; + var headerClone = $( table[0].cloneNode(false) ); + var footerClone = $( table[0].cloneNode(false) ); + var footer = table.children('tfoot'); + var _div = '<div/>'; + var size = function ( s ) { + return !s ? null : _fnStringToCss( s ); + }; + + // This is fairly messy, but with x scrolling enabled, if the table has a + // width attribute, regardless of any width applied using the column width + // options, the browser will shrink or grow the table as needed to fit into + // that 100%. That would make the width options useless. So we remove it. + // This is okay, under the assumption that width:100% is applied to the + // table in CSS (it is in the default stylesheet) which will set the table + // width as appropriate (the attribute and css behave differently...) + if ( scroll.sX && table.attr('width') === '100%' ) { + table.removeAttr('width'); + } + + if ( ! footer.length ) { + footer = null; + } + + /* + * The HTML structure that we want to generate in this function is: + * div - scroller + * div - scroll head + * div - scroll head inner + * table - scroll head table + * thead - thead + * div - scroll body + * table - table (master table) + * thead - thead clone for sizing + * tbody - tbody + * div - scroll foot + * div - scroll foot inner + * table - scroll foot table + * tfoot - tfoot + */ + var scroller = $( _div, { 'class': classes.sScrollWrapper } ) + .append( + $(_div, { 'class': classes.sScrollHead } ) + .css( { + overflow: 'hidden', + position: 'relative', + border: 0, + width: scrollX ? size(scrollX) : '100%' + } ) + .append( + $(_div, { 'class': classes.sScrollHeadInner } ) + .css( { + 'box-sizing': 'content-box', + width: scroll.sXInner || '100%' + } ) + .append( + headerClone + .removeAttr('id') + .css( 'margin-left', 0 ) + .append( + table.children('thead') + ) + ) + ) + .append( captionSide === 'top' ? caption : null ) + ) + .append( + $(_div, { 'class': classes.sScrollBody } ) + .css( { + overflow: 'auto', + height: size( scrollY ), + width: size( scrollX ) + } ) + .append( table ) + ); + + if ( footer ) { + scroller.append( + $(_div, { 'class': classes.sScrollFoot } ) + .css( { + overflow: 'hidden', + border: 0, + width: scrollX ? size(scrollX) : '100%' + } ) + .append( + $(_div, { 'class': classes.sScrollFootInner } ) + .append( + footerClone + .removeAttr('id') + .css( 'margin-left', 0 ) + .append( + table.children('tfoot') + ) + ) + ) + .append( captionSide === 'bottom' ? caption : null ) + ); + } + + var children = scroller.children(); + var scrollHead = children[0]; + var scrollBody = children[1]; + var scrollFoot = footer ? children[2] : null; + + // When the body is scrolled, then we also want to scroll the headers + if ( scrollX ) { + $(scrollBody).scroll( function (e) { + var scrollLeft = this.scrollLeft; + + scrollHead.scrollLeft = scrollLeft; + + if ( footer ) { + scrollFoot.scrollLeft = scrollLeft; + } + } ); + } + + settings.nScrollHead = scrollHead; + settings.nScrollBody = scrollBody; + settings.nScrollFoot = scrollFoot; + + // On redraw - align columns + settings.aoDrawCallback.push( { + "fn": _fnScrollDraw, + "sName": "scrolling" + } ); + + return scroller[0]; + } + + + + /** + * Update the header, footer and body tables for resizing - i.e. column + * alignment. + * + * Welcome to the most horrible function DataTables. The process that this + * function follows is basically: + * 1. Re-create the table inside the scrolling div + * 2. Take live measurements from the DOM + * 3. Apply the measurements to align the columns + * 4. Clean up + * + * @param {object} settings dataTables settings object + * @memberof DataTable#oApi + */ + function _fnScrollDraw ( settings ) + { + // Given that this is such a monster function, a lot of variables are use + // to try and keep the minimised size as small as possible + var + scroll = settings.oScroll, + scrollX = scroll.sX, + scrollXInner = scroll.sXInner, + scrollY = scroll.sY, + barWidth = scroll.iBarWidth, + divHeader = $(settings.nScrollHead), + divHeaderStyle = divHeader[0].style, + divHeaderInner = divHeader.children('div'), + divHeaderInnerStyle = divHeaderInner[0].style, + divHeaderTable = divHeaderInner.children('table'), + divBodyEl = settings.nScrollBody, + divBody = $(divBodyEl), + divBodyStyle = divBodyEl.style, + divFooter = $(settings.nScrollFoot), + divFooterInner = divFooter.children('div'), + divFooterTable = divFooterInner.children('table'), + header = $(settings.nTHead), + table = $(settings.nTable), + tableEl = table[0], + tableStyle = tableEl.style, + footer = settings.nTFoot ? $(settings.nTFoot) : null, + browser = settings.oBrowser, + ie67 = browser.bScrollOversize, + headerTrgEls, footerTrgEls, + headerSrcEls, footerSrcEls, + headerCopy, footerCopy, + headerWidths=[], footerWidths=[], + headerContent=[], + idx, correction, sanityWidth, + zeroOut = function(nSizer) { + var style = nSizer.style; + style.paddingTop = "0"; + style.paddingBottom = "0"; + style.borderTopWidth = "0"; + style.borderBottomWidth = "0"; + style.height = 0; + }; + + /* + * 1. Re-create the table inside the scrolling div + */ + + // Remove the old minimised thead and tfoot elements in the inner table + table.children('thead, tfoot').remove(); + + // Clone the current header and footer elements and then place it into the inner table + headerCopy = header.clone().prependTo( table ); + headerTrgEls = header.find('tr'); // original header is in its own table + headerSrcEls = headerCopy.find('tr'); + headerCopy.find('th, td').removeAttr('tabindex'); + + if ( footer ) { + footerCopy = footer.clone().prependTo( table ); + footerTrgEls = footer.find('tr'); // the original tfoot is in its own table and must be sized + footerSrcEls = footerCopy.find('tr'); + } + + + /* + * 2. Take live measurements from the DOM - do not alter the DOM itself! + */ + + // Remove old sizing and apply the calculated column widths + // Get the unique column headers in the newly created (cloned) header. We want to apply the + // calculated sizes to this header + if ( ! scrollX ) + { + divBodyStyle.width = '100%'; + divHeader[0].style.width = '100%'; + } + + $.each( _fnGetUniqueThs( settings, headerCopy ), function ( i, el ) { + idx = _fnVisibleToColumnIndex( settings, i ); + el.style.width = settings.aoColumns[idx].sWidth; + } ); + + if ( footer ) { + _fnApplyToChildren( function(n) { + n.style.width = ""; + }, footerSrcEls ); + } + + // If scroll collapse is enabled, when we put the headers back into the body for sizing, we + // will end up forcing the scrollbar to appear, making our measurements wrong for when we + // then hide it (end of this function), so add the header height to the body scroller. + if ( scroll.bCollapse && scrollY !== "" ) { + divBodyStyle.height = (divBody[0].offsetHeight + header[0].offsetHeight)+"px"; + } + + // Size the table as a whole + sanityWidth = table.outerWidth(); + if ( scrollX === "" ) { + // No x scrolling + tableStyle.width = "100%"; + + // IE7 will make the width of the table when 100% include the scrollbar + // - which is shouldn't. When there is a scrollbar we need to take this + // into account. + if ( ie67 && (table.find('tbody').height() > divBodyEl.offsetHeight || + divBody.css('overflow-y') == "scroll") + ) { + tableStyle.width = _fnStringToCss( table.outerWidth() - barWidth); + } + } + else + { + // x scrolling + if ( scrollXInner !== "" ) { + // x scroll inner has been given - use it + tableStyle.width = _fnStringToCss(scrollXInner); + } + else if ( sanityWidth == divBody.width() && divBody.height() < table.height() ) { + // There is y-scrolling - try to take account of the y scroll bar + tableStyle.width = _fnStringToCss( sanityWidth-barWidth ); + if ( table.outerWidth() > sanityWidth-barWidth ) { + // Not possible to take account of it + tableStyle.width = _fnStringToCss( sanityWidth ); + } + } + else { + // When all else fails + tableStyle.width = _fnStringToCss( sanityWidth ); + } + } + + // Recalculate the sanity width - now that we've applied the required width, + // before it was a temporary variable. This is required because the column + // width calculation is done before this table DOM is created. + sanityWidth = table.outerWidth(); + + // Hidden header should have zero height, so remove padding and borders. Then + // set the width based on the real headers + + // Apply all styles in one pass + _fnApplyToChildren( zeroOut, headerSrcEls ); + + // Read all widths in next pass + _fnApplyToChildren( function(nSizer) { + headerContent.push( nSizer.innerHTML ); + headerWidths.push( _fnStringToCss( $(nSizer).css('width') ) ); + }, headerSrcEls ); + + // Apply all widths in final pass + _fnApplyToChildren( function(nToSize, i) { + nToSize.style.width = headerWidths[i]; + }, headerTrgEls ); + + $(headerSrcEls).height(0); + + /* Same again with the footer if we have one */ + if ( footer ) + { + _fnApplyToChildren( zeroOut, footerSrcEls ); + + _fnApplyToChildren( function(nSizer) { + footerWidths.push( _fnStringToCss( $(nSizer).css('width') ) ); + }, footerSrcEls ); + + _fnApplyToChildren( function(nToSize, i) { + nToSize.style.width = footerWidths[i]; + }, footerTrgEls ); + + $(footerSrcEls).height(0); + } + + + /* + * 3. Apply the measurements + */ + + // "Hide" the header and footer that we used for the sizing. We need to keep + // the content of the cell so that the width applied to the header and body + // both match, but we want to hide it completely. We want to also fix their + // width to what they currently are + _fnApplyToChildren( function(nSizer, i) { + nSizer.innerHTML = '<div class="dataTables_sizing" style="height:0;overflow:hidden;">'+headerContent[i]+'</div>'; + nSizer.style.width = headerWidths[i]; + }, headerSrcEls ); + + if ( footer ) + { + _fnApplyToChildren( function(nSizer, i) { + nSizer.innerHTML = ""; + nSizer.style.width = footerWidths[i]; + }, footerSrcEls ); + } + + // Sanity check that the table is of a sensible width. If not then we are going to get + // misalignment - try to prevent this by not allowing the table to shrink below its min width + if ( table.outerWidth() < sanityWidth ) + { + // The min width depends upon if we have a vertical scrollbar visible or not */ + correction = ((divBodyEl.scrollHeight > divBodyEl.offsetHeight || + divBody.css('overflow-y') == "scroll")) ? + sanityWidth+barWidth : + sanityWidth; + + // IE6/7 are a law unto themselves... + if ( ie67 && (divBodyEl.scrollHeight > + divBodyEl.offsetHeight || divBody.css('overflow-y') == "scroll") + ) { + tableStyle.width = _fnStringToCss( correction-barWidth ); + } + + // And give the user a warning that we've stopped the table getting too small + if ( scrollX === "" || scrollXInner !== "" ) { + _fnLog( settings, 1, 'Possible column misalignment', 6 ); + } + } + else + { + correction = '100%'; + } + + // Apply to the container elements + divBodyStyle.width = _fnStringToCss( correction ); + divHeaderStyle.width = _fnStringToCss( correction ); + + if ( footer ) { + settings.nScrollFoot.style.width = _fnStringToCss( correction ); + } + + + /* + * 4. Clean up + */ + if ( ! scrollY ) { + /* IE7< puts a vertical scrollbar in place (when it shouldn't be) due to subtracting + * the scrollbar height from the visible display, rather than adding it on. We need to + * set the height in order to sort this. Don't want to do it in any other browsers. + */ + if ( ie67 ) { + divBodyStyle.height = _fnStringToCss( tableEl.offsetHeight+barWidth ); + } + } + + if ( scrollY && scroll.bCollapse ) { + divBodyStyle.height = _fnStringToCss( scrollY ); + + var iExtra = (scrollX && tableEl.offsetWidth > divBodyEl.offsetWidth) ? + barWidth : + 0; + + if ( tableEl.offsetHeight < divBodyEl.offsetHeight ) { + divBodyStyle.height = _fnStringToCss( tableEl.offsetHeight+iExtra ); + } + } + + /* Finally set the width's of the header and footer tables */ + var iOuterWidth = table.outerWidth(); + divHeaderTable[0].style.width = _fnStringToCss( iOuterWidth ); + divHeaderInnerStyle.width = _fnStringToCss( iOuterWidth ); + + // Figure out if there are scrollbar present - if so then we need a the header and footer to + // provide a bit more space to allow "overflow" scrolling (i.e. past the scrollbar) + var bScrolling = table.height() > divBodyEl.clientHeight || divBody.css('overflow-y') == "scroll"; + var padding = 'padding' + (browser.bScrollbarLeft ? 'Left' : 'Right' ); + divHeaderInnerStyle[ padding ] = bScrolling ? barWidth+"px" : "0px"; + + if ( footer ) { + divFooterTable[0].style.width = _fnStringToCss( iOuterWidth ); + divFooterInner[0].style.width = _fnStringToCss( iOuterWidth ); + divFooterInner[0].style[padding] = bScrolling ? barWidth+"px" : "0px"; + } + + /* Adjust the position of the header in case we loose the y-scrollbar */ + divBody.scroll(); + + /* If sorting or filtering has occurred, jump the scrolling back to the top */ + if ( settings.bSorted || settings.bFiltered ) { + divBodyEl.scrollTop = 0; + } + } + + + + /** + * Apply a given function to the display child nodes of an element array (typically + * TD children of TR rows + * @param {function} fn Method to apply to the objects + * @param array {nodes} an1 List of elements to look through for display children + * @param array {nodes} an2 Another list (identical structure to the first) - optional + * @memberof DataTable#oApi + */ + function _fnApplyToChildren( fn, an1, an2 ) + { + var index=0, i=0, iLen=an1.length; + var nNode1, nNode2; + + while ( i < iLen ) { + nNode1 = an1[i].firstChild; + nNode2 = an2 ? an2[i].firstChild : null; + + while ( nNode1 ) { + if ( nNode1.nodeType === 1 ) { + if ( an2 ) { + fn( nNode1, nNode2, index ); + } + else { + fn( nNode1, index ); + } + + index++; + } + + nNode1 = nNode1.nextSibling; + nNode2 = an2 ? nNode2.nextSibling : null; + } + + i++; + } + } + + + + var __re_html_remove = /<.*?>/g; + + + /** + * Calculate the width of columns for the table + * @param {object} oSettings dataTables settings object + * @memberof DataTable#oApi + */ + function _fnCalculateColumnWidths ( oSettings ) + { + var + table = oSettings.nTable, + columns = oSettings.aoColumns, + scroll = oSettings.oScroll, + scrollY = scroll.sY, + scrollX = scroll.sX, + scrollXInner = scroll.sXInner, + columnCount = columns.length, + visibleColumns = _fnGetColumns( oSettings, 'bVisible' ), + headerCells = $('th', oSettings.nTHead), + tableWidthAttr = table.getAttribute('width'), + tableContainer = table.parentNode, + userInputs = false, + i, column, columnIdx, width, outerWidth; + + /* Convert any user input sizes into pixel sizes */ + for ( i=0 ; i<visibleColumns.length ; i++ ) { + column = columns[ visibleColumns[i] ]; + + if ( column.sWidth !== null ) { + column.sWidth = _fnConvertToWidth( column.sWidthOrig, tableContainer ); + + userInputs = true; + } + } + + /* If the number of columns in the DOM equals the number that we have to + * process in DataTables, then we can use the offsets that are created by + * the web- browser. No custom sizes can be set in order for this to happen, + * nor scrolling used + */ + if ( ! userInputs && ! scrollX && ! scrollY && + columnCount == _fnVisbleColumns( oSettings ) && + columnCount == headerCells.length + ) { + for ( i=0 ; i<columnCount ; i++ ) { + columns[i].sWidth = _fnStringToCss( headerCells.eq(i).width() ); + } + } + else + { + // Otherwise construct a single row table with the widest node in the + // data, assign any user defined widths, then insert it into the DOM and + // allow the browser to do all the hard work of calculating table widths + var tmpTable = $( table.cloneNode( false ) ) + .css( 'visibility', 'hidden' ) + .removeAttr( 'id' ) + .append( $(oSettings.nTHead).clone( false ) ) + .append( $(oSettings.nTFoot).clone( false ) ) + .append( $('<tbody><tr/></tbody>') ); + + // Remove any assigned widths from the footer (from scrolling) + tmpTable.find('tfoot th, tfoot td').css('width', ''); + + var tr = tmpTable.find( 'tbody tr' ); + + // Apply custom sizing to the cloned header + headerCells = _fnGetUniqueThs( oSettings, tmpTable.find('thead')[0] ); + + for ( i=0 ; i<visibleColumns.length ; i++ ) { + column = columns[ visibleColumns[i] ]; + + headerCells[i].style.width = column.sWidthOrig !== null && column.sWidthOrig !== '' ? + _fnStringToCss( column.sWidthOrig ) : + ''; + } + + // Find the widest cell for each column and put it into the table + if ( oSettings.aoData.length ) { + for ( i=0 ; i<visibleColumns.length ; i++ ) { + columnIdx = visibleColumns[i]; + column = columns[ columnIdx ]; + + $( _fnGetWidestNode( oSettings, columnIdx ) ) + .clone( false ) + .append( column.sContentPadding ) + .appendTo( tr ); + } + } + + // Table has been built, attach to the document so we can work with it + tmpTable.appendTo( tableContainer ); + + // When scrolling (X or Y) we want to set the width of the table as + // appropriate. However, when not scrolling leave the table width as it + // is. This results in slightly different, but I think correct behaviour + if ( scrollX && scrollXInner ) { + tmpTable.width( scrollXInner ); + } + else if ( scrollX ) { + tmpTable.css( 'width', 'auto' ); + + if ( tmpTable.width() < tableContainer.offsetWidth ) { + tmpTable.width( tableContainer.offsetWidth ); + } + } + else if ( scrollY ) { + tmpTable.width( tableContainer.offsetWidth ); + } + else if ( tableWidthAttr ) { + tmpTable.width( tableWidthAttr ); + } + + // Take into account the y scrollbar + _fnScrollingWidthAdjust( oSettings, tmpTable[0] ); + + // Browsers need a bit of a hand when a width is assigned to any columns + // when x-scrolling as they tend to collapse the table to the min-width, + // even if we sent the column widths. So we need to keep track of what + // the table width should be by summing the user given values, and the + // automatic values + if ( scrollX ) + { + var total = 0; + + for ( i=0 ; i<visibleColumns.length ; i++ ) { + column = columns[ visibleColumns[i] ]; + outerWidth = $(headerCells[i]).outerWidth(); + + total += column.sWidthOrig === null ? + outerWidth : + parseInt( column.sWidth, 10 ) + outerWidth - $(headerCells[i]).width(); + } + + tmpTable.width( _fnStringToCss( total ) ); + table.style.width = _fnStringToCss( total ); + } + + // Get the width of each column in the constructed table + for ( i=0 ; i<visibleColumns.length ; i++ ) { + column = columns[ visibleColumns[i] ]; + width = $(headerCells[i]).width(); + + if ( width ) { + column.sWidth = _fnStringToCss( width ); + } + } + + table.style.width = _fnStringToCss( tmpTable.css('width') ); + + // Finished with the table - ditch it + tmpTable.remove(); + } + + // If there is a width attr, we want to attach an event listener which + // allows the table sizing to automatically adjust when the window is + // resized. Use the width attr rather than CSS, since we can't know if the + // CSS is a relative value or absolute - DOM read is always px. + if ( tableWidthAttr ) { + table.style.width = _fnStringToCss( tableWidthAttr ); + } + + if ( (tableWidthAttr || scrollX) && ! oSettings._reszEvt ) { + $(window).bind('resize.DT-'+oSettings.sInstance, _fnThrottle( function () { + _fnAdjustColumnSizing( oSettings ); + } ) ); + + oSettings._reszEvt = true; + } + } + + + /** + * Throttle the calls to a function. Arguments and context are maintained for + * the throttled function + * @param {function} fn Function to be called + * @param {int} [freq=200] call frequency in mS + * @returns {function} wrapped function + * @memberof DataTable#oApi + */ + function _fnThrottle( fn, freq ) { + var + frequency = freq || 200, + last, + timer; + + return function () { + var + that = this, + now = +new Date(), + args = arguments; + + if ( last && now < last + frequency ) { + clearTimeout( timer ); + + timer = setTimeout( function () { + last = undefined; + fn.apply( that, args ); + }, frequency ); + } + else if ( last ) { + last = now; + fn.apply( that, args ); + } + else { + last = now; + } + }; + } + + + /** + * Convert a CSS unit width to pixels (e.g. 2em) + * @param {string} width width to be converted + * @param {node} parent parent to get the with for (required for relative widths) - optional + * @returns {int} width in pixels + * @memberof DataTable#oApi + */ + function _fnConvertToWidth ( width, parent ) + { + if ( ! width ) { + return 0; + } + + var n = $('<div/>') + .css( 'width', _fnStringToCss( width ) ) + .appendTo( parent || document.body ); + + var val = n[0].offsetWidth; + n.remove(); + + return val; + } + + + /** + * Adjust a table's width to take account of vertical scroll bar + * @param {object} oSettings dataTables settings object + * @param {node} n table node + * @memberof DataTable#oApi + */ + + function _fnScrollingWidthAdjust ( settings, n ) + { + var scroll = settings.oScroll; + + if ( scroll.sX || scroll.sY ) { + // When y-scrolling only, we want to remove the width of the scroll bar + // so the table + scroll bar will fit into the area available, otherwise + // we fix the table at its current size with no adjustment + var correction = ! scroll.sX ? scroll.iBarWidth : 0; + n.style.width = _fnStringToCss( $(n).outerWidth() - correction ); + } + } + + + /** + * Get the widest node + * @param {object} settings dataTables settings object + * @param {int} colIdx column of interest + * @returns {node} widest table node + * @memberof DataTable#oApi + */ + function _fnGetWidestNode( settings, colIdx ) + { + var idx = _fnGetMaxLenString( settings, colIdx ); + if ( idx < 0 ) { + return null; + } + + var data = settings.aoData[ idx ]; + return ! data.nTr ? // Might not have been created when deferred rendering + $('<td/>').html( _fnGetCellData( settings, idx, colIdx, 'display' ) )[0] : + data.anCells[ colIdx ]; + } + + + /** + * Get the maximum strlen for each data column + * @param {object} settings dataTables settings object + * @param {int} colIdx column of interest + * @returns {string} max string length for each column + * @memberof DataTable#oApi + */ + function _fnGetMaxLenString( settings, colIdx ) + { + var s, max=-1, maxIdx = -1; + + for ( var i=0, ien=settings.aoData.length ; i<ien ; i++ ) { + s = _fnGetCellData( settings, i, colIdx, 'display' )+''; + s = s.replace( __re_html_remove, '' ); + + if ( s.length > max ) { + max = s.length; + maxIdx = i; + } + } + + return maxIdx; + } + + + /** + * Append a CSS unit (only if required) to a string + * @param {string} value to css-ify + * @returns {string} value with css unit + * @memberof DataTable#oApi + */ + function _fnStringToCss( s ) + { + if ( s === null ) { + return '0px'; + } + + if ( typeof s == 'number' ) { + return s < 0 ? + '0px' : + s+'px'; + } + + // Check it has a unit character already + return s.match(/\d$/) ? + s+'px' : + s; + } + + + /** + * Get the width of a scroll bar in this browser being used + * @returns {int} width in pixels + * @memberof DataTable#oApi + */ + function _fnScrollBarWidth () + { + // On first run a static variable is set, since this is only needed once. + // Subsequent runs will just use the previously calculated value + if ( ! DataTable.__scrollbarWidth ) { + var inner = $('<p/>').css( { + width: '100%', + height: 200, + padding: 0 + } )[0]; + + var outer = $('<div/>') + .css( { + position: 'absolute', + top: 0, + left: 0, + width: 200, + height: 150, + padding: 0, + overflow: 'hidden', + visibility: 'hidden' + } ) + .append( inner ) + .appendTo( 'body' ); + + var w1 = inner.offsetWidth; + outer.css( 'overflow', 'scroll' ); + var w2 = inner.offsetWidth; + + if ( w1 === w2 ) { + w2 = outer[0].clientWidth; + } + + outer.remove(); + + DataTable.__scrollbarWidth = w1 - w2; + } + + return DataTable.__scrollbarWidth; + } + + + + function _fnSortFlatten ( settings ) + { + var + i, iLen, k, kLen, + aSort = [], + aiOrig = [], + aoColumns = settings.aoColumns, + aDataSort, iCol, sType, srcCol, + fixed = settings.aaSortingFixed, + fixedObj = $.isPlainObject( fixed ), + nestedSort = [], + add = function ( a ) { + if ( a.length && ! $.isArray( a[0] ) ) { + // 1D array + nestedSort.push( a ); + } + else { + // 2D array + nestedSort.push.apply( nestedSort, a ); + } + }; + + // Build the sort array, with pre-fix and post-fix options if they have been + // specified + if ( $.isArray( fixed ) ) { + add( fixed ); + } + + if ( fixedObj && fixed.pre ) { + add( fixed.pre ); + } + + add( settings.aaSorting ); + + if (fixedObj && fixed.post ) { + add( fixed.post ); + } + + for ( i=0 ; i<nestedSort.length ; i++ ) + { + srcCol = nestedSort[i][0]; + aDataSort = aoColumns[ srcCol ].aDataSort; + + for ( k=0, kLen=aDataSort.length ; k<kLen ; k++ ) + { + iCol = aDataSort[k]; + sType = aoColumns[ iCol ].sType || 'string'; + + aSort.push( { + src: srcCol, + col: iCol, + dir: nestedSort[i][1], + index: nestedSort[i][2], + type: sType, + formatter: DataTable.ext.type.order[ sType+"-pre" ] + } ); + } + } + + return aSort; + } + + /** + * Change the order of the table + * @param {object} oSettings dataTables settings object + * @memberof DataTable#oApi + * @todo This really needs split up! + */ + function _fnSort ( oSettings ) + { + var + i, ien, iLen, j, jLen, k, kLen, + sDataType, nTh, + aiOrig = [], + oExtSort = DataTable.ext.type.order, + aoData = oSettings.aoData, + aoColumns = oSettings.aoColumns, + aDataSort, data, iCol, sType, oSort, + formatters = 0, + sortCol, + displayMaster = oSettings.aiDisplayMaster, + aSort; + + // Resolve any column types that are unknown due to addition or invalidation + // @todo Can this be moved into a 'data-ready' handler which is called when + // data is going to be used in the table? + _fnColumnTypes( oSettings ); + + aSort = _fnSortFlatten( oSettings ); + + for ( i=0, ien=aSort.length ; i<ien ; i++ ) { + sortCol = aSort[i]; + + // Track if we can use the fast sort algorithm + if ( sortCol.formatter ) { + formatters++; + } + + // Load the data needed for the sort, for each cell + _fnSortData( oSettings, sortCol.col ); + } + + /* No sorting required if server-side or no sorting array */ + if ( _fnDataSource( oSettings ) != 'ssp' && aSort.length !== 0 ) + { + // Create a value - key array of the current row positions such that we can use their + // current position during the sort, if values match, in order to perform stable sorting + for ( i=0, iLen=displayMaster.length ; i<iLen ; i++ ) { + aiOrig[ displayMaster[i] ] = i; + } + + /* Do the sort - here we want multi-column sorting based on a given data source (column) + * and sorting function (from oSort) in a certain direction. It's reasonably complex to + * follow on it's own, but this is what we want (example two column sorting): + * fnLocalSorting = function(a,b){ + * var iTest; + * iTest = oSort['string-asc']('data11', 'data12'); + * if (iTest !== 0) + * return iTest; + * iTest = oSort['numeric-desc']('data21', 'data22'); + * if (iTest !== 0) + * return iTest; + * return oSort['numeric-asc']( aiOrig[a], aiOrig[b] ); + * } + * Basically we have a test for each sorting column, if the data in that column is equal, + * test the next column. If all columns match, then we use a numeric sort on the row + * positions in the original data array to provide a stable sort. + * + * Note - I know it seems excessive to have two sorting methods, but the first is around + * 15% faster, so the second is only maintained for backwards compatibility with sorting + * methods which do not have a pre-sort formatting function. + */ + if ( formatters === aSort.length ) { + // All sort types have formatting functions + displayMaster.sort( function ( a, b ) { + var + x, y, k, test, sort, + len=aSort.length, + dataA = aoData[a]._aSortData, + dataB = aoData[b]._aSortData; + + for ( k=0 ; k<len ; k++ ) { + sort = aSort[k]; + + x = dataA[ sort.col ]; + y = dataB[ sort.col ]; + + test = x<y ? -1 : x>y ? 1 : 0; + if ( test !== 0 ) { + return sort.dir === 'asc' ? test : -test; + } + } + + x = aiOrig[a]; + y = aiOrig[b]; + return x<y ? -1 : x>y ? 1 : 0; + } ); + } + else { + // Depreciated - remove in 1.11 (providing a plug-in option) + // Not all sort types have formatting methods, so we have to call their sorting + // methods. + displayMaster.sort( function ( a, b ) { + var + x, y, k, l, test, sort, fn, + len=aSort.length, + dataA = aoData[a]._aSortData, + dataB = aoData[b]._aSortData; + + for ( k=0 ; k<len ; k++ ) { + sort = aSort[k]; + + x = dataA[ sort.col ]; + y = dataB[ sort.col ]; + + fn = oExtSort[ sort.type+"-"+sort.dir ] || oExtSort[ "string-"+sort.dir ]; + test = fn( x, y ); + if ( test !== 0 ) { + return test; + } + } + + x = aiOrig[a]; + y = aiOrig[b]; + return x<y ? -1 : x>y ? 1 : 0; + } ); + } + } + + /* Tell the draw function that we have sorted the data */ + oSettings.bSorted = true; + } + + + function _fnSortAria ( settings ) + { + var label; + var nextSort; + var columns = settings.aoColumns; + var aSort = _fnSortFlatten( settings ); + var oAria = settings.oLanguage.oAria; + + // ARIA attributes - need to loop all columns, to update all (removing old + // attributes as needed) + for ( var i=0, iLen=columns.length ; i<iLen ; i++ ) + { + var col = columns[i]; + var asSorting = col.asSorting; + var sTitle = col.sTitle.replace( /<.*?>/g, "" ); + var th = col.nTh; + + // IE7 is throwing an error when setting these properties with jQuery's + // attr() and removeAttr() methods... + th.removeAttribute('aria-sort'); + + /* In ARIA only the first sorting column can be marked as sorting - no multi-sort option */ + if ( col.bSortable ) { + if ( aSort.length > 0 && aSort[0].col == i ) { + th.setAttribute('aria-sort', aSort[0].dir=="asc" ? "ascending" : "descending" ); + nextSort = asSorting[ aSort[0].index+1 ] || asSorting[0]; + } + else { + nextSort = asSorting[0]; + } + + label = sTitle + ( nextSort === "asc" ? + oAria.sSortAscending : + oAria.sSortDescending + ); + } + else { + label = sTitle; + } + + th.setAttribute('aria-label', label); + } + } + + + /** + * Function to run on user sort request + * @param {object} settings dataTables settings object + * @param {node} attachTo node to attach the handler to + * @param {int} colIdx column sorting index + * @param {boolean} [append=false] Append the requested sort to the existing + * sort if true (i.e. multi-column sort) + * @param {function} [callback] callback function + * @memberof DataTable#oApi + */ + function _fnSortListener ( settings, colIdx, append, callback ) + { + var col = settings.aoColumns[ colIdx ]; + var sorting = settings.aaSorting; + var asSorting = col.asSorting; + var nextSortIdx; + var next = function ( a ) { + var idx = a._idx; + if ( idx === undefined ) { + idx = $.inArray( a[1], asSorting ); + } + + return idx+1 >= asSorting.length ? 0 : idx+1; + }; + + // If appending the sort then we are multi-column sorting + if ( append && settings.oFeatures.bSortMulti ) { + // Are we already doing some kind of sort on this column? + var sortIdx = $.inArray( colIdx, _pluck(sorting, '0') ); + + if ( sortIdx !== -1 ) { + // Yes, modify the sort + nextSortIdx = next( sorting[sortIdx] ); + + sorting[sortIdx][1] = asSorting[ nextSortIdx ]; + sorting[sortIdx]._idx = nextSortIdx; + } + else { + // No sort on this column yet + sorting.push( [ colIdx, asSorting[0], 0 ] ); + sorting[sorting.length-1]._idx = 0; + } + } + else if ( sorting.length && sorting[0][0] == colIdx ) { + // Single column - already sorting on this column, modify the sort + nextSortIdx = next( sorting[0] ); + + sorting.length = 1; + sorting[0][1] = asSorting[ nextSortIdx ]; + sorting[0]._idx = nextSortIdx; + } + else { + // Single column - sort only on this column + sorting.length = 0; + sorting.push( [ colIdx, asSorting[0] ] ); + sorting[0]._idx = 0; + } + + // Run the sort by calling a full redraw + _fnReDraw( settings ); + + // callback used for async user interaction + if ( typeof callback == 'function' ) { + callback( settings ); + } + } + + + /** + * Attach a sort handler (click) to a node + * @param {object} settings dataTables settings object + * @param {node} attachTo node to attach the handler to + * @param {int} colIdx column sorting index + * @param {function} [callback] callback function + * @memberof DataTable#oApi + */ + function _fnSortAttachListener ( settings, attachTo, colIdx, callback ) + { + var col = settings.aoColumns[ colIdx ]; + + _fnBindAction( attachTo, {}, function (e) { + /* If the column is not sortable - don't to anything */ + if ( col.bSortable === false ) { + return; + } + + // If processing is enabled use a timeout to allow the processing + // display to be shown - otherwise to it synchronously + if ( settings.oFeatures.bProcessing ) { + _fnProcessingDisplay( settings, true ); + + setTimeout( function() { + _fnSortListener( settings, colIdx, e.shiftKey, callback ); + + // In server-side processing, the draw callback will remove the + // processing display + if ( _fnDataSource( settings ) !== 'ssp' ) { + _fnProcessingDisplay( settings, false ); + } + }, 0 ); + } + else { + _fnSortListener( settings, colIdx, e.shiftKey, callback ); + } + } ); + } + + + /** + * Set the sorting classes on table's body, Note: it is safe to call this function + * when bSort and bSortClasses are false + * @param {object} oSettings dataTables settings object + * @memberof DataTable#oApi + */ + function _fnSortingClasses( settings ) + { + var oldSort = settings.aLastSort; + var sortClass = settings.oClasses.sSortColumn; + var sort = _fnSortFlatten( settings ); + var features = settings.oFeatures; + var i, ien, colIdx; + + if ( features.bSort && features.bSortClasses ) { + // Remove old sorting classes + for ( i=0, ien=oldSort.length ; i<ien ; i++ ) { + colIdx = oldSort[i].src; + + // Remove column sorting + $( _pluck( settings.aoData, 'anCells', colIdx ) ) + .removeClass( sortClass + (i<2 ? i+1 : 3) ); + } + + // Add new column sorting + for ( i=0, ien=sort.length ; i<ien ; i++ ) { + colIdx = sort[i].src; + + $( _pluck( settings.aoData, 'anCells', colIdx ) ) + .addClass( sortClass + (i<2 ? i+1 : 3) ); + } + } + + settings.aLastSort = sort; + } + + + // Get the data to sort a column, be it from cache, fresh (populating the + // cache), or from a sort formatter + function _fnSortData( settings, idx ) + { + // Custom sorting function - provided by the sort data type + var column = settings.aoColumns[ idx ]; + var customSort = DataTable.ext.order[ column.sSortDataType ]; + var customData; + + if ( customSort ) { + customData = customSort.call( settings.oInstance, settings, idx, + _fnColumnIndexToVisible( settings, idx ) + ); + } + + // Use / populate cache + var row, cellData; + var formatter = DataTable.ext.type.order[ column.sType+"-pre" ]; + + for ( var i=0, ien=settings.aoData.length ; i<ien ; i++ ) { + row = settings.aoData[i]; + + if ( ! row._aSortData ) { + row._aSortData = []; + } + + if ( ! row._aSortData[idx] || customSort ) { + cellData = customSort ? + customData[i] : // If there was a custom sort function, use data from there + _fnGetCellData( settings, i, idx, 'sort' ); + + row._aSortData[ idx ] = formatter ? + formatter( cellData ) : + cellData; + } + } + } + + + + /** + * Save the state of a table + * @param {object} oSettings dataTables settings object + * @memberof DataTable#oApi + */ + function _fnSaveState ( oSettings ) + { + if ( !oSettings.oFeatures.bStateSave || oSettings.bDestroying ) + { + return; + } + + /* Store the interesting variables */ + var i, iLen; + var oState = { + "iCreate": +new Date(), + "iStart": oSettings._iDisplayStart, + "iLength": oSettings._iDisplayLength, + "aaSorting": $.extend( true, [], oSettings.aaSorting ), + "oSearch": $.extend( true, {}, oSettings.oPreviousSearch ), + "aoSearchCols": $.extend( true, [], oSettings.aoPreSearchCols ), + "abVisCols": _pluck( oSettings.aoColumns, 'bVisible' ) + }; + + _fnCallbackFire( oSettings, "aoStateSaveParams", 'stateSaveParams', [oSettings, oState] ); + + oSettings.fnStateSaveCallback.call( oSettings.oInstance, oSettings, oState ); + } + + + /** + * Attempt to load a saved table state + * @param {object} oSettings dataTables settings object + * @param {object} oInit DataTables init object so we can override settings + * @memberof DataTable#oApi + */ + function _fnLoadState ( oSettings, oInit ) + { + var i, ien; + var columns = oSettings.aoColumns; + + if ( ! oSettings.oFeatures.bStateSave ) { + return; + } + + var oData = oSettings.fnStateLoadCallback.call( oSettings.oInstance, oSettings ); + if ( !oData ) { + return; + } + + /* Allow custom and plug-in manipulation functions to alter the saved data set and + * cancelling of loading by returning false + */ + var abStateLoad = _fnCallbackFire( oSettings, 'aoStateLoadParams', 'stateLoadParams', [oSettings, oData] ); + if ( $.inArray( false, abStateLoad ) !== -1 ) { + return; + } + + /* Reject old data */ + var duration = oSettings.iStateDuration; + if ( duration > 0 && oData.iCreate < +new Date() - (duration*1000) ) { + return; + } + + // Number of columns have changed - all bets are off, no restore of settings + if ( columns.length !== oData.aoSearchCols.length ) { + return; + } + + /* Store the saved state so it might be accessed at any time */ + oSettings.oLoadedState = $.extend( true, {}, oData ); + + /* Restore key features */ + oSettings._iDisplayStart = oData.iStart; + oSettings.iInitDisplayStart = oData.iStart; + oSettings._iDisplayLength = oData.iLength; + oSettings.aaSorting = $.map( oData.aaSorting, function ( col, i ) { + return col[0] >= columns.length ? + [ 0, col[1] ] : + col; + } ); + + /* Search filtering */ + $.extend( oSettings.oPreviousSearch, oData.oSearch ); + $.extend( true, oSettings.aoPreSearchCols, oData.aoSearchCols ); + + /* Column visibility state */ + var visColumns = oData.abVisCols; + for ( i=0, ien=visColumns.length ; i<ien ; i++ ) { + columns[i].bVisible = visColumns[i]; + } + + _fnCallbackFire( oSettings, 'aoStateLoaded', 'stateLoaded', [oSettings, oData] ); + } + + + /** + * Return the settings object for a particular table + * @param {node} table table we are using as a dataTable + * @returns {object} Settings object - or null if not found + * @memberof DataTable#oApi + */ + function _fnSettingsFromNode ( table ) + { + var settings = DataTable.settings; + var idx = $.inArray( table, _pluck( settings, 'nTable' ) ); + + return idx !== -1 ? + settings[ idx ] : + null; + } + + + /** + * Log an error message + * @param {object} settings dataTables settings object + * @param {int} level log error messages, or display them to the user + * @param {string} msg error message + * @param {int} tn Technical note id to get more information about the error. + * @memberof DataTable#oApi + */ + function _fnLog( settings, level, msg, tn ) + { + msg = 'DataTables warning: '+ + (settings!==null ? 'table id='+settings.sTableId+' - ' : '')+msg; + + if ( tn ) { + msg += '. For more information about this error, please see '+ + 'http://datatables.net/tn/'+tn; + } + + if ( ! level ) { + // Backwards compatibility pre 1.10 + var ext = DataTable.ext; + var type = ext.sErrMode || ext.errMode; + + if ( type == 'alert' ) { + alert( msg ); + } + else { + throw new Error(msg); + } + } + else if ( window.console && console.log ) { + console.log( msg ); + } + } + + + /** + * See if a property is defined on one object, if so assign it to the other object + * @param {object} ret target object + * @param {object} src source object + * @param {string} name property + * @param {string} [mappedName] name to map too - optional, name used if not given + * @memberof DataTable#oApi + */ + function _fnMap( ret, src, name, mappedName ) + { + if ( $.isArray( name ) ) { + $.each( name, function (i, val) { + if ( $.isArray( val ) ) { + _fnMap( ret, src, val[0], val[1] ); + } + else { + _fnMap( ret, src, val ); + } + } ); + + return; + } + + if ( mappedName === undefined ) { + mappedName = name; + } + + if ( src[name] !== undefined ) { + ret[mappedName] = src[name]; + } + } + + + /** + * Extend objects - very similar to jQuery.extend, but deep copy objects, and + * shallow copy arrays. The reason we need to do this, is that we don't want to + * deep copy array init values (such as aaSorting) since the dev wouldn't be + * able to override them, but we do want to deep copy arrays. + * @param {object} out Object to extend + * @param {object} extender Object from which the properties will be applied to + * out + * @param {boolean} breakRefs If true, then arrays will be sliced to take an + * independent copy with the exception of the `data` or `aaData` parameters + * if they are present. This is so you can pass in a collection to + * DataTables and have that used as your data source without breaking the + * references + * @returns {object} out Reference, just for convenience - out === the return. + * @memberof DataTable#oApi + * @todo This doesn't take account of arrays inside the deep copied objects. + */ + function _fnExtend( out, extender, breakRefs ) + { + var val; + + for ( var prop in extender ) { + if ( extender.hasOwnProperty(prop) ) { + val = extender[prop]; + + if ( $.isPlainObject( val ) ) { + if ( ! $.isPlainObject( out[prop] ) ) { + out[prop] = {}; + } + $.extend( true, out[prop], val ); + } + else if ( breakRefs && prop !== 'data' && prop !== 'aaData' && $.isArray(val) ) { + out[prop] = val.slice(); + } + else { + out[prop] = val; + } + } + } + + return out; + } + + + /** + * Bind an event handers to allow a click or return key to activate the callback. + * This is good for accessibility since a return on the keyboard will have the + * same effect as a click, if the element has focus. + * @param {element} n Element to bind the action to + * @param {object} oData Data object to pass to the triggered function + * @param {function} fn Callback function for when the event is triggered + * @memberof DataTable#oApi + */ + function _fnBindAction( n, oData, fn ) + { + $(n) + .bind( 'click.DT', oData, function (e) { + n.blur(); // Remove focus outline for mouse users + fn(e); + } ) + .bind( 'keypress.DT', oData, function (e){ + if ( e.which === 13 ) { + e.preventDefault(); + fn(e); + } + } ) + .bind( 'selectstart.DT', function () { + /* Take the brutal approach to cancelling text selection */ + return false; + } ); + } + + + /** + * Register a callback function. Easily allows a callback function to be added to + * an array store of callback functions that can then all be called together. + * @param {object} oSettings dataTables settings object + * @param {string} sStore Name of the array storage for the callbacks in oSettings + * @param {function} fn Function to be called back + * @param {string} sName Identifying name for the callback (i.e. a label) + * @memberof DataTable#oApi + */ + function _fnCallbackReg( oSettings, sStore, fn, sName ) + { + if ( fn ) + { + oSettings[sStore].push( { + "fn": fn, + "sName": sName + } ); + } + } + + + /** + * Fire callback functions and trigger events. Note that the loop over the + * callback array store is done backwards! Further note that you do not want to + * fire off triggers in time sensitive applications (for example cell creation) + * as its slow. + * @param {object} settings dataTables settings object + * @param {string} callbackArr Name of the array storage for the callbacks in + * oSettings + * @param {string} event Name of the jQuery custom event to trigger. If null no + * trigger is fired + * @param {array} args Array of arguments to pass to the callback function / + * trigger + * @memberof DataTable#oApi + */ + function _fnCallbackFire( settings, callbackArr, event, args ) + { + var ret = []; + + if ( callbackArr ) { + ret = $.map( settings[callbackArr].slice().reverse(), function (val, i) { + return val.fn.apply( settings.oInstance, args ); + } ); + } + + if ( event !== null ) { + $(settings.nTable).trigger( event+'.dt', args ); + } + + return ret; + } + + + function _fnLengthOverflow ( settings ) + { + var + start = settings._iDisplayStart, + end = settings.fnDisplayEnd(), + len = settings._iDisplayLength; + + /* If we have space to show extra rows (backing up from the end point - then do so */ + if ( end === settings.fnRecordsDisplay() ) + { + start = end - len; + } + + if ( len === -1 || start < 0 ) + { + start = 0; + } + + settings._iDisplayStart = start; + } + + + function _fnRenderer( settings, type ) + { + var renderer = settings.renderer; + var host = DataTable.ext.renderer[type]; + + if ( $.isPlainObject( renderer ) && renderer[type] ) { + // Specific renderer for this type. If available use it, otherwise use + // the default. + return host[renderer[type]] || host._; + } + else if ( typeof renderer === 'string' ) { + // Common renderer - if there is one available for this type use it, + // otherwise use the default + return host[renderer] || host._; + } + + // Use the default + return host._; + } + + + /** + * Detect the data source being used for the table. Used to simplify the code + * a little (ajax) and to make it compress a little smaller. + * + * @param {object} settings dataTables settings object + * @returns {string} Data source + * @memberof DataTable#oApi + */ + function _fnDataSource ( settings ) + { + if ( settings.oFeatures.bServerSide ) { + return 'ssp'; + } + else if ( settings.ajax || settings.sAjaxSource ) { + return 'ajax'; + } + return 'dom'; + } + + + DataTable = function( options ) + { + /** + * Perform a jQuery selector action on the table's TR elements (from the tbody) and + * return the resulting jQuery object. + * @param {string|node|jQuery} sSelector jQuery selector or node collection to act on + * @param {object} [oOpts] Optional parameters for modifying the rows to be included + * @param {string} [oOpts.filter=none] Select TR elements that meet the current filter + * criterion ("applied") or all TR elements (i.e. no filter). + * @param {string} [oOpts.order=current] Order of the TR elements in the processed array. + * Can be either 'current', whereby the current sorting of the table is used, or + * 'original' whereby the original order the data was read into the table is used. + * @param {string} [oOpts.page=all] Limit the selection to the currently displayed page + * ("current") or not ("all"). If 'current' is given, then order is assumed to be + * 'current' and filter is 'applied', regardless of what they might be given as. + * @returns {object} jQuery object, filtered by the given selector. + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Highlight every second row + * oTable.$('tr:odd').css('backgroundColor', 'blue'); + * } ); + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Filter to rows with 'Webkit' in them, add a background colour and then + * // remove the filter, thus highlighting the 'Webkit' rows only. + * oTable.fnFilter('Webkit'); + * oTable.$('tr', {"search": "applied"}).css('backgroundColor', 'blue'); + * oTable.fnFilter(''); + * } ); + */ + this.$ = function ( sSelector, oOpts ) + { + return this.api(true).$( sSelector, oOpts ); + }; + + + /** + * Almost identical to $ in operation, but in this case returns the data for the matched + * rows - as such, the jQuery selector used should match TR row nodes or TD/TH cell nodes + * rather than any descendants, so the data can be obtained for the row/cell. If matching + * rows are found, the data returned is the original data array/object that was used to + * create the row (or a generated array if from a DOM source). + * + * This method is often useful in-combination with $ where both functions are given the + * same parameters and the array indexes will match identically. + * @param {string|node|jQuery} sSelector jQuery selector or node collection to act on + * @param {object} [oOpts] Optional parameters for modifying the rows to be included + * @param {string} [oOpts.filter=none] Select elements that meet the current filter + * criterion ("applied") or all elements (i.e. no filter). + * @param {string} [oOpts.order=current] Order of the data in the processed array. + * Can be either 'current', whereby the current sorting of the table is used, or + * 'original' whereby the original order the data was read into the table is used. + * @param {string} [oOpts.page=all] Limit the selection to the currently displayed page + * ("current") or not ("all"). If 'current' is given, then order is assumed to be + * 'current' and filter is 'applied', regardless of what they might be given as. + * @returns {array} Data for the matched elements. If any elements, as a result of the + * selector, were not TR, TD or TH elements in the DataTable, they will have a null + * entry in the array. + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Get the data from the first row in the table + * var data = oTable._('tr:first'); + * + * // Do something useful with the data + * alert( "First cell is: "+data[0] ); + * } ); + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Filter to 'Webkit' and get all data for + * oTable.fnFilter('Webkit'); + * var data = oTable._('tr', {"search": "applied"}); + * + * // Do something with the data + * alert( data.length+" rows matched the search" ); + * } ); + */ + this._ = function ( sSelector, oOpts ) + { + return this.api(true).rows( sSelector, oOpts ).data(); + }; + + + /** + * Create a DataTables Api instance, with the currently selected tables for + * the Api's context. + * @param {boolean} [traditional=false] Set the API instance's context to be + * only the table referred to by the `DataTable.ext.iApiIndex` option, as was + * used in the API presented by DataTables 1.9- (i.e. the traditional mode), + * or if all tables captured in the jQuery object should be used. + * @return {DataTables.Api} + */ + this.api = function ( traditional ) + { + return traditional ? + new _Api( + _fnSettingsFromNode( this[ _ext.iApiIndex ] ) + ) : + new _Api( this ); + }; + + + /** + * Add a single new row or multiple rows of data to the table. Please note + * that this is suitable for client-side processing only - if you are using + * server-side processing (i.e. "bServerSide": true), then to add data, you + * must add it to the data source, i.e. the server-side, through an Ajax call. + * @param {array|object} data The data to be added to the table. This can be: + * <ul> + * <li>1D array of data - add a single row with the data provided</li> + * <li>2D array of arrays - add multiple rows in a single call</li> + * <li>object - data object when using <i>mData</i></li> + * <li>array of objects - multiple data objects when using <i>mData</i></li> + * </ul> + * @param {bool} [redraw=true] redraw the table or not + * @returns {array} An array of integers, representing the list of indexes in + * <i>aoData</i> ({@link DataTable.models.oSettings}) that have been added to + * the table. + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * // Global var for counter + * var giCount = 2; + * + * $(document).ready(function() { + * $('#example').dataTable(); + * } ); + * + * function fnClickAddRow() { + * $('#example').dataTable().fnAddData( [ + * giCount+".1", + * giCount+".2", + * giCount+".3", + * giCount+".4" ] + * ); + * + * giCount++; + * } + */ + this.fnAddData = function( data, redraw ) + { + var api = this.api( true ); + + /* Check if we want to add multiple rows or not */ + var rows = $.isArray(data) && ( $.isArray(data[0]) || $.isPlainObject(data[0]) ) ? + api.rows.add( data ) : + api.row.add( data ); + + if ( redraw === undefined || redraw ) { + api.draw(); + } + + return rows.flatten().toArray(); + }; + + + /** + * This function will make DataTables recalculate the column sizes, based on the data + * contained in the table and the sizes applied to the columns (in the DOM, CSS or + * through the sWidth parameter). This can be useful when the width of the table's + * parent element changes (for example a window resize). + * @param {boolean} [bRedraw=true] Redraw the table or not, you will typically want to + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable( { + * "sScrollY": "200px", + * "bPaginate": false + * } ); + * + * $(window).bind('resize', function () { + * oTable.fnAdjustColumnSizing(); + * } ); + * } ); + */ + this.fnAdjustColumnSizing = function ( bRedraw ) + { + var api = this.api( true ).columns.adjust(); + var settings = api.settings()[0]; + var scroll = settings.oScroll; + + if ( bRedraw === undefined || bRedraw ) { + api.draw( false ); + } + else if ( scroll.sX !== "" || scroll.sY !== "" ) { + /* If not redrawing, but scrolling, we want to apply the new column sizes anyway */ + _fnScrollDraw( settings ); + } + }; + + + /** + * Quickly and simply clear a table + * @param {bool} [bRedraw=true] redraw the table or not + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Immediately 'nuke' the current rows (perhaps waiting for an Ajax callback...) + * oTable.fnClearTable(); + * } ); + */ + this.fnClearTable = function( bRedraw ) + { + var api = this.api( true ).clear(); + + if ( bRedraw === undefined || bRedraw ) { + api.draw(); + } + }; + + + /** + * The exact opposite of 'opening' a row, this function will close any rows which + * are currently 'open'. + * @param {node} nTr the table row to 'close' + * @returns {int} 0 on success, or 1 if failed (can't find the row) + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable; + * + * // 'open' an information row when a row is clicked on + * $('#example tbody tr').click( function () { + * if ( oTable.fnIsOpen(this) ) { + * oTable.fnClose( this ); + * } else { + * oTable.fnOpen( this, "Temporary row opened", "info_row" ); + * } + * } ); + * + * oTable = $('#example').dataTable(); + * } ); + */ + this.fnClose = function( nTr ) + { + this.api( true ).row( nTr ).child.hide(); + }; + + + /** + * Remove a row for the table + * @param {mixed} target The index of the row from aoData to be deleted, or + * the TR element you want to delete + * @param {function|null} [callBack] Callback function + * @param {bool} [redraw=true] Redraw the table or not + * @returns {array} The row that was deleted + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Immediately remove the first row + * oTable.fnDeleteRow( 0 ); + * } ); + */ + this.fnDeleteRow = function( target, callback, redraw ) + { + var api = this.api( true ); + var rows = api.rows( target ); + var settings = rows.settings()[0]; + var data = settings.aoData[ rows[0][0] ]; + + rows.remove(); + + if ( callback ) { + callback.call( this, settings, data ); + } + + if ( redraw === undefined || redraw ) { + api.draw(); + } + + return data; + }; + + + /** + * Restore the table to it's original state in the DOM by removing all of DataTables + * enhancements, alterations to the DOM structure of the table and event listeners. + * @param {boolean} [remove=false] Completely remove the table from the DOM + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * // This example is fairly pointless in reality, but shows how fnDestroy can be used + * var oTable = $('#example').dataTable(); + * oTable.fnDestroy(); + * } ); + */ + this.fnDestroy = function ( remove ) + { + this.api( true ).destroy( remove ); + }; + + + /** + * Redraw the table + * @param {bool} [complete=true] Re-filter and resort (if enabled) the table before the draw. + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Re-draw the table - you wouldn't want to do it here, but it's an example :-) + * oTable.fnDraw(); + * } ); + */ + this.fnDraw = function( complete ) + { + // Note that this isn't an exact match to the old call to _fnDraw - it takes + // into account the new data, but can old position. + this.api( true ).draw( ! complete ); + }; + + + /** + * Filter the input based on data + * @param {string} sInput String to filter the table on + * @param {int|null} [iColumn] Column to limit filtering to + * @param {bool} [bRegex=false] Treat as regular expression or not + * @param {bool} [bSmart=true] Perform smart filtering or not + * @param {bool} [bShowGlobal=true] Show the input global filter in it's input box(es) + * @param {bool} [bCaseInsensitive=true] Do case-insensitive matching (true) or not (false) + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Sometime later - filter... + * oTable.fnFilter( 'test string' ); + * } ); + */ + this.fnFilter = function( sInput, iColumn, bRegex, bSmart, bShowGlobal, bCaseInsensitive ) + { + var api = this.api( true ); + + if ( iColumn === null || iColumn === undefined ) { + api.search( sInput, bRegex, bSmart, bCaseInsensitive ); + } + else { + api.column( iColumn ).search( sInput, bRegex, bSmart, bCaseInsensitive ); + } + + api.draw(); + }; + + + /** + * Get the data for the whole table, an individual row or an individual cell based on the + * provided parameters. + * @param {int|node} [src] A TR row node, TD/TH cell node or an integer. If given as + * a TR node then the data source for the whole row will be returned. If given as a + * TD/TH cell node then iCol will be automatically calculated and the data for the + * cell returned. If given as an integer, then this is treated as the aoData internal + * data index for the row (see fnGetPosition) and the data for that row used. + * @param {int} [col] Optional column index that you want the data of. + * @returns {array|object|string} If mRow is undefined, then the data for all rows is + * returned. If mRow is defined, just data for that row, and is iCol is + * defined, only data for the designated cell is returned. + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * // Row data + * $(document).ready(function() { + * oTable = $('#example').dataTable(); + * + * oTable.$('tr').click( function () { + * var data = oTable.fnGetData( this ); + * // ... do something with the array / object of data for the row + * } ); + * } ); + * + * @example + * // Individual cell data + * $(document).ready(function() { + * oTable = $('#example').dataTable(); + * + * oTable.$('td').click( function () { + * var sData = oTable.fnGetData( this ); + * alert( 'The cell clicked on had the value of '+sData ); + * } ); + * } ); + */ + this.fnGetData = function( src, col ) + { + var api = this.api( true ); + + if ( src !== undefined ) { + var type = src.nodeName ? src.nodeName.toLowerCase() : ''; + + return col !== undefined || type == 'td' || type == 'th' ? + api.cell( src, col ).data() : + api.row( src ).data() || null; + } + + return api.data().toArray(); + }; + + + /** + * Get an array of the TR nodes that are used in the table's body. Note that you will + * typically want to use the '$' API method in preference to this as it is more + * flexible. + * @param {int} [iRow] Optional row index for the TR element you want + * @returns {array|node} If iRow is undefined, returns an array of all TR elements + * in the table's body, or iRow is defined, just the TR element requested. + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Get the nodes from the table + * var nNodes = oTable.fnGetNodes( ); + * } ); + */ + this.fnGetNodes = function( iRow ) + { + var api = this.api( true ); + + return iRow !== undefined ? + api.row( iRow ).node() : + api.rows().nodes().flatten().toArray(); + }; + + + /** + * Get the array indexes of a particular cell from it's DOM element + * and column index including hidden columns + * @param {node} node this can either be a TR, TD or TH in the table's body + * @returns {int} If nNode is given as a TR, then a single index is returned, or + * if given as a cell, an array of [row index, column index (visible), + * column index (all)] is given. + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * $('#example tbody td').click( function () { + * // Get the position of the current data from the node + * var aPos = oTable.fnGetPosition( this ); + * + * // Get the data array for this row + * var aData = oTable.fnGetData( aPos[0] ); + * + * // Update the data array and return the value + * aData[ aPos[1] ] = 'clicked'; + * this.innerHTML = 'clicked'; + * } ); + * + * // Init DataTables + * oTable = $('#example').dataTable(); + * } ); + */ + this.fnGetPosition = function( node ) + { + var api = this.api( true ); + var nodeName = node.nodeName.toUpperCase(); + + if ( nodeName == 'TR' ) { + return api.row( node ).index(); + } + else if ( nodeName == 'TD' || nodeName == 'TH' ) { + var cell = api.cell( node ).index(); + + return [ + cell.row, + cell.columnVisible, + cell.column + ]; + } + return null; + }; + + + /** + * Check to see if a row is 'open' or not. + * @param {node} nTr the table row to check + * @returns {boolean} true if the row is currently open, false otherwise + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable; + * + * // 'open' an information row when a row is clicked on + * $('#example tbody tr').click( function () { + * if ( oTable.fnIsOpen(this) ) { + * oTable.fnClose( this ); + * } else { + * oTable.fnOpen( this, "Temporary row opened", "info_row" ); + * } + * } ); + * + * oTable = $('#example').dataTable(); + * } ); + */ + this.fnIsOpen = function( nTr ) + { + return this.api( true ).row( nTr ).child.isShown(); + }; + + + /** + * This function will place a new row directly after a row which is currently + * on display on the page, with the HTML contents that is passed into the + * function. This can be used, for example, to ask for confirmation that a + * particular record should be deleted. + * @param {node} nTr The table row to 'open' + * @param {string|node|jQuery} mHtml The HTML to put into the row + * @param {string} sClass Class to give the new TD cell + * @returns {node} The row opened. Note that if the table row passed in as the + * first parameter, is not found in the table, this method will silently + * return. + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable; + * + * // 'open' an information row when a row is clicked on + * $('#example tbody tr').click( function () { + * if ( oTable.fnIsOpen(this) ) { + * oTable.fnClose( this ); + * } else { + * oTable.fnOpen( this, "Temporary row opened", "info_row" ); + * } + * } ); + * + * oTable = $('#example').dataTable(); + * } ); + */ + this.fnOpen = function( nTr, mHtml, sClass ) + { + return this.api( true ) + .row( nTr ) + .child( mHtml, sClass ) + .show() + .child()[0]; + }; + + + /** + * Change the pagination - provides the internal logic for pagination in a simple API + * function. With this function you can have a DataTables table go to the next, + * previous, first or last pages. + * @param {string|int} mAction Paging action to take: "first", "previous", "next" or "last" + * or page number to jump to (integer), note that page 0 is the first page. + * @param {bool} [bRedraw=true] Redraw the table or not + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * oTable.fnPageChange( 'next' ); + * } ); + */ + this.fnPageChange = function ( mAction, bRedraw ) + { + var api = this.api( true ).page( mAction ); + + if ( bRedraw === undefined || bRedraw ) { + api.draw(false); + } + }; + + + /** + * Show a particular column + * @param {int} iCol The column whose display should be changed + * @param {bool} bShow Show (true) or hide (false) the column + * @param {bool} [bRedraw=true] Redraw the table or not + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Hide the second column after initialisation + * oTable.fnSetColumnVis( 1, false ); + * } ); + */ + this.fnSetColumnVis = function ( iCol, bShow, bRedraw ) + { + var api = this.api( true ).column( iCol ).visible( bShow ); + + if ( bRedraw === undefined || bRedraw ) { + api.columns.adjust().draw(); + } + }; + + + /** + * Get the settings for a particular table for external manipulation + * @returns {object} DataTables settings object. See + * {@link DataTable.models.oSettings} + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * var oSettings = oTable.fnSettings(); + * + * // Show an example parameter from the settings + * alert( oSettings._iDisplayStart ); + * } ); + */ + this.fnSettings = function() + { + return _fnSettingsFromNode( this[_ext.iApiIndex] ); + }; + + + /** + * Sort the table by a particular column + * @param {int} iCol the data index to sort on. Note that this will not match the + * 'display index' if you have hidden data entries + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Sort immediately with columns 0 and 1 + * oTable.fnSort( [ [0,'asc'], [1,'asc'] ] ); + * } ); + */ + this.fnSort = function( aaSort ) + { + this.api( true ).order( aaSort ).draw(); + }; + + + /** + * Attach a sort listener to an element for a given column + * @param {node} nNode the element to attach the sort listener to + * @param {int} iColumn the column that a click on this node will sort on + * @param {function} [fnCallback] callback function when sort is run + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Sort on column 1, when 'sorter' is clicked on + * oTable.fnSortListener( document.getElementById('sorter'), 1 ); + * } ); + */ + this.fnSortListener = function( nNode, iColumn, fnCallback ) + { + this.api( true ).order.listener( nNode, iColumn, fnCallback ); + }; + + + /** + * Update a table cell or row - this method will accept either a single value to + * update the cell with, an array of values with one element for each column or + * an object in the same format as the original data source. The function is + * self-referencing in order to make the multi column updates easier. + * @param {object|array|string} mData Data to update the cell/row with + * @param {node|int} mRow TR element you want to update or the aoData index + * @param {int} [iColumn] The column to update, give as null or undefined to + * update a whole row. + * @param {bool} [bRedraw=true] Redraw the table or not + * @param {bool} [bAction=true] Perform pre-draw actions or not + * @returns {int} 0 on success, 1 on error + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * oTable.fnUpdate( 'Example update', 0, 0 ); // Single cell + * oTable.fnUpdate( ['a', 'b', 'c', 'd', 'e'], $('tbody tr')[0] ); // Row + * } ); + */ + this.fnUpdate = function( mData, mRow, iColumn, bRedraw, bAction ) + { + var api = this.api( true ); + + if ( iColumn === undefined || iColumn === null ) { + api.row( mRow ).data( mData ); + } + else { + api.cell( mRow, iColumn ).data( mData ); + } + + if ( bAction === undefined || bAction ) { + api.columns.adjust(); + } + + if ( bRedraw === undefined || bRedraw ) { + api.draw(); + } + return 0; + }; + + + /** + * Provide a common method for plug-ins to check the version of DataTables being used, in order + * to ensure compatibility. + * @param {string} sVersion Version string to check for, in the format "X.Y.Z". Note that the + * formats "X" and "X.Y" are also acceptable. + * @returns {boolean} true if this version of DataTables is greater or equal to the required + * version, or false if this version of DataTales is not suitable + * @method + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * alert( oTable.fnVersionCheck( '1.9.0' ) ); + * } ); + */ + this.fnVersionCheck = _ext.fnVersionCheck; + + + var _that = this; + var emptyInit = options === undefined; + var len = this.length; + + if ( emptyInit ) { + options = {}; + } + + this.oApi = this.internal = _ext.internal; + + // Extend with old style plug-in API methods + for ( var fn in DataTable.ext.internal ) { + if ( fn ) { + this[fn] = _fnExternApiFunc(fn); + } + } + + this.each(function() { + // For each initialisation we want to give it a clean initialisation + // object that can be bashed around + var o = {}; + var oInit = len > 1 ? // optimisation for single table case + _fnExtend( o, options, true ) : + options; + + /*global oInit,_that,emptyInit*/ + var i=0, iLen, j, jLen, k, kLen; + var sId = this.getAttribute( 'id' ); + var bInitHandedOff = false; + var defaults = DataTable.defaults; + + + /* Sanity check */ + if ( this.nodeName.toLowerCase() != 'table' ) + { + _fnLog( null, 0, 'Non-table node initialisation ('+this.nodeName+')', 2 ); + return; + } + + /* Backwards compatibility for the defaults */ + _fnCompatOpts( defaults ); + _fnCompatCols( defaults.column ); + + /* Convert the camel-case defaults to Hungarian */ + _fnCamelToHungarian( defaults, defaults, true ); + _fnCamelToHungarian( defaults.column, defaults.column, true ); + + /* Setting up the initialisation object */ + _fnCamelToHungarian( defaults, oInit ); + + /* Check to see if we are re-initialising a table */ + var allSettings = DataTable.settings; + for ( i=0, iLen=allSettings.length ; i<iLen ; i++ ) + { + /* Base check on table node */ + if ( allSettings[i].nTable == this ) + { + var bRetrieve = oInit.bRetrieve !== undefined ? oInit.bRetrieve : defaults.bRetrieve; + var bDestroy = oInit.bDestroy !== undefined ? oInit.bDestroy : defaults.bDestroy; + + if ( emptyInit || bRetrieve ) + { + return allSettings[i].oInstance; + } + else if ( bDestroy ) + { + allSettings[i].oInstance.fnDestroy(); + break; + } + else + { + _fnLog( allSettings[i], 0, 'Cannot reinitialise DataTable', 3 ); + return; + } + } + + /* If the element we are initialising has the same ID as a table which was previously + * initialised, but the table nodes don't match (from before) then we destroy the old + * instance by simply deleting it. This is under the assumption that the table has been + * destroyed by other methods. Anyone using non-id selectors will need to do this manually + */ + if ( allSettings[i].sTableId == this.id ) + { + allSettings.splice( i, 1 ); + break; + } + } + + /* Ensure the table has an ID - required for accessibility */ + if ( sId === null || sId === "" ) + { + sId = "DataTables_Table_"+(DataTable.ext._unique++); + this.id = sId; + } + + /* Create the settings object for this table and set some of the default parameters */ + var oSettings = $.extend( true, {}, DataTable.models.oSettings, { + "nTable": this, + "oApi": _that.internal, + "oInit": oInit, + "sDestroyWidth": $(this)[0].style.width, + "sInstance": sId, + "sTableId": sId + } ); + allSettings.push( oSettings ); + + // Need to add the instance after the instance after the settings object has been added + // to the settings array, so we can self reference the table instance if more than one + oSettings.oInstance = (_that.length===1) ? _that : $(this).dataTable(); + + // Backwards compatibility, before we apply all the defaults + _fnCompatOpts( oInit ); + + if ( oInit.oLanguage ) + { + _fnLanguageCompat( oInit.oLanguage ); + } + + // If the length menu is given, but the init display length is not, use the length menu + if ( oInit.aLengthMenu && ! oInit.iDisplayLength ) + { + oInit.iDisplayLength = $.isArray( oInit.aLengthMenu[0] ) ? + oInit.aLengthMenu[0][0] : oInit.aLengthMenu[0]; + } + + // Apply the defaults and init options to make a single init object will all + // options defined from defaults and instance options. + oInit = _fnExtend( $.extend( true, {}, defaults ), oInit ); + + + // Map the initialisation options onto the settings object + _fnMap( oSettings.oFeatures, oInit, [ + "bPaginate", + "bLengthChange", + "bFilter", + "bSort", + "bSortMulti", + "bInfo", + "bProcessing", + "bAutoWidth", + "bSortClasses", + "bServerSide", + "bDeferRender" + ] ); + _fnMap( oSettings, oInit, [ + "asStripeClasses", + "ajax", + "fnServerData", + "fnFormatNumber", + "sServerMethod", + "aaSorting", + "aaSortingFixed", + "aLengthMenu", + "sPaginationType", + "sAjaxSource", + "sAjaxDataProp", + "iStateDuration", + "sDom", + "bSortCellsTop", + "iTabIndex", + "fnStateLoadCallback", + "fnStateSaveCallback", + "renderer", + [ "iCookieDuration", "iStateDuration" ], // backwards compat + [ "oSearch", "oPreviousSearch" ], + [ "aoSearchCols", "aoPreSearchCols" ], + [ "iDisplayLength", "_iDisplayLength" ], + [ "bJQueryUI", "bJUI" ] + ] ); + _fnMap( oSettings.oScroll, oInit, [ + [ "sScrollX", "sX" ], + [ "sScrollXInner", "sXInner" ], + [ "sScrollY", "sY" ], + [ "bScrollCollapse", "bCollapse" ] + ] ); + _fnMap( oSettings.oLanguage, oInit, "fnInfoCallback" ); + + /* Callback functions which are array driven */ + _fnCallbackReg( oSettings, 'aoDrawCallback', oInit.fnDrawCallback, 'user' ); + _fnCallbackReg( oSettings, 'aoServerParams', oInit.fnServerParams, 'user' ); + _fnCallbackReg( oSettings, 'aoStateSaveParams', oInit.fnStateSaveParams, 'user' ); + _fnCallbackReg( oSettings, 'aoStateLoadParams', oInit.fnStateLoadParams, 'user' ); + _fnCallbackReg( oSettings, 'aoStateLoaded', oInit.fnStateLoaded, 'user' ); + _fnCallbackReg( oSettings, 'aoRowCallback', oInit.fnRowCallback, 'user' ); + _fnCallbackReg( oSettings, 'aoRowCreatedCallback', oInit.fnCreatedRow, 'user' ); + _fnCallbackReg( oSettings, 'aoHeaderCallback', oInit.fnHeaderCallback, 'user' ); + _fnCallbackReg( oSettings, 'aoFooterCallback', oInit.fnFooterCallback, 'user' ); + _fnCallbackReg( oSettings, 'aoInitComplete', oInit.fnInitComplete, 'user' ); + _fnCallbackReg( oSettings, 'aoPreDrawCallback', oInit.fnPreDrawCallback, 'user' ); + + var oClasses = oSettings.oClasses; + + // @todo Remove in 1.11 + if ( oInit.bJQueryUI ) + { + /* Use the JUI classes object for display. You could clone the oStdClasses object if + * you want to have multiple tables with multiple independent classes + */ + $.extend( oClasses, DataTable.ext.oJUIClasses, oInit.oClasses ); + + if ( oInit.sDom === defaults.sDom && defaults.sDom === "lfrtip" ) + { + /* Set the DOM to use a layout suitable for jQuery UI's theming */ + oSettings.sDom = '<"H"lfr>t<"F"ip>'; + } + + if ( ! oSettings.renderer ) { + oSettings.renderer = 'jqueryui'; + } + else if ( $.isPlainObject( oSettings.renderer ) && ! oSettings.renderer.header ) { + oSettings.renderer.header = 'jqueryui'; + } + } + else + { + $.extend( oClasses, DataTable.ext.classes, oInit.oClasses ); + } + $(this).addClass( oClasses.sTable ); + + /* Calculate the scroll bar width and cache it for use later on */ + if ( oSettings.oScroll.sX !== "" || oSettings.oScroll.sY !== "" ) + { + oSettings.oScroll.iBarWidth = _fnScrollBarWidth(); + } + if ( oSettings.oScroll.sX === true ) { // Easy initialisation of x-scrolling + oSettings.oScroll.sX = '100%'; + } + + if ( oSettings.iInitDisplayStart === undefined ) + { + /* Display start point, taking into account the save saving */ + oSettings.iInitDisplayStart = oInit.iDisplayStart; + oSettings._iDisplayStart = oInit.iDisplayStart; + } + + if ( oInit.iDeferLoading !== null ) + { + oSettings.bDeferLoading = true; + var tmp = $.isArray( oInit.iDeferLoading ); + oSettings._iRecordsDisplay = tmp ? oInit.iDeferLoading[0] : oInit.iDeferLoading; + oSettings._iRecordsTotal = tmp ? oInit.iDeferLoading[1] : oInit.iDeferLoading; + } + + /* Language definitions */ + if ( oInit.oLanguage.sUrl !== "" ) + { + /* Get the language definitions from a file - because this Ajax call makes the language + * get async to the remainder of this function we use bInitHandedOff to indicate that + * _fnInitialise will be fired by the returned Ajax handler, rather than the constructor + */ + oSettings.oLanguage.sUrl = oInit.oLanguage.sUrl; + $.getJSON( oSettings.oLanguage.sUrl, null, function( json ) { + _fnLanguageCompat( json ); + _fnCamelToHungarian( defaults.oLanguage, json ); + $.extend( true, oSettings.oLanguage, oInit.oLanguage, json ); + _fnInitialise( oSettings ); + } ); + bInitHandedOff = true; + } + else + { + $.extend( true, oSettings.oLanguage, oInit.oLanguage ); + } + + + /* + * Stripes + */ + if ( oInit.asStripeClasses === null ) + { + oSettings.asStripeClasses =[ + oClasses.sStripeOdd, + oClasses.sStripeEven + ]; + } + + /* Remove row stripe classes if they are already on the table row */ + var stripeClasses = oSettings.asStripeClasses; + var rowOne = $('tbody tr:eq(0)', this); + if ( $.inArray( true, $.map( stripeClasses, function(el, i) { + return rowOne.hasClass(el); + } ) ) !== -1 ) { + $('tbody tr', this).removeClass( stripeClasses.join(' ') ); + oSettings.asDestroyStripes = stripeClasses.slice(); + } + + /* + * Columns + * See if we should load columns automatically or use defined ones + */ + var anThs = []; + var aoColumnsInit; + var nThead = this.getElementsByTagName('thead'); + if ( nThead.length !== 0 ) + { + _fnDetectHeader( oSettings.aoHeader, nThead[0] ); + anThs = _fnGetUniqueThs( oSettings ); + } + + /* If not given a column array, generate one with nulls */ + if ( oInit.aoColumns === null ) + { + aoColumnsInit = []; + for ( i=0, iLen=anThs.length ; i<iLen ; i++ ) + { + aoColumnsInit.push( null ); + } + } + else + { + aoColumnsInit = oInit.aoColumns; + } + + /* Add the columns */ + for ( i=0, iLen=aoColumnsInit.length ; i<iLen ; i++ ) + { + _fnAddColumn( oSettings, anThs ? anThs[i] : null ); + } + + /* Apply the column definitions */ + _fnApplyColumnDefs( oSettings, oInit.aoColumnDefs, aoColumnsInit, function (iCol, oDef) { + _fnColumnOptions( oSettings, iCol, oDef ); + } ); + + /* HTML5 attribute detection - build an mData object automatically if the + * attributes are found + */ + if ( rowOne.length ) { + var a = function ( cell, name ) { + return cell.getAttribute( 'data-'+name ) ? name : null; + }; + + $.each( _fnGetRowElements( oSettings, rowOne[0] ).cells, function (i, cell) { + var col = oSettings.aoColumns[i]; + + if ( col.mData === i ) { + var sort = a( cell, 'sort' ) || a( cell, 'order' ); + var filter = a( cell, 'filter' ) || a( cell, 'search' ); + + if ( sort !== null || filter !== null ) { + col.mData = { + _: i+'.display', + sort: sort !== null ? i+'.@data-'+sort : undefined, + type: sort !== null ? i+'.@data-'+sort : undefined, + filter: filter !== null ? i+'.@data-'+filter : undefined + }; + + _fnColumnOptions( oSettings, i ); + } + } + } ); + } + + var features = oSettings.oFeatures; + + /* Must be done after everything which can be overridden by the state saving! */ + if ( oInit.bStateSave ) + { + features.bStateSave = true; + _fnLoadState( oSettings, oInit ); + _fnCallbackReg( oSettings, 'aoDrawCallback', _fnSaveState, 'state_save' ); + } + + + /* + * Sorting + * @todo For modularisation (1.11) this needs to do into a sort start up handler + */ + + // If aaSorting is not defined, then we use the first indicator in asSorting + // in case that has been altered, so the default sort reflects that option + if ( oInit.aaSorting === undefined ) + { + var sorting = oSettings.aaSorting; + for ( i=0, iLen=sorting.length ; i<iLen ; i++ ) + { + sorting[i][1] = oSettings.aoColumns[ i ].asSorting[0]; + } + } + + /* Do a first pass on the sorting classes (allows any size changes to be taken into + * account, and also will apply sorting disabled classes if disabled + */ + _fnSortingClasses( oSettings ); + + if ( features.bSort ) + { + _fnCallbackReg( oSettings, 'aoDrawCallback', function () { + if ( oSettings.bSorted ) { + var aSort = _fnSortFlatten( oSettings ); + var sortedColumns = {}; + + $.each( aSort, function (i, val) { + sortedColumns[ val.src ] = val.dir; + } ); + + _fnCallbackFire( oSettings, null, 'order', [oSettings, aSort, sortedColumns] ); + _fnSortAria( oSettings ); + } + } ); + } + + _fnCallbackReg( oSettings, 'aoDrawCallback', function () { + if ( oSettings.bSorted || _fnDataSource( oSettings ) === 'ssp' || features.bDeferRender ) { + _fnSortingClasses( oSettings ); + } + }, 'sc' ); + + + /* + * Final init + * Cache the header, body and footer as required, creating them if needed + */ + + /* Browser support detection */ + _fnBrowserDetect( oSettings ); + + // Work around for Webkit bug 83867 - store the caption-side before removing from doc + var captions = $(this).children('caption').each( function () { + this._captionSide = $(this).css('caption-side'); + } ); + + var thead = $(this).children('thead'); + if ( thead.length === 0 ) + { + thead = $('<thead/>').appendTo(this); + } + oSettings.nTHead = thead[0]; + + var tbody = $(this).children('tbody'); + if ( tbody.length === 0 ) + { + tbody = $('<tbody/>').appendTo(this); + } + oSettings.nTBody = tbody[0]; + + var tfoot = $(this).children('tfoot'); + if ( tfoot.length === 0 && captions.length > 0 && (oSettings.oScroll.sX !== "" || oSettings.oScroll.sY !== "") ) + { + // If we are a scrolling table, and no footer has been given, then we need to create + // a tfoot element for the caption element to be appended to + tfoot = $('<tfoot/>').appendTo(this); + } + + if ( tfoot.length === 0 || tfoot.children().length === 0 ) { + $(this).addClass( oClasses.sNoFooter ); + } + else if ( tfoot.length > 0 ) { + oSettings.nTFoot = tfoot[0]; + _fnDetectHeader( oSettings.aoFooter, oSettings.nTFoot ); + } + + /* Check if there is data passing into the constructor */ + if ( oInit.aaData ) + { + for ( i=0 ; i<oInit.aaData.length ; i++ ) + { + _fnAddData( oSettings, oInit.aaData[ i ] ); + } + } + else if ( oSettings.bDeferLoading || _fnDataSource( oSettings ) == 'dom' ) + { + /* Grab the data from the page - only do this when deferred loading or no Ajax + * source since there is no point in reading the DOM data if we are then going + * to replace it with Ajax data + */ + _fnAddTr( oSettings, $(oSettings.nTBody).children('tr') ); + } + + /* Copy the data index array */ + oSettings.aiDisplay = oSettings.aiDisplayMaster.slice(); + + /* Initialisation complete - table can be drawn */ + oSettings.bInitialised = true; + + /* Check if we need to initialise the table (it might not have been handed off to the + * language processor) + */ + if ( bInitHandedOff === false ) + { + _fnInitialise( oSettings ); + } + } ); + _that = null; + return this; + }; + + + + /** + * Computed structure of the DataTables API, defined by the options passed to + * `DataTable.Api.register()` when building the API. + * + * The structure is built in order to speed creation and extension of the Api + * objects since the extensions are effectively pre-parsed. + * + * The array is an array of objects with the following structure, where this + * base array represents the Api prototype base: + * + * [ + * { + * name: 'data' -- string - Property name + * val: function () {}, -- function - Api method (or undefined if just an object + * methodExt: [ ... ], -- array - Array of Api object definitions to extend the method result + * propExt: [ ... ] -- array - Array of Api object definitions to extend the property + * }, + * { + * name: 'row' + * val: {}, + * methodExt: [ ... ], + * propExt: [ + * { + * name: 'data' + * val: function () {}, + * methodExt: [ ... ], + * propExt: [ ... ] + * }, + * ... + * ] + * } + * ] + * + * @type {Array} + * @ignore + */ + var __apiStruct = []; + + + /** + * `Array.prototype` reference. + * + * @type object + * @ignore + */ + var __arrayProto = Array.prototype; + + + /** + * Abstraction for `context` parameter of the `Api` constructor to allow it to + * take several different forms for ease of use. + * + * Each of the input parameter types will be converted to a DataTables settings + * object where possible. + * + * @param {string|node|jQuery|object} mixed DataTable identifier. Can be one + * of: + * + * * `string` - jQuery selector. Any DataTables' matching the given selector + * with be found and used. + * * `node` - `TABLE` node which has already been formed into a DataTable. + * * `jQuery` - A jQuery object of `TABLE` nodes. + * * `object` - DataTables settings object + * * `DataTables.Api` - API instance + * @return {array|null} Matching DataTables settings objects. `null` or + * `undefined` is returned if no matching DataTable is found. + * @ignore + */ + var _toSettings = function ( mixed ) + { + var idx, jq; + var settings = DataTable.settings; + var tables = $.map( settings, function (el, i) { + return el.nTable; + } ); + + if ( ! mixed ) { + return []; + } + else if ( mixed.nTable && mixed.oApi ) { + // DataTables settings object + return [ mixed ]; + } + else if ( mixed.nodeName && mixed.nodeName.toLowerCase() === 'table' ) { + // Table node + idx = $.inArray( mixed, tables ); + return idx !== -1 ? [ settings[idx] ] : null; + } + else if ( mixed && typeof mixed.settings === 'function' ) { + return mixed.settings().toArray(); + } + else if ( typeof mixed === 'string' ) { + // jQuery selector + jq = $(mixed); + } + else if ( mixed instanceof $ ) { + // jQuery object (also DataTables instance) + jq = mixed; + } + + if ( jq ) { + return jq.map( function(i) { + idx = $.inArray( this, tables ); + return idx !== -1 ? settings[idx] : null; + } ).toArray(); + } + }; + + + /** + * DataTables API class - used to control and interface with one or more + * DataTables enhanced tables. + * + * The API class is heavily based on jQuery, presenting a chainable interface + * that you can use to interact with tables. Each instance of the API class has + * a "context" - i.e. the tables that it will operate on. This could be a single + * table, all tables on a page or a sub-set thereof. + * + * Additionally the API is designed to allow you to easily work with the data in + * the tables, retrieving and manipulating it as required. This is done by + * presenting the API class as an array like interface. The contents of the + * array depend upon the actions requested by each method (for example + * `rows().nodes()` will return an array of nodes, while `rows().data()` will + * return an array of objects or arrays depending upon your table's + * configuration). The API object has a number of array like methods (`push`, + * `pop`, `reverse` etc) as well as additional helper methods (`each`, `pluck`, + * `unique` etc) to assist your working with the data held in a table. + * + * Most methods (those which return an Api instance) are chainable, which means + * the return from a method call also has all of the methods available that the + * top level object had. For example, these two calls are equivalent: + * + * // Not chained + * api.row.add( {...} ); + * api.draw(); + * + * // Chained + * api.row.add( {...} ).draw(); + * + * @class DataTable.Api + * @param {array|object|string|jQuery} context DataTable identifier. This is + * used to define which DataTables enhanced tables this API will operate on. + * Can be one of: + * + * * `string` - jQuery selector. Any DataTables' matching the given selector + * with be found and used. + * * `node` - `TABLE` node which has already been formed into a DataTable. + * * `jQuery` - A jQuery object of `TABLE` nodes. + * * `object` - DataTables settings object + * @param {array} [data] Data to initialise the Api instance with. + * + * @example + * // Direct initialisation during DataTables construction + * var api = $('#example').DataTable(); + * + * @example + * // Initialisation using a DataTables jQuery object + * var api = $('#example').dataTable().api(); + * + * @example + * // Initialisation as a constructor + * var api = new $.fn.DataTable.Api( 'table.dataTable' ); + */ + DataTable.Api = _Api = function ( context, data ) + { + if ( ! this instanceof _Api ) { + throw 'DT API must be constructed as a new object'; + // or should it do the 'new' for the caller? + // return new _Api.apply( this, arguments ); + } + + var settings = []; + var ctxSettings = function ( o ) { + var a = _toSettings( o ); + if ( a ) { + settings.push.apply( settings, a ); + } + }; + + if ( $.isArray( context ) ) { + for ( var i=0, ien=context.length ; i<ien ; i++ ) { + ctxSettings( context[i] ); + } + } + else { + ctxSettings( context ); + } + + // Remove duplicates + this.context = _unique( settings ); + + // Initial data + if ( data ) { + this.push.apply( this, data.toArray ? data.toArray() : data ); + } + + // selector + this.selector = { + rows: null, + cols: null, + opts: null + }; + + _Api.extend( this, this, __apiStruct ); + }; + + + _Api.prototype = /** @lends DataTables.Api */{ + /** + * Return a new Api instance, comprised of the data held in the current + * instance, join with the other array(s) and/or value(s). + * + * An alias for `Array.prototype.concat`. + * + * @type method + * @param {*} value1 Arrays and/or values to concatenate. + * @param {*} [...] Additional arrays and/or values to concatenate. + * @returns {DataTables.Api} New API instance, comprising of the combined + * array. + */ + concat: __arrayProto.concat, + + + context: [], // array of table settings objects + + + each: function ( fn ) + { + if ( __arrayProto.forEach ) { + // Where possible, use the built-in forEach + __arrayProto.forEach.call( this, fn, this ); + } + else { + // Compatibility for browsers without EMCA-252-5 (JS 1.6) + for ( var i=0, ien=this.length ; i<ien; i++ ) { + // In strict mode the execution scope is the passed value + fn.call( this, this[i], i, this ); + } + } + + return this; + }, + + + eq: function ( idx ) + { + var ctx = this.context; + + return ctx.length > idx ? + new _Api( ctx[idx], this[idx] ) : + null; + }, + + + filter: function ( fn ) + { + var a = []; + + if ( __arrayProto.filter ) { + a = __arrayProto.filter.call( this, fn, this ); + } + else { + // Compatibility for browsers without EMCA-252-5 (JS 1.6) + for ( var i=0, ien=this.length ; i<ien ; i++ ) { + if ( fn.call( this, this[i], i, this ) ) { + a.push( this[i] ); + } + } + } + + return new _Api( this.context, a ); + }, + + + flatten: function () + { + var a = []; + return new _Api( this.context, a.concat.apply( a, this.toArray() ) ); + }, + + + join: __arrayProto.join, + + + indexOf: __arrayProto.indexOf || function (obj, start) + { + for ( var i=(start || 0), ien=this.length ; i<ien ; i++ ) { + if ( this[i] === obj ) { + return i; + } + } + return -1; + }, + + // Internal only at the moment - relax? + iterator: function ( flatten, type, fn ) { + var + a = [], ret, + i, ien, j, jen, + context = this.context, + rows, items, item, + selector = this.selector; + + // Argument shifting + if ( typeof flatten === 'string' ) { + fn = type; + type = flatten; + flatten = false; + } + + for ( i=0, ien=context.length ; i<ien ; i++ ) { + if ( type === 'table' ) { + ret = fn( context[i], i ); + + if ( ret !== undefined ) { + a.push( ret ); + } + } + else if ( type === 'columns' || type === 'rows' ) { + // this has same length as context - one entry for each table + ret = fn( context[i], this[i], i ); + + if ( ret !== undefined ) { + a.push( ret ); + } + } + else if ( type === 'column' || type === 'column-rows' || type === 'row' || type === 'cell' ) { + // columns and rows share the same structure. + // 'this' is an array of column indexes for each context + items = this[i]; + + if ( type === 'column-rows' ) { + rows = _selector_row_indexes( context[i], selector.opts ); + } + + for ( j=0, jen=items.length ; j<jen ; j++ ) { + item = items[j]; + + if ( type === 'cell' ) { + ret = fn( context[i], item.row, item.column, i, j ); + } + else { + ret = fn( context[i], item, i, j, rows ); + } + + if ( ret !== undefined ) { + a.push( ret ); + } + } + } + } + + if ( a.length ) { + var api = new _Api( context, flatten ? a.concat.apply( [], a ) : a ); + var apiSelector = api.selector; + apiSelector.rows = selector.rows; + apiSelector.cols = selector.cols; + apiSelector.opts = selector.opts; + return api; + } + return this; + }, + + + lastIndexOf: __arrayProto.lastIndexOf || function (obj, start) + { + // Bit cheeky... + return this.indexOf.apply( this.toArray.reverse(), arguments ); + }, + + + length: 0, + + + map: function ( fn ) + { + var a = []; + + if ( __arrayProto.map ) { + a = __arrayProto.map.call( this, fn, this ); + } + else { + // Compatibility for browsers without EMCA-252-5 (JS 1.6) + for ( var i=0, ien=this.length ; i<ien ; i++ ) { + a.push( fn.call( this, this[i], i ) ); + } + } + + return new _Api( this.context, a ); + }, + + + pluck: function ( prop ) + { + return this.map( function ( el ) { + return el[ prop ]; + } ); + }, + + pop: __arrayProto.pop, + + + push: __arrayProto.push, + + + // Does not return an API instance + reduce: __arrayProto.reduce || function ( fn, init ) + { + return _fnReduce( this, fn, init, 0, this.length, 1 ); + }, + + + reduceRight: __arrayProto.reduceRight || function ( fn, init ) + { + return _fnReduce( this, fn, init, this.length-1, -1, -1 ); + }, + + + reverse: __arrayProto.reverse, + + + // Object with rows, columns and opts + selector: null, + + + shift: __arrayProto.shift, + + + sort: __arrayProto.sort, // ? name - order? + + + splice: __arrayProto.splice, + + + toArray: function () + { + return __arrayProto.slice.call( this ); + }, + + + to$: function () + { + return $( this ); + }, + + + toJQuery: function () + { + return $( this ); + }, + + + unique: function () + { + return new _Api( this.context, _unique(this) ); + }, + + + unshift: __arrayProto.unshift + }; + + + _Api.extend = function ( scope, obj, ext ) + { + // Only extend API instances and static properties of the API + if ( ! obj || ( ! (obj instanceof _Api) && ! obj.__dt_wrapper ) ) { + return; + } + + var + i, ien, + j, jen, + struct, inner, + methodScoping = function ( fn, struc ) { + return function () { + var ret = fn.apply( scope, arguments ); + + // Method extension + _Api.extend( ret, ret, struc.methodExt ); + return ret; + }; + }; + + for ( i=0, ien=ext.length ; i<ien ; i++ ) { + struct = ext[i]; + + // Value + obj[ struct.name ] = typeof struct.val === 'function' ? + methodScoping( struct.val, struct ) : + $.isPlainObject( struct.val ) ? + {} : + struct.val; + + obj[ struct.name ].__dt_wrapper = true; + + // Property extension + _Api.extend( scope, obj[ struct.name ], struct.propExt ); + } + }; + + + // @todo - Is there need for an augment function? + // _Api.augment = function ( inst, name ) + // { + // // Find src object in the structure from the name + // var parts = name.split('.'); + + // _Api.extend( inst, obj ); + // }; + + + // [ + // { + // name: 'data' -- string - Property name + // val: function () {}, -- function - Api method (or undefined if just an object + // methodExt: [ ... ], -- array - Array of Api object definitions to extend the method result + // propExt: [ ... ] -- array - Array of Api object definitions to extend the property + // }, + // { + // name: 'row' + // val: {}, + // methodExt: [ ... ], + // propExt: [ + // { + // name: 'data' + // val: function () {}, + // methodExt: [ ... ], + // propExt: [ ... ] + // }, + // ... + // ] + // } + // ] + + _Api.register = _api_register = function ( name, val ) + { + if ( $.isArray( name ) ) { + for ( var j=0, jen=name.length ; j<jen ; j++ ) { + _Api.register( name[j], val ); + } + return; + } + + var + i, ien, + heir = name.split('.'), + struct = __apiStruct, + key, method; + + var find = function ( src, name ) { + for ( var i=0, ien=src.length ; i<ien ; i++ ) { + if ( src[i].name === name ) { + return src[i]; + } + } + return null; + }; + + for ( i=0, ien=heir.length ; i<ien ; i++ ) { + method = heir[i].indexOf('()') !== -1; + key = method ? + heir[i].replace('()', '') : + heir[i]; + + var src = find( struct, key ); + if ( ! src ) { + src = { + name: key, + val: {}, + methodExt: [], + propExt: [] + }; + struct.push( src ); + } + + if ( i === ien-1 ) { + src.val = val; + } + else { + struct = method ? + src.methodExt : + src.propExt; + } + } + + // Rebuild the API with the new construct + if ( _Api.ready ) { + DataTable.api.build(); + } + }; + + + _Api.registerPlural = _api_registerPlural = function ( pluralName, singularName, val ) { + _Api.register( pluralName, val ); + + _Api.register( singularName, function () { + var ret = val.apply( this, arguments ); + + if ( ret === this ) { + // Returned item is the API instance that was passed in, return it + return this; + } + else if ( ret instanceof _Api ) { + // New API instance returned, want the value from the first item + // in the returned array for the singular result. + return ret.length ? + $.isArray( ret[0] ) ? + new _Api( ret.context, ret[0] ) : // Array results are 'enhanced' + ret[0] : + undefined; + } + + // Non-API return - just fire it back + return ret; + } ); + }; + + + /** + * Selector for HTML tables. Apply the given selector to the give array of + * DataTables settings objects. + * + * @param {string|integer} [selector] jQuery selector string or integer + * @param {array} Array of DataTables settings objects to be filtered + * @return {array} + * @ignore + */ + var __table_selector = function ( selector, a ) + { + // Integer is used to pick out a table by index + if ( typeof selector === 'number' ) { + return [ a[ selector ] ]; + } + + // Perform a jQuery selector on the table nodes + var nodes = $.map( a, function (el, i) { + return el.nTable; + } ); + + return $(nodes) + .filter( selector ) + .map( function (i) { + // Need to translate back from the table node to the settings + var idx = $.inArray( this, nodes ); + return a[ idx ]; + } ) + .toArray(); + }; + + + + /** + * Context selector for the API's context (i.e. the tables the API instance + * refers to. + * + * @name DataTable.Api#tables + * @param {string|integer} [selector] Selector to pick which tables the iterator + * should operate on. If not given, all tables in the current context are + * used. This can be given as a jQuery selector (for example `':gt(0)'`) to + * select multiple tables or as an integer to select a single table. + * @returns {DataTable.Api} Returns a new API instance if a selector is given. + */ + _api_register( 'tables()', function ( selector ) { + // A new instance is created if there was a selector specified + return selector ? + new _Api( __table_selector( selector, this.context ) ) : + this; + } ); + + + _api_register( 'table()', function ( selector ) { + var tables = this.tables( selector ); + var ctx = tables.context; + + // Truncate to the first matched table + return ctx.length ? + new _Api( ctx[0] ) : + tables; + } ); + + + _api_registerPlural( 'tables().nodes()', 'table().node()' , function () { + return this.iterator( 'table', function ( ctx ) { + return ctx.nTable; + } ); + } ); + + + _api_registerPlural( 'tables().body()', 'table().body()' , function () { + return this.iterator( 'table', function ( ctx ) { + return ctx.nTBody; + } ); + } ); + + + _api_registerPlural( 'tables().header()', 'table().header()' , function () { + return this.iterator( 'table', function ( ctx ) { + return ctx.nTHead; + } ); + } ); + + + _api_registerPlural( 'tables().footer()', 'table().footer()' , function () { + return this.iterator( 'table', function ( ctx ) { + return ctx.nTFoot; + } ); + } ); + + + + /** + * Redraw the tables in the current context. + * + * @param {boolean} [reset=true] Reset (default) or hold the current paging + * position. A full re-sort and re-filter is performed when this method is + * called, which is why the pagination reset is the default action. + * @returns {DataTables.Api} this + */ + _api_register( 'draw()', function ( resetPaging ) { + return this.iterator( 'table', function ( settings ) { + _fnReDraw( settings, resetPaging===false ); + } ); + } ); + + + + /** + * Get the current page index. + * + * @return {integer} Current page index (zero based) + *//** + * Set the current page. + * + * Note that if you attempt to show a page which does not exist, DataTables will + * not throw an error, but rather reset the paging. + * + * @param {integer|string} action The paging action to take. This can be one of: + * * `integer` - The page index to jump to + * * `string` - An action to take: + * * `first` - Jump to first page. + * * `next` - Jump to the next page + * * `previous` - Jump to previous page + * * `last` - Jump to the last page. + * @returns {DataTables.Api} this + */ + _api_register( 'page()', function ( action ) { + if ( action === undefined ) { + return this.page.info().page; // not an expensive call + } + + // else, have an action to take on all tables + return this.iterator( 'table', function ( settings ) { + _fnPageChange( settings, action ); + } ); + } ); + + + /** + * Paging information for the first table in the current context. + * + * If you require paging information for another table, use the `table()` method + * with a suitable selector. + * + * @return {object} Object with the following properties set: + * * `page` - Current page index (zero based - i.e. the first page is `0`) + * * `pages` - Total number of pages + * * `start` - Display index for the first record shown on the current page + * * `end` - Display index for the last record shown on the current page + * * `length` - Display length (number of records). Note that generally `start + * + length = end`, but this is not always true, for example if there are + * only 2 records to show on the final page, with a length of 10. + * * `recordsTotal` - Full data set length + * * `recordsDisplay` - Data set length once the current filtering criterion + * are applied. + */ + _api_register( 'page.info()', function ( action ) { + if ( this.context.length === 0 ) { + return undefined; + } + + var + settings = this.context[0], + start = settings._iDisplayStart, + len = settings._iDisplayLength, + visRecords = settings.fnRecordsDisplay(), + all = len === -1; + + return { + "page": all ? 0 : Math.floor( start / len ), + "pages": all ? 1 : Math.ceil( visRecords / len ), + "start": start, + "end": settings.fnDisplayEnd(), + "length": len, + "recordsTotal": settings.fnRecordsTotal(), + "recordsDisplay": visRecords + }; + } ); + + + /** + * Get the current page length. + * + * @return {integer} Current page length. Note `-1` indicates that all records + * are to be shown. + *//** + * Set the current page length. + * + * @param {integer} Page length to set. Use `-1` to show all records. + * @returns {DataTables.Api} this + */ + _api_register( 'page.len()', function ( len ) { + // Note that we can't call this function 'length()' because `length` + // is a Javascript property of functions which defines how many arguments + // the function expects. + if ( len === undefined ) { + return this.context.length !== 0 ? + this.context[0]._iDisplayLength : + undefined; + } + + // else, set the page length + return this.iterator( 'table', function ( settings ) { + _fnLengthChange( settings, len ); + } ); + } ); + + + + var __reload = function ( settings, holdPosition, callback ) { + if ( _fnDataSource( settings ) == 'ssp' ) { + _fnReDraw( settings, holdPosition ); + } + else { + // Trigger xhr + _fnProcessingDisplay( settings, true ); + + _fnBuildAjax( settings, [], function( json ) { + _fnClearTable( settings ); + + var data = _fnAjaxDataSrc( settings, json ); + for ( var i=0, ien=data.length ; i<ien ; i++ ) { + _fnAddData( settings, data[i] ); + } + + _fnReDraw( settings, holdPosition ); + _fnProcessingDisplay( settings, false ); + } ); + } + + // Use the draw event to trigger a callback, regardless of if it is an async + // or sync draw + if ( callback ) { + var api = new _Api( settings ); + + api.one( 'draw', function () { + callback( api.ajax.json() ); + } ); + } + }; + + + /** + * Get the JSON response from the last Ajax request that DataTables made to the + * server. Note that this returns the JSON from the first table in the current + * context. + * + * @return {object} JSON received from the server. + */ + _api_register( 'ajax.json()', function () { + var ctx = this.context; + + if ( ctx.length > 0 ) { + return ctx[0].json; + } + + // else return undefined; + } ); + + + /** + * Get the data submitted in the last Ajax request + */ + _api_register( 'ajax.params()', function () { + var ctx = this.context; + + if ( ctx.length > 0 ) { + return ctx[0].oAjaxData; + } + + // else return undefined; + } ); + + + /** + * Reload tables from the Ajax data source. Note that this function will + * automatically re-draw the table when the remote data has been loaded. + * + * @param {boolean} [reset=true] Reset (default) or hold the current paging + * position. A full re-sort and re-filter is performed when this method is + * called, which is why the pagination reset is the default action. + * @returns {DataTables.Api} this + */ + _api_register( 'ajax.reload()', function ( callback, resetPaging ) { + return this.iterator( 'table', function (settings) { + __reload( settings, resetPaging===false, callback ); + } ); + } ); + + + /** + * Get the current Ajax URL. Note that this returns the URL from the first + * table in the current context. + * + * @return {string} Current Ajax source URL + *//** + * Set the Ajax URL. Note that this will set the URL for all tables in the + * current context. + * + * @param {string} url URL to set. + * @returns {DataTables.Api} this + */ + _api_register( 'ajax.url()', function ( url ) { + var ctx = this.context; + + if ( url === undefined ) { + // get + if ( ctx.length === 0 ) { + return undefined; + } + ctx = ctx[0]; + + return ctx.ajax ? + $.isPlainObject( ctx.ajax ) ? + ctx.ajax.url : + ctx.ajax : + ctx.sAjaxSource; + } + + // set + return this.iterator( 'table', function ( settings ) { + if ( $.isPlainObject( settings.ajax ) ) { + settings.ajax.url = url; + } + else { + settings.ajax = url; + } + // No need to consider sAjaxSource here since DataTables gives priority + // to `ajax` over `sAjaxSource`. So setting `ajax` here, renders any + // value of `sAjaxSource` redundant. + } ); + } ); + + + /** + * Load data from the newly set Ajax URL. Note that this method is only + * available when `ajax.url()` is used to set a URL. Additionally, this method + * has the same effect as calling `ajax.reload()` but is provided for + * convenience when setting a new URL. Like `ajax.reload()` it will + * automatically redraw the table once the remote data has been loaded. + * + * @returns {DataTables.Api} this + */ + _api_register( 'ajax.url().load()', function ( callback, resetPaging ) { + // Same as a reload, but makes sense to present it for easy access after a + // url change + return this.iterator( 'table', function ( ctx ) { + __reload( ctx, resetPaging===false, callback ); + } ); + } ); + + + + + var _selector_run = function ( selector, select ) + { + var + out = [], res, + a, i, ien, j, jen; + + // Can't just check for isArray here, as an API or jQuery instance might be + // given with their array like look + if ( ! selector || typeof selector === 'string' || selector.length === undefined ) { + selector = [ selector ]; + } + + for ( i=0, ien=selector.length ; i<ien ; i++ ) { + a = selector[i] && selector[i].split ? + selector[i].split(',') : + [ selector[i] ]; + + for ( j=0, jen=a.length ; j<jen ; j++ ) { + res = select( typeof a[j] === 'string' ? $.trim(a[j]) : a[j] ); + + if ( res && res.length ) { + out.push.apply( out, res ); + } + } + } + + return out; + }; + + + var _selector_opts = function ( opts ) + { + if ( ! opts ) { + opts = {}; + } + + // Backwards compatibility for 1.9- which used the terminology filter rather + // than search + if ( opts.filter && ! opts.search ) { + opts.search = opts.filter; + } + + return { + search: opts.search || 'none', + order: opts.order || 'current', + page: opts.page || 'all' + }; + }; + + + var _selector_first = function ( inst ) + { + // Reduce the API instance to the first item found + for ( var i=0, ien=inst.length ; i<ien ; i++ ) { + if ( inst[i].length > 0 ) { + // Assign the first element to the first item in the instance + // and truncate the instance and context + inst[0] = inst[i]; + inst.length = 1; + inst.context = [ inst.context[i] ]; + + return inst; + } + } + + // Not found - return an empty instance + inst.length = 0; + return inst; + }; + + + var _selector_row_indexes = function ( settings, opts ) + { + var + i, ien, tmp, a=[], + displayFiltered = settings.aiDisplay, + displayMaster = settings.aiDisplayMaster; + + var + search = opts.search, // none, applied, removed + order = opts.order, // applied, current, index (original - compatibility with 1.9) + page = opts.page; // all, current + + if ( _fnDataSource( settings ) == 'ssp' ) { + // In server-side processing mode, most options are irrelevant since + // rows not shown don't exist and the index order is the applied order + // Removed is a special case - for consistency just return an empty + // array + return search === 'removed' ? + [] : + _range( 0, displayMaster.length ); + } + else if ( page == 'current' ) { + // Current page implies that order=current and fitler=applied, since it is + // fairly senseless otherwise, regardless of what order and search actually + // are + for ( i=settings._iDisplayStart, ien=settings.fnDisplayEnd() ; i<ien ; i++ ) { + a.push( displayFiltered[i] ); + } + } + else if ( order == 'current' || order == 'applied' ) { + a = search == 'none' ? + displayMaster.slice() : // no search + search == 'applied' ? + displayFiltered.slice() : // applied search + $.map( displayMaster, function (el, i) { // removed search + return $.inArray( el, displayFiltered ) === -1 ? el : null; + } ); + } + else if ( order == 'index' || order == 'original' ) { + for ( i=0, ien=settings.aoData.length ; i<ien ; i++ ) { + if ( search == 'none' ) { + a.push( i ); + } + else { // applied | removed + tmp = $.inArray( i, displayFiltered ); + + if ((tmp === -1 && search == 'removed') || + (tmp === 1 && search == 'applied') ) + { + a.push( i ); + } + } + } + } + + return a; + }; + + + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Rows + * + * {} - no selector - use all available rows + * {integer} - row aoData index + * {node} - TR node + * {string} - jQuery selector to apply to the TR elements + * {array} - jQuery array of nodes, or simply an array of TR nodes + * + */ + + + var __row_selector = function ( settings, selector, opts ) + { + return _selector_run( selector, function ( sel ) { + var selInt = _intVal( sel ); + + // Short cut - selector is a number and no options provided (default is + // all records, so no need to check if the index is in there, since it + // must be - dev error if the index doesn't exist). + if ( selInt !== null && ! opts ) { + return [ selInt ]; + } + + var rows = _selector_row_indexes( settings, opts ); + + if ( selInt !== null && $.inArray( selInt, rows ) !== -1 ) { + // Selector - integer + return [ selInt ]; + } + else if ( ! sel ) { + // Selector - none + return rows; + } + + // Get nodes in the order from the `rows` array (can't use `pluck`) @todo - use pluck_order + var nodes = []; + for ( var i=0, ien=rows.length ; i<ien ; i++ ) { + nodes.push( settings.aoData[ rows[i] ].nTr ); + } + + if ( sel.nodeName ) { + // Selector - node + if ( $.inArray( sel, nodes ) !== -1 ) { + return [ sel._DT_RowIndex ];// sel is a TR node that is in the table + // and DataTables adds a prop for fast lookup + } + } + + // Selector - jQuery selector string, array of nodes or jQuery object/ + // As jQuery's .filter() allows jQuery objects to be passed in filter, + // it also allows arrays, so this will cope with all three options + return $(nodes) + .filter( sel ) + .map( function () { + return this._DT_RowIndex; + } ) + .toArray(); + } ); + }; + + + /** + * + */ + _api_register( 'rows()', function ( selector, opts ) { + // argument shifting + if ( selector === undefined ) { + selector = ''; + } + else if ( $.isPlainObject( selector ) ) { + opts = selector; + selector = ''; + } + + opts = _selector_opts( opts ); + + var inst = this.iterator( 'table', function ( settings ) { + return __row_selector( settings, selector, opts ); + } ); + + // Want argument shifting here and in __row_selector? + inst.selector.rows = selector; + inst.selector.opts = opts; + + return inst; + } ); + + + _api_register( 'rows().nodes()', function () { + return this.iterator( 'row', function ( settings, row ) { + return settings.aoData[ row ].nTr || undefined; + } ); + } ); + + _api_register( 'rows().data()', function () { + return this.iterator( true, 'rows', function ( settings, rows ) { + return _pluck_order( settings.aoData, rows, '_aData' ); + } ); + } ); + + _api_registerPlural( 'rows().cache()', 'row().cache()', function ( type ) { + return this.iterator( 'row', function ( settings, row ) { + var r = settings.aoData[ row ]; + return type === 'search' ? r._aFilterData : r._aSortData; + } ); + } ); + + _api_registerPlural( 'rows().invalidate()', 'row().invalidate()', function ( src ) { + return this.iterator( 'row', function ( settings, row ) { + _fnInvalidateRow( settings, row, src ); + } ); + } ); + + _api_registerPlural( 'rows().indexes()', 'row().index()', function () { + return this.iterator( 'row', function ( settings, row ) { + return row; + } ); + } ); + + _api_registerPlural( 'rows().remove()', 'row().remove()', function () { + var that = this; + + return this.iterator( 'row', function ( settings, row, thatIdx ) { + var data = settings.aoData; + + data.splice( row, 1 ); + + // Update the _DT_RowIndex parameter on all rows in the table + for ( var i=0, ien=data.length ; i<ien ; i++ ) { + if ( data[i].nTr !== null ) { + data[i].nTr._DT_RowIndex = i; + } + } + + // Remove the target row from the search array + var displayIndex = $.inArray( row, settings.aiDisplay ); + + // Delete from the display arrays + _fnDeleteIndex( settings.aiDisplayMaster, row ); + _fnDeleteIndex( settings.aiDisplay, row ); + _fnDeleteIndex( that[ thatIdx ], row, false ); // maintain local indexes + + // Check for an 'overflow' they case for displaying the table + _fnLengthOverflow( settings ); + } ); + } ); + + + _api_register( 'rows.add()', function ( rows ) { + var newRows = this.iterator( 'table', function ( settings ) { + var row, i, ien; + var out = []; + + for ( i=0, ien=rows.length ; i<ien ; i++ ) { + row = rows[i]; + + if ( row.nodeName && row.nodeName.toUpperCase() === 'TR' ) { + out.push( _fnAddTr( settings, row )[0] ); + } + else { + out.push( _fnAddData( settings, row ) ); + } + } + + return out; + } ); + + // Return an Api.rows() extended instance, so rows().nodes() etc can be used + var modRows = this.rows( -1 ); + modRows.pop(); + modRows.push.apply( modRows, newRows.toArray() ); + + return modRows; + } ); + + + + + + /** + * + */ + _api_register( 'row()', function ( selector, opts ) { + return _selector_first( this.rows( selector, opts ) ); + } ); + + + _api_register( 'row().data()', function ( data ) { + var ctx = this.context; + + if ( data === undefined ) { + // Get + return ctx.length && this.length ? + ctx[0].aoData[ this[0] ]._aData : + undefined; + } + + // Set + ctx[0].aoData[ this[0] ]._aData = data; + + // Automatically invalidate + _fnInvalidateRow( ctx[0], this[0], 'data' ); + + return this; + } ); + + + _api_register( 'row().node()', function () { + var ctx = this.context; + + return ctx.length && this.length ? + ctx[0].aoData[ this[0] ].nTr || null : + null; + } ); + + + _api_register( 'row.add()', function ( row ) { + // Allow a jQuery object to be passed in - only a single row is added from + // it though - the first element in the set + if ( row instanceof $ && row.length ) { + row = row[0]; + } + + var rows = this.iterator( 'table', function ( settings ) { + if ( row.nodeName && row.nodeName.toUpperCase() === 'TR' ) { + return _fnAddTr( settings, row )[0]; + } + return _fnAddData( settings, row ); + } ); + + // Return an Api.rows() extended instance, with the newly added row selected + return this.row( rows[0] ); + } ); + + + + var __details_add = function ( ctx, row, data, klass ) + { + // Convert to array of TR elements + var rows = []; + var addRow = function ( r, k ) { + // If we get a TR element, then just add it directly - up to the dev + // to add the correct number of columns etc + if ( r.nodeName && r.nodeName.toLowerCase() === 'tr' ) { + rows.push( r ); + } + else { + // Otherwise create a row with a wrapper + var created = $('<tr><td/></tr>'); + $('td', created) + .addClass( k ) + .html( r ) + [0].colSpan = _fnVisbleColumns( ctx ); + + rows.push( created[0] ); + } + }; + + if ( $.isArray( data ) || data instanceof $ ) { + for ( var i=0, ien=data.length ; i<ien ; i++ ) { + addRow( data[i], klass ); + } + } + else { + addRow( data, klass ); + } + + if ( row._details ) { + row._details.remove(); + } + + row._details = $(rows); + + // If the children were already shown, that state should be retained + if ( row._detailsShow ) { + row._details.insertAfter( row.nTr ); + } + }; + + + var __details_display = function ( show ) { + var ctx = this.context; + + if ( ctx.length && this.length ) { + var row = ctx[0].aoData[ this[0] ]; + + if ( row._details ) { + row._detailsShow = show; + if ( show ) { + row._details.insertAfter( row.nTr ); + } + else { + row._details.remove(); + } + + __details_events( ctx[0] ); + } + } + + return this; + }; + + + var __details_events = function ( settings ) + { + var api = new _Api( settings ); + var namespace = '.dt.DT_details'; + var drawEvent = 'draw'+namespace; + var colvisEvent = 'column-visibility'+namespace; + + api.off( drawEvent +' '+ colvisEvent ); + + if ( _pluck( settings.aoData, '_details' ).length > 0 ) { + // On each draw, insert the required elements into the document + api.on( drawEvent, function () { + api.rows( {page:'current'} ).eq(0).each( function (idx) { + // Internal data grab + var row = settings.aoData[ idx ]; + + if ( row._detailsShow ) { + row._details.insertAfter( row.nTr ); + } + } ); + } ); + + // Column visibility change - update the colspan + api.on( colvisEvent, function ( e, settings, idx, vis ) { + // Update the colspan for the details rows (note, only if it already has + // a colspan) + var row, visible = _fnVisbleColumns( settings ); + + for ( var i=0, ien=settings.aoData.length ; i<ien ; i++ ) { + row = settings.aoData[i]; + + if ( row._details ) { + row._details.children('td[colspan]').attr('colspan', visible ); + } + } + } ); + } + }; + + // data can be: + // tr + // string + // jQuery or array of any of the above + _api_register( 'row().child()', function ( data, klass ) { + var ctx = this.context; + + if ( data === undefined ) { + // get + return ctx.length && this.length ? + ctx[0].aoData[ this[0] ]._details : + undefined; + } + else if ( ctx.length && this.length ) { + // set + __details_add( ctx[0], ctx[0].aoData[ this[0] ], data, klass ); + } + + return this; + } ); + + _api_register( [ + 'row().child.show()', + 'row().child().show()' + ], function () { + __details_display.call( this, true ); + return this; + } ); + + _api_register( [ + 'row().child.hide()', + 'row().child().hide()' + ], function () { + __details_display.call( this, false ); + return this; + } ); + + _api_register( 'row().child.isShown()', function () { + var ctx = this.context; + + if ( ctx.length && this.length ) { + // _detailsShown as false or undefined will fall through to return false + return ctx[0].aoData[ this[0] ]._detailsShow || false; + } + return false; + } ); + + + + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Columns + * + * {integer} - column index (>=0 count from left, <0 count from right) + * "{integer}:visIdx" - visible column index (i.e. translate to column index) (>=0 count from left, <0 count from right) + * "{integer}:visible" - alias for {integer}:visIdx (>=0 count from left, <0 count from right) + * "{string}:name" - column name + * "{string}" - jQuery selector on column header nodes + * + */ + + // can be an array of these items, comma separated list, or an array of comma + // separated lists + + var __re_column_selector = /^(.*):(name|visIdx|visible)$/; + + var __column_selector = function ( settings, selector, opts ) + { + var + columns = settings.aoColumns, + names = _pluck( columns, 'sName' ), + nodes = _pluck( columns, 'nTh' ); + + return _selector_run( selector, function ( s ) { + var selInt = _intVal( s ); + + if ( s === '' ) { + // All columns + return _range( columns.length ); + } + else if ( selInt !== null ) { + // Integer selector + return [ selInt >= 0 ? + selInt : // Count from left + columns.length + selInt // Count from right (+ because its a negative value) + ]; + } + else { + var match = typeof s === 'string' ? + s.match( __re_column_selector ) : + ''; + + if ( match ) { + switch( match[2] ) { + case 'visIdx': + case 'visible': + var idx = parseInt( match[1], 10 ); + // Visible index given, convert to column index + if ( idx < 0 ) { + // Counting from the right + var visColumns = $.map( columns, function (col,i) { + return col.bVisible ? i : null; + } ); + return [ visColumns[ visColumns.length + idx ] ]; + } + // Counting from the left + return [ _fnVisibleToColumnIndex( settings, idx ) ]; + + case 'name': + // match by name. `names` is column index complete and in order + return $.map( names, function (name, i) { + return name === match[1] ? i : null; + } ); + } + } + else { + // jQuery selector on the TH elements for the columns + return $( nodes ) + .filter( s ) + .map( function () { + return $.inArray( this, nodes ); // `nodes` is column index complete and in order + } ) + .toArray(); + } + } + } ); + }; + + + + + + var __setColumnVis = function ( settings, column, vis ) { + var + cols = settings.aoColumns, + col = cols[ column ], + data = settings.aoData, + row, cells, i, ien, tr; + + // Get + if ( vis === undefined ) { + return col.bVisible; + } + + // Set + // No change + if ( col.bVisible === vis ) { + return; + } + + if ( vis ) { + // Insert column + // Need to decide if we should use appendChild or insertBefore + var insertBefore = $.inArray( true, _pluck(cols, 'bVisible'), column+1 ); + + for ( i=0, ien=data.length ; i<ien ; i++ ) { + tr = data[i].nTr; + cells = data[i].anCells; + + if ( tr ) { + // insertBefore can act like appendChild if 2nd arg is null + tr.insertBefore( cells[ column ], cells[ insertBefore ] || null ); + } + } + } + else { + // Remove column + $( _pluck( settings.aoData, 'anCells', column ) ).detach(); + + col.bVisible = false; + _fnDrawHead( settings, settings.aoHeader ); + _fnDrawHead( settings, settings.aoFooter ); + + _fnSaveState( settings ); + } + + // Common actions + col.bVisible = vis; + _fnDrawHead( settings, settings.aoHeader ); + _fnDrawHead( settings, settings.aoFooter ); + + // Automatically adjust column sizing + _fnAdjustColumnSizing( settings ); + + // Realign columns for scrolling + if ( settings.oScroll.sX || settings.oScroll.sY ) { + _fnScrollDraw( settings ); + } + + _fnCallbackFire( settings, null, 'column-visibility', [settings, column, vis] ); + + _fnSaveState( settings ); + }; + + + /** + * + */ + _api_register( 'columns()', function ( selector, opts ) { + // argument shifting + if ( selector === undefined ) { + selector = ''; + } + else if ( $.isPlainObject( selector ) ) { + opts = selector; + selector = ''; + } + + opts = _selector_opts( opts ); + + var inst = this.iterator( 'table', function ( settings ) { + return __column_selector( settings, selector, opts ); + } ); + + // Want argument shifting here and in _row_selector? + inst.selector.cols = selector; + inst.selector.opts = opts; + + return inst; + } ); + + + /** + * + */ + _api_registerPlural( 'columns().header()', 'column().header()', function ( selector, opts ) { + return this.iterator( 'column', function ( settings, column ) { + return settings.aoColumns[column].nTh; + } ); + } ); + + + /** + * + */ + _api_registerPlural( 'columns().footer()', 'column().footer()', function ( selector, opts ) { + return this.iterator( 'column', function ( settings, column ) { + return settings.aoColumns[column].nTf; + } ); + } ); + + + /** + * + */ + _api_registerPlural( 'columns().data()', 'column().data()', function () { + return this.iterator( 'column-rows', function ( settings, column, i, j, rows ) { + var a = []; + for ( var row=0, ien=rows.length ; row<ien ; row++ ) { + a.push( _fnGetCellData( settings, rows[row], column, '' ) ); + } + return a; + } ); + } ); + + + _api_registerPlural( 'columns().cache()', 'column().cache()', function ( type ) { + return this.iterator( 'column-rows', function ( settings, column, i, j, rows ) { + return _pluck_order( settings.aoData, rows, + type === 'search' ? '_aFilterData' : '_aSortData', column + ); + } ); + } ); + + + _api_registerPlural( 'columns().nodes()', 'column().nodes()', function () { + return this.iterator( 'column-rows', function ( settings, column, i, j, rows ) { + return _pluck_order( settings.aoData, rows, 'anCells', column ) ; + } ); + } ); + + + + _api_registerPlural( 'columns().visible()', 'column().visible()', function ( vis ) { + return this.iterator( 'column', function ( settings, column ) { + return vis === undefined ? + settings.aoColumns[ column ].bVisible : + __setColumnVis( settings, column, vis ); + } ); + } ); + + + + _api_registerPlural( 'columns().indexes()', 'column().index()', function ( type ) { + return this.iterator( 'column', function ( settings, column ) { + return type === 'visible' ? + _fnColumnIndexToVisible( settings, column ) : + column; + } ); + } ); + + + // _api_register( 'columns().show()', function () { + // var selector = this.selector; + // return this.columns( selector.cols, selector.opts ).visible( true ); + // } ); + + + // _api_register( 'columns().hide()', function () { + // var selector = this.selector; + // return this.columns( selector.cols, selector.opts ).visible( false ); + // } ); + + + + _api_register( 'columns.adjust()', function () { + return this.iterator( 'table', function ( settings ) { + _fnAdjustColumnSizing( settings ); + } ); + } ); + + + // Convert from one column index type, to another type + _api_register( 'column.index()', function ( type, idx ) { + if ( this.context.length !== 0 ) { + var ctx = this.context[0]; + + if ( type === 'fromVisible' || type === 'toData' ) { + return _fnVisibleToColumnIndex( ctx, idx ); + } + else if ( type === 'fromData' || type === 'toVisible' ) { + return _fnColumnIndexToVisible( ctx, idx ); + } + } + } ); + + + _api_register( 'column()', function ( selector, opts ) { + return _selector_first( this.columns( selector, opts ) ); + } ); + + + + + var __cell_selector = function ( settings, selector, opts ) + { + var data = settings.aoData; + var rows = _selector_row_indexes( settings, opts ); + var cells = _pluck_order( data, rows, 'anCells' ); + var allCells = $( [].concat.apply([], cells) ); + var row; + var columns = settings.aoColumns.length; + var a, i, ien, j; + + return _selector_run( selector, function ( s ) { + if ( ! s ) { + // All cells + a = []; + + for ( i=0, ien=rows.length ; i<ien ; i++ ) { + row = rows[i]; + + for ( j=0 ; j<columns ; j++ ) { + a.push( { + row: row, + column: j + } ); + } + } + + return a; + } + else if ( $.isPlainObject( s ) ) { + return [s]; + } + + // jQuery filtered cells + return allCells + .filter( s ) + .map( function (i, el) { + row = el.parentNode._DT_RowIndex; + + return { + row: row, + column: $.inArray( el, data[ row ].anCells ) + }; + } ) + .toArray(); + } ); + }; + + + + + _api_register( 'cells()', function ( rowSelector, columnSelector, opts ) { + // Argument shifting + if ( $.isPlainObject( rowSelector ) ) { + // If passing in a cell index + if ( rowSelector.row ) { + opts = columnSelector; + columnSelector = null; + } + else { + opts = rowSelector; + rowSelector = null; + } + } + if ( $.isPlainObject( columnSelector ) ) { + opts = columnSelector; + columnSelector = null; + } + + // Cell selector + if ( columnSelector === null || columnSelector === undefined ) { + return this.iterator( 'table', function ( settings ) { + return __cell_selector( settings, rowSelector, _selector_opts( opts ) ); + } ); + } + + // Row + column selector + var columns = this.columns( columnSelector, opts ); + var rows = this.rows( rowSelector, opts ); + var a, i, ien, j, jen; + + var cells = this.iterator( 'table', function ( settings, idx ) { + a = []; + + for ( i=0, ien=rows[idx].length ; i<ien ; i++ ) { + for ( j=0, jen=columns[idx].length ; j<jen ; j++ ) { + a.push( { + row: rows[idx][i], + column: columns[idx][j] + } ); + } + } + + return a; + } ); + + $.extend( cells.selector, { + cols: columnSelector, + rows: rowSelector, + opts: opts + } ); + + return cells; + } ); + + + _api_registerPlural( 'cells().nodes()', 'cell().node()', function () { + return this.iterator( 'cell', function ( settings, row, column ) { + return settings.aoData[ row ].anCells[ column ]; + } ); + } ); + + + _api_register( 'cells().data()', function () { + return this.iterator( 'cell', function ( settings, row, column ) { + return _fnGetCellData( settings, row, column ); + } ); + } ); + + + _api_registerPlural( 'cells().cache()', 'cell().cache()', function ( type ) { + type = type === 'search' ? '_aFilterData' : '_aSortData'; + + return this.iterator( 'cell', function ( settings, row, column ) { + return settings.aoData[ row ][ type ][ column ]; + } ); + } ); + + + _api_registerPlural( 'cells().indexes()', 'cell().index()', function () { + return this.iterator( 'cell', function ( settings, row, column ) { + return { + row: row, + column: column, + columnVisible: _fnColumnIndexToVisible( settings, column ) + }; + } ); + } ); + + + _api_register( [ + 'cells().invalidate()', + 'cell().invalidate()' + ], function ( src ) { + var selector = this.selector; + + // Use the rows method of the instance to perform the invalidation, rather + // than doing it here. This avoids needing to handle duplicate rows from + // the cells. + this.rows( selector.rows, selector.opts ).invalidate( src ); + + return this; + } ); + + + + + _api_register( 'cell()', function ( rowSelector, columnSelector, opts ) { + return _selector_first( this.cells( rowSelector, columnSelector, opts ) ); + } ); + + + + _api_register( 'cell().data()', function ( data ) { + var ctx = this.context; + var cell = this[0]; + + if ( data === undefined ) { + // Get + return ctx.length && cell.length ? + _fnGetCellData( ctx[0], cell[0].row, cell[0].column ) : + undefined; + } + + // Set + _fnSetCellData( ctx[0], cell[0].row, cell[0].column, data ); + _fnInvalidateRow( ctx[0], cell[0].row, 'data', cell[0].column ); + + return this; + } ); + + + + /** + * Get current ordering (sorting) that has been applied to the table. + * + * @returns {array} 2D array containing the sorting information for the first + * table in the current context. Each element in the parent array represents + * a column being sorted upon (i.e. multi-sorting with two columns would have + * 2 inner arrays). The inner arrays may have 2 or 3 elements. The first is + * the column index that the sorting condition applies to, the second is the + * direction of the sort (`desc` or `asc`) and, optionally, the third is the + * index of the sorting order from the `column.sorting` initialisation array. + *//** + * Set the ordering for the table. + * + * @param {integer} order Column index to sort upon. + * @param {string} direction Direction of the sort to be applied (`asc` or `desc`) + * @returns {DataTables.Api} this + *//** + * Set the ordering for the table. + * + * @param {array} order 1D array of sorting information to be applied. + * @param {array} [...] Optional additional sorting conditions + * @returns {DataTables.Api} this + *//** + * Set the ordering for the table. + * + * @param {array} order 2D array of sorting information to be applied. + * @returns {DataTables.Api} this + */ + _api_register( 'order()', function ( order, dir ) { + var ctx = this.context; + + if ( order === undefined ) { + // get + return ctx.length !== 0 ? + ctx[0].aaSorting : + undefined; + } + + // set + if ( typeof order === 'number' ) { + // Simple column / direction passed in + order = [ [ order, dir ] ]; + } + else if ( ! $.isArray( order[0] ) ) { + // Arguments passed in (list of 1D arrays) + order = Array.prototype.slice.call( arguments ); + } + // otherwise a 2D array was passed in + + return this.iterator( 'table', function ( settings ) { + settings.aaSorting = order.slice(); + } ); + } ); + + + /** + * Attach a sort listener to an element for a given column + * + * @param {node|jQuery|string} node Identifier for the element(s) to attach the + * listener to. This can take the form of a single DOM node, a jQuery + * collection of nodes or a jQuery selector which will identify the node(s). + * @param {integer} column the column that a click on this node will sort on + * @param {function} [callback] callback function when sort is run + * @returns {DataTables.Api} this + */ + _api_register( 'order.listener()', function ( node, column, callback ) { + return this.iterator( 'table', function ( settings ) { + _fnSortAttachListener( settings, node, column, callback ); + } ); + } ); + + + // Order by the selected column(s) + _api_register( [ + 'columns().order()', + 'column().order()' + ], function ( dir ) { + var that = this; + + return this.iterator( 'table', function ( settings, i ) { + var sort = []; + + $.each( that[i], function (j, col) { + sort.push( [ col, dir ] ); + } ); + + settings.aaSorting = sort; + } ); + } ); + + + + _api_register( 'search()', function ( input, regex, smart, caseInsen ) { + var ctx = this.context; + + if ( input === undefined ) { + // get + return ctx.length !== 0 ? + ctx[0].oPreviousSearch.sSearch : + undefined; + } + + // set + return this.iterator( 'table', function ( settings ) { + if ( ! settings.oFeatures.bFilter ) { + return; + } + + _fnFilterComplete( settings, $.extend( {}, settings.oPreviousSearch, { + "sSearch": input+"", + "bRegex": regex === null ? false : regex, + "bSmart": smart === null ? true : smart, + "bCaseInsensitive": caseInsen === null ? true : caseInsen + } ), 1 ); + } ); + } ); + + + _api_register( [ + 'columns().search()', + 'column().search()' + ], function ( input, regex, smart, caseInsen ) { + return this.iterator( 'column', function ( settings, column ) { + var preSearch = settings.aoPreSearchCols; + + if ( input === undefined ) { + // get + return preSearch[ column ].sSearch; + } + + // set + if ( ! settings.oFeatures.bFilter ) { + return; + } + + $.extend( preSearch[ column ], { + "sSearch": input+"", + "bRegex": regex === null ? false : regex, + "bSmart": smart === null ? true : smart, + "bCaseInsensitive": caseInsen === null ? true : caseInsen + } ); + + _fnFilterComplete( settings, settings.oPreviousSearch, 1 ); + } ); + } ); + + + + /** + * Provide a common method for plug-ins to check the version of DataTables being + * used, in order to ensure compatibility. + * + * @param {string} version Version string to check for, in the format "X.Y.Z". + * Note that the formats "X" and "X.Y" are also acceptable. + * @returns {boolean} true if this version of DataTables is greater or equal to + * the required version, or false if this version of DataTales is not + * suitable + * @static + * @dtopt API-Static + * + * @example + * alert( $.fn.dataTable.versionCheck( '1.9.0' ) ); + */ + DataTable.versionCheck = DataTable.fnVersionCheck = function( version ) + { + var aThis = DataTable.version.split('.'); + var aThat = version.split('.'); + var iThis, iThat; + + for ( var i=0, iLen=aThat.length ; i<iLen ; i++ ) { + iThis = parseInt( aThis[i], 10 ) || 0; + iThat = parseInt( aThat[i], 10 ) || 0; + + // Parts are the same, keep comparing + if (iThis === iThat) { + continue; + } + + // Parts are different, return immediately + return iThis > iThat; + } + + return true; + }; + + + /** + * Check if a `<table>` node is a DataTable table already or not. + * + * @param {node|jquery|string} table Table node, jQuery object or jQuery + * selector for the table to test. Note that if more than more than one + * table is passed on, only the first will be checked + * @returns {boolean} true the table given is a DataTable, or false otherwise + * @static + * @dtopt API-Static + * + * @example + * if ( ! $.fn.DataTable.isDataTable( '#example' ) ) { + * $('#example').dataTable(); + * } + */ + DataTable.isDataTable = DataTable.fnIsDataTable = function ( table ) + { + var t = $(table).get(0); + var is = false; + + $.each( DataTable.settings, function (i, o) { + if ( o.nTable === t || o.nScrollHead === t || o.nScrollFoot === t ) { + is = true; + } + } ); + + return is; + }; + + + /** + * Get all DataTable tables that have been initialised - optionally you can + * select to get only currently visible tables. + * + * @param {boolean} [visible=false] Flag to indicate if you want all (default) + * or visible tables only. + * @returns {array} Array of `table` nodes (not DataTable instances) which are + * DataTables + * @static + * @dtopt API-Static + * + * @example + * $.each( $.fn.dataTable.tables(true), function () { + * $(table).DataTable().columns.adjust(); + * } ); + */ + DataTable.tables = DataTable.fnTables = function ( visible ) + { + return jQuery.map( DataTable.settings, function (o) { + if ( !visible || (visible && $(o.nTable).is(':visible')) ) { + return o.nTable; + } + } ); + }; + + + /** + * Convert from camel case parameters to Hungarian notation. This is made public + * for the extensions to provide the same ability as DataTables core to accept + * either the 1.9 style Hungarian notation, or the 1.10+ style camelCase + * parameters. + * + * @param {object} src The model object which holds all parameters that can be + * mapped. + * @param {object} user The object to convert from camel case to Hungarian. + * @param {boolean} force When set to `true`, properties which already have a + * Hungarian value in the `user` object will be overwritten. Otherwise they + * won't be. + */ + DataTable.camelToHungarian = _fnCamelToHungarian; + + + + /** + * + */ + _api_register( '$()', function ( selector, opts ) { + var + rows = this.rows( opts ).nodes(), // Get all rows + jqRows = $(rows); + + return $( [].concat( + jqRows.filter( selector ).toArray(), + jqRows.find( selector ).toArray() + ) ); + } ); + + + // jQuery functions to operate on the tables + $.each( [ 'on', 'one', 'off' ], function (i, key) { + _api_register( key+'()', function ( /* event, handler */ ) { + var args = Array.prototype.slice.call(arguments); + + // Add the `dt` namespace automatically if it isn't already present + if ( args[0].indexOf( '.dt' ) === -1 ) { + args[0] += '.dt'; + } + + var inst = $( this.tables().nodes() ); + inst[key].apply( inst, args ); + return this; + } ); + } ); + + + _api_register( 'clear()', function () { + return this.iterator( 'table', function ( settings ) { + _fnClearTable( settings ); + } ); + } ); + + + _api_register( 'settings()', function () { + return new _Api( this.context, this.context ); + } ); + + + _api_register( 'data()', function () { + return this.iterator( 'table', function ( settings ) { + return _pluck( settings.aoData, '_aData' ); + } ).flatten(); + } ); + + + _api_register( 'destroy()', function ( remove ) { + remove = remove || false; + + return this.iterator( 'table', function ( settings ) { + var orig = settings.nTableWrapper.parentNode; + var classes = settings.oClasses; + var table = settings.nTable; + var tbody = settings.nTBody; + var thead = settings.nTHead; + var tfoot = settings.nTFoot; + var jqTable = $(table); + var jqTbody = $(tbody); + var jqWrapper = $(settings.nTableWrapper); + var rows = $.map( settings.aoData, function (r) { return r.nTr; } ); + var i, ien; + + // Flag to note that the table is currently being destroyed - no action + // should be taken + settings.bDestroying = true; + + // Fire off the destroy callbacks for plug-ins etc + _fnCallbackFire( settings, "aoDestroyCallback", "destroy", [settings] ); + + // If not being removed from the document, make all columns visible + if ( ! remove ) { + new _Api( settings ).columns().visible( true ); + } + + // Blitz all `DT` namespaced events (these are internal events, the + // lowercase, `dt` events are user subscribed and they are responsible + // for removing them + jqWrapper.unbind('.DT').find(':not(tbody *)').unbind('.DT'); + $(window).unbind('.DT-'+settings.sInstance); + + // When scrolling we had to break the table up - restore it + if ( table != thead.parentNode ) { + jqTable.children('thead').detach(); + jqTable.append( thead ); + } + + if ( tfoot && table != tfoot.parentNode ) { + jqTable.children('tfoot').detach(); + jqTable.append( tfoot ); + } + + // Remove the DataTables generated nodes, events and classes + jqTable.detach(); + jqWrapper.detach(); + + settings.aaSorting = []; + settings.aaSortingFixed = []; + _fnSortingClasses( settings ); + + $( rows ).removeClass( settings.asStripeClasses.join(' ') ); + + $('th, td', thead).removeClass( classes.sSortable+' '+ + classes.sSortableAsc+' '+classes.sSortableDesc+' '+classes.sSortableNone + ); + + if ( settings.bJUI ) { + $('th span.'+classes.sSortIcon+ ', td span.'+classes.sSortIcon, thead).detach(); + $('th, td', thead).each( function () { + var wrapper = $('div.'+classes.sSortJUIWrapper, this); + $(this).append( wrapper.contents() ); + wrapper.detach(); + } ); + } + + if ( ! remove && orig ) { + // insertBefore acts like appendChild if !arg[1] + orig.insertBefore( table, settings.nTableReinsertBefore ); + } + + // Add the TR elements back into the table in their original order + jqTbody.children().detach(); + jqTbody.append( rows ); + + // Restore the width of the original table - was read from the style property, + // so we can restore directly to that + jqTable + .css( 'width', settings.sDestroyWidth ) + .removeClass( classes.sTable ); + + // If the were originally stripe classes - then we add them back here. + // Note this is not fool proof (for example if not all rows had stripe + // classes - but it's a good effort without getting carried away + ien = settings.asDestroyStripes.length; + + if ( ien ) { + jqTbody.children().each( function (i) { + $(this).addClass( settings.asDestroyStripes[i % ien] ); + } ); + } + + /* Remove the settings object from the settings array */ + var idx = $.inArray( settings, DataTable.settings ); + if ( idx !== -1 ) { + DataTable.settings.splice( idx, 1 ); + } + } ); + } ); + + + /** + * Version string for plug-ins to check compatibility. Allowed format is + * `a.b.c-d` where: a:int, b:int, c:int, d:string(dev|beta|alpha). `d` is used + * only for non-release builds. See http://semver.org/ for more information. + * @member + * @type string + * @default Version number + */ + DataTable.version = "1.10.0"; + + /** + * Private data store, containing all of the settings objects that are + * created for the tables on a given page. + * + * Note that the `DataTable.settings` object is aliased to + * `jQuery.fn.dataTableExt` through which it may be accessed and + * manipulated, or `jQuery.fn.dataTable.settings`. + * @member + * @type array + * @default [] + * @private + */ + DataTable.settings = []; + + /** + * Object models container, for the various models that DataTables has + * available to it. These models define the objects that are used to hold + * the active state and configuration of the table. + * @namespace + */ + DataTable.models = {}; + + + + /** + * Template object for the way in which DataTables holds information about + * search information for the global filter and individual column filters. + * @namespace + */ + DataTable.models.oSearch = { + /** + * Flag to indicate if the filtering should be case insensitive or not + * @type boolean + * @default true + */ + "bCaseInsensitive": true, + + /** + * Applied search term + * @type string + * @default <i>Empty string</i> + */ + "sSearch": "", + + /** + * Flag to indicate if the search term should be interpreted as a + * regular expression (true) or not (false) and therefore and special + * regex characters escaped. + * @type boolean + * @default false + */ + "bRegex": false, + + /** + * Flag to indicate if DataTables is to use its smart filtering or not. + * @type boolean + * @default true + */ + "bSmart": true + }; + + + + + /** + * Template object for the way in which DataTables holds information about + * each individual row. This is the object format used for the settings + * aoData array. + * @namespace + */ + DataTable.models.oRow = { + /** + * TR element for the row + * @type node + * @default null + */ + "nTr": null, + + /** + * Array of TD elements for each row. This is null until the row has been + * created. + * @type array nodes + * @default [] + */ + "anCells": null, + + /** + * Data object from the original data source for the row. This is either + * an array if using the traditional form of DataTables, or an object if + * using mData options. The exact type will depend on the passed in + * data from the data source, or will be an array if using DOM a data + * source. + * @type array|object + * @default [] + */ + "_aData": [], + + /** + * Sorting data cache - this array is ostensibly the same length as the + * number of columns (although each index is generated only as it is + * needed), and holds the data that is used for sorting each column in the + * row. We do this cache generation at the start of the sort in order that + * the formatting of the sort data need be done only once for each cell + * per sort. This array should not be read from or written to by anything + * other than the master sorting methods. + * @type array + * @default null + * @private + */ + "_aSortData": null, + + /** + * Per cell filtering data cache. As per the sort data cache, used to + * increase the performance of the filtering in DataTables + * @type array + * @default null + * @private + */ + "_aFilterData": null, + + /** + * Filtering data cache. This is the same as the cell filtering cache, but + * in this case a string rather than an array. This is easily computed with + * a join on `_aFilterData`, but is provided as a cache so the join isn't + * needed on every search (memory traded for performance) + * @type array + * @default null + * @private + */ + "_sFilterRow": null, + + /** + * Cache of the class name that DataTables has applied to the row, so we + * can quickly look at this variable rather than needing to do a DOM check + * on className for the nTr property. + * @type string + * @default <i>Empty string</i> + * @private + */ + "_sRowStripe": "", + + /** + * Denote if the original data source was from the DOM, or the data source + * object. This is used for invalidating data, so DataTables can + * automatically read data from the original source, unless uninstructed + * otherwise. + * @type string + * @default null + * @private + */ + "src": null + }; + + + /** + * Template object for the column information object in DataTables. This object + * is held in the settings aoColumns array and contains all the information that + * DataTables needs about each individual column. + * + * Note that this object is related to {@link DataTable.defaults.column} + * but this one is the internal data store for DataTables's cache of columns. + * It should NOT be manipulated outside of DataTables. Any configuration should + * be done through the initialisation options. + * @namespace + */ + DataTable.models.oColumn = { + /** + * Column index. This could be worked out on-the-fly with $.inArray, but it + * is faster to just hold it as a variable + * @type integer + * @default null + */ + "idx": null, + + /** + * A list of the columns that sorting should occur on when this column + * is sorted. That this property is an array allows multi-column sorting + * to be defined for a column (for example first name / last name columns + * would benefit from this). The values are integers pointing to the + * columns to be sorted on (typically it will be a single integer pointing + * at itself, but that doesn't need to be the case). + * @type array + */ + "aDataSort": null, + + /** + * Define the sorting directions that are applied to the column, in sequence + * as the column is repeatedly sorted upon - i.e. the first value is used + * as the sorting direction when the column if first sorted (clicked on). + * Sort it again (click again) and it will move on to the next index. + * Repeat until loop. + * @type array + */ + "asSorting": null, + + /** + * Flag to indicate if the column is searchable, and thus should be included + * in the filtering or not. + * @type boolean + */ + "bSearchable": null, + + /** + * Flag to indicate if the column is sortable or not. + * @type boolean + */ + "bSortable": null, + + /** + * Flag to indicate if the column is currently visible in the table or not + * @type boolean + */ + "bVisible": null, + + /** + * Store for manual type assignment using the `column.type` option. This + * is held in store so we can manipulate the column's `sType` property. + * @type string + * @default null + * @private + */ + "_sManualType": null, + + /** + * Flag to indicate if HTML5 data attributes should be used as the data + * source for filtering or sorting. True is either are. + * @type boolean + * @default false + * @private + */ + "_bAttrSrc": false, + + /** + * Developer definable function that is called whenever a cell is created (Ajax source, + * etc) or processed for input (DOM source). This can be used as a compliment to mRender + * allowing you to modify the DOM element (add background colour for example) when the + * element is available. + * @type function + * @param {element} nTd The TD node that has been created + * @param {*} sData The Data for the cell + * @param {array|object} oData The data for the whole row + * @param {int} iRow The row index for the aoData data store + * @default null + */ + "fnCreatedCell": null, + + /** + * Function to get data from a cell in a column. You should <b>never</b> + * access data directly through _aData internally in DataTables - always use + * the method attached to this property. It allows mData to function as + * required. This function is automatically assigned by the column + * initialisation method + * @type function + * @param {array|object} oData The data array/object for the array + * (i.e. aoData[]._aData) + * @param {string} sSpecific The specific data type you want to get - + * 'display', 'type' 'filter' 'sort' + * @returns {*} The data for the cell from the given row's data + * @default null + */ + "fnGetData": null, + + /** + * Function to set data for a cell in the column. You should <b>never</b> + * set the data directly to _aData internally in DataTables - always use + * this method. It allows mData to function as required. This function + * is automatically assigned by the column initialisation method + * @type function + * @param {array|object} oData The data array/object for the array + * (i.e. aoData[]._aData) + * @param {*} sValue Value to set + * @default null + */ + "fnSetData": null, + + /** + * Property to read the value for the cells in the column from the data + * source array / object. If null, then the default content is used, if a + * function is given then the return from the function is used. + * @type function|int|string|null + * @default null + */ + "mData": null, + + /** + * Partner property to mData which is used (only when defined) to get + * the data - i.e. it is basically the same as mData, but without the + * 'set' option, and also the data fed to it is the result from mData. + * This is the rendering method to match the data method of mData. + * @type function|int|string|null + * @default null + */ + "mRender": null, + + /** + * Unique header TH/TD element for this column - this is what the sorting + * listener is attached to (if sorting is enabled.) + * @type node + * @default null + */ + "nTh": null, + + /** + * Unique footer TH/TD element for this column (if there is one). Not used + * in DataTables as such, but can be used for plug-ins to reference the + * footer for each column. + * @type node + * @default null + */ + "nTf": null, + + /** + * The class to apply to all TD elements in the table's TBODY for the column + * @type string + * @default null + */ + "sClass": null, + + /** + * When DataTables calculates the column widths to assign to each column, + * it finds the longest string in each column and then constructs a + * temporary table and reads the widths from that. The problem with this + * is that "mmm" is much wider then "iiii", but the latter is a longer + * string - thus the calculation can go wrong (doing it properly and putting + * it into an DOM object and measuring that is horribly(!) slow). Thus as + * a "work around" we provide this option. It will append its value to the + * text that is found to be the longest string for the column - i.e. padding. + * @type string + */ + "sContentPadding": null, + + /** + * Allows a default value to be given for a column's data, and will be used + * whenever a null data source is encountered (this can be because mData + * is set to null, or because the data source itself is null). + * @type string + * @default null + */ + "sDefaultContent": null, + + /** + * Name for the column, allowing reference to the column by name as well as + * by index (needs a lookup to work by name). + * @type string + */ + "sName": null, + + /** + * Custom sorting data type - defines which of the available plug-ins in + * afnSortData the custom sorting will use - if any is defined. + * @type string + * @default std + */ + "sSortDataType": 'std', + + /** + * Class to be applied to the header element when sorting on this column + * @type string + * @default null + */ + "sSortingClass": null, + + /** + * Class to be applied to the header element when sorting on this column - + * when jQuery UI theming is used. + * @type string + * @default null + */ + "sSortingClassJUI": null, + + /** + * Title of the column - what is seen in the TH element (nTh). + * @type string + */ + "sTitle": null, + + /** + * Column sorting and filtering type + * @type string + * @default null + */ + "sType": null, + + /** + * Width of the column + * @type string + * @default null + */ + "sWidth": null, + + /** + * Width of the column when it was first "encountered" + * @type string + * @default null + */ + "sWidthOrig": null + }; + + + /* + * Developer note: The properties of the object below are given in Hungarian + * notation, that was used as the interface for DataTables prior to v1.10, however + * from v1.10 onwards the primary interface is camel case. In order to avoid + * breaking backwards compatibility utterly with this change, the Hungarian + * version is still, internally the primary interface, but is is not documented + * - hence the @name tags in each doc comment. This allows a Javascript function + * to create a map from Hungarian notation to camel case (going the other direction + * would require each property to be listed, which would at around 3K to the size + * of DataTables, while this method is about a 0.5K hit. + * + * Ultimately this does pave the way for Hungarian notation to be dropped + * completely, but that is a massive amount of work and will break current + * installs (therefore is on-hold until v2). + */ + + /** + * Initialisation options that can be given to DataTables at initialisation + * time. + * @namespace + */ + DataTable.defaults = { + /** + * An array of data to use for the table, passed in at initialisation which + * will be used in preference to any data which is already in the DOM. This is + * particularly useful for constructing tables purely in Javascript, for + * example with a custom Ajax call. + * @type array + * @default null + * + * @dtopt Option + * @name DataTable.defaults.data + * + * @example + * // Using a 2D array data source + * $(document).ready( function () { + * $('#example').dataTable( { + * "data": [ + * ['Trident', 'Internet Explorer 4.0', 'Win 95+', 4, 'X'], + * ['Trident', 'Internet Explorer 5.0', 'Win 95+', 5, 'C'], + * ], + * "columns": [ + * { "title": "Engine" }, + * { "title": "Browser" }, + * { "title": "Platform" }, + * { "title": "Version" }, + * { "title": "Grade" } + * ] + * } ); + * } ); + * + * @example + * // Using an array of objects as a data source (`data`) + * $(document).ready( function () { + * $('#example').dataTable( { + * "data": [ + * { + * "engine": "Trident", + * "browser": "Internet Explorer 4.0", + * "platform": "Win 95+", + * "version": 4, + * "grade": "X" + * }, + * { + * "engine": "Trident", + * "browser": "Internet Explorer 5.0", + * "platform": "Win 95+", + * "version": 5, + * "grade": "C" + * } + * ], + * "columns": [ + * { "title": "Engine", "data": "engine" }, + * { "title": "Browser", "data": "browser" }, + * { "title": "Platform", "data": "platform" }, + * { "title": "Version", "data": "version" }, + * { "title": "Grade", "data": "grade" } + * ] + * } ); + * } ); + */ + "aaData": null, + + + /** + * If ordering is enabled, then DataTables will perform a first pass sort on + * initialisation. You can define which column(s) the sort is performed + * upon, and the sorting direction, with this variable. The `sorting` array + * should contain an array for each column to be sorted initially containing + * the column's index and a direction string ('asc' or 'desc'). + * @type array + * @default [[0,'asc']] + * + * @dtopt Option + * @name DataTable.defaults.order + * + * @example + * // Sort by 3rd column first, and then 4th column + * $(document).ready( function() { + * $('#example').dataTable( { + * "order": [[2,'asc'], [3,'desc']] + * } ); + * } ); + * + * // No initial sorting + * $(document).ready( function() { + * $('#example').dataTable( { + * "order": [] + * } ); + * } ); + */ + "aaSorting": [[0,'asc']], + + + /** + * This parameter is basically identical to the `sorting` parameter, but + * cannot be overridden by user interaction with the table. What this means + * is that you could have a column (visible or hidden) which the sorting + * will always be forced on first - any sorting after that (from the user) + * will then be performed as required. This can be useful for grouping rows + * together. + * @type array + * @default null + * + * @dtopt Option + * @name DataTable.defaults.orderFixed + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "orderFixed": [[0,'asc']] + * } ); + * } ) + */ + "aaSortingFixed": [], + + + /** + * DataTables can be instructed to load data to display in the table from a + * Ajax source. This option defines how that Ajax call is made and where to. + * + * The `ajax` property has three different modes of operation, depending on + * how it is defined. These are: + * + * * `string` - Set the URL from where the data should be loaded from. + * * `object` - Define properties for `jQuery.ajax`. + * * `function` - Custom data get function + * + * `string` + * -------- + * + * As a string, the `ajax` property simply defines the URL from which + * DataTables will load data. + * + * `object` + * -------- + * + * As an object, the parameters in the object are passed to + * [jQuery.ajax](http://api.jquery.com/jQuery.ajax/) allowing fine control + * of the Ajax request. DataTables has a number of default parameters which + * you can override using this option. Please refer to the jQuery + * documentation for a full description of the options available, although + * the following parameters provide additional options in DataTables or + * require special consideration: + * + * * `data` - As with jQuery, `data` can be provided as an object, but it + * can also be used as a function to manipulate the data DataTables sends + * to the server. The function takes a single parameter, an object of + * parameters with the values that DataTables has readied for sending. An + * object may be returned which will be merged into the DataTables + * defaults, or you can add the items to the object that was passed in and + * not return anything from the function. This supersedes `fnServerParams` + * from DataTables 1.9-. + * + * * `dataSrc` - By default DataTables will look for the property `data` (or + * `aaData` for compatibility with DataTables 1.9-) when obtaining data + * from an Ajax source or for server-side processing - this parameter + * allows that property to be changed. You can use Javascript dotted + * object notation to get a data source for multiple levels of nesting, or + * it my be used as a function. As a function it takes a single parameter, + * the JSON returned from the server, which can be manipulated as + * required, with the returned value being that used by DataTables as the + * data source for the table. This supersedes `sAjaxDataProp` from + * DataTables 1.9-. + * + * * `success` - Should not be overridden it is used internally in + * DataTables. To manipulate / transform the data returned by the server + * use `ajax.dataSrc`, or use `ajax` as a function (see below). + * + * `function` + * ---------- + * + * As a function, making the Ajax call is left up to yourself allowing + * complete control of the Ajax request. Indeed, if desired, a method other + * than Ajax could be used to obtain the required data, such as Web storage + * or an AIR database. + * + * The function is given four parameters and no return is required. The + * parameters are: + * + * 1. _object_ - Data to send to the server + * 2. _function_ - Callback function that must be executed when the required + * data has been obtained. That data should be passed into the callback + * as the only parameter + * 3. _object_ - DataTables settings object for the table + * + * Note that this supersedes `fnServerData` from DataTables 1.9-. + * + * @type string|object|function + * @default null + * + * @dtopt Option + * @name DataTable.defaults.ajax + * @since 1.10.0 + * + * @example + * // Get JSON data from a file via Ajax. + * // Note DataTables expects data in the form `{ data: [ ...data... ] }` by default). + * $('#example').dataTable( { + * "ajax": "data.json" + * } ); + * + * @example + * // Get JSON data from a file via Ajax, using `dataSrc` to change + * // `data` to `tableData` (i.e. `{ tableData: [ ...data... ] }`) + * $('#example').dataTable( { + * "ajax": { + * "url": "data.json", + * "dataSrc": "tableData" + * } + * } ); + * + * @example + * // Get JSON data from a file via Ajax, using `dataSrc` to read data + * // from a plain array rather than an array in an object + * $('#example').dataTable( { + * "ajax": { + * "url": "data.json", + * "dataSrc": "" + * } + * } ); + * + * @example + * // Manipulate the data returned from the server - add a link to data + * // (note this can, should, be done using `render` for the column - this + * // is just a simple example of how the data can be manipulated). + * $('#example').dataTable( { + * "ajax": { + * "url": "data.json", + * "dataSrc": function ( json ) { + * for ( var i=0, ien=json.length ; i<ien ; i++ ) { + * json[i][0] = '<a href="/message/'+json[i][0]+'>View message</a>'; + * } + * return json; + * } + * } + * } ); + * + * @example + * // Add data to the request + * $('#example').dataTable( { + * "ajax": { + * "url": "data.json", + * "data": function ( d ) { + * return { + * "extra_search": $('#extra').val() + * }; + * } + * } + * } ); + * + * @example + * // Send request as POST + * $('#example').dataTable( { + * "ajax": { + * "url": "data.json", + * "type": "POST" + * } + * } ); + * + * @example + * // Get the data from localStorage (could interface with a form for + * // adding, editing and removing rows). + * $('#example').dataTable( { + * "ajax": function (data, callback, settings) { + * callback( + * JSON.parse( localStorage.getItem('dataTablesData') ) + * ); + * } + * } ); + */ + "ajax": null, + + + /** + * This parameter allows you to readily specify the entries in the length drop + * down menu that DataTables shows when pagination is enabled. It can be + * either a 1D array of options which will be used for both the displayed + * option and the value, or a 2D array which will use the array in the first + * position as the value, and the array in the second position as the + * displayed options (useful for language strings such as 'All'). + * + * Note that the `pageLength` property will be automatically set to the + * first value given in this array, unless `pageLength` is also provided. + * @type array + * @default [ 10, 25, 50, 100 ] + * + * @dtopt Option + * @name DataTable.defaults.lengthMenu + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "lengthMenu": [[10, 25, 50, -1], [10, 25, 50, "All"]] + * } ); + * } ); + */ + "aLengthMenu": [ 10, 25, 50, 100 ], + + + /** + * The `columns` option in the initialisation parameter allows you to define + * details about the way individual columns behave. For a full list of + * column options that can be set, please see + * {@link DataTable.defaults.column}. Note that if you use `columns` to + * define your columns, you must have an entry in the array for every single + * column that you have in your table (these can be null if you don't which + * to specify any options). + * @member + * + * @name DataTable.defaults.column + */ + "aoColumns": null, + + /** + * Very similar to `columns`, `columnDefs` allows you to target a specific + * column, multiple columns, or all columns, using the `targets` property of + * each object in the array. This allows great flexibility when creating + * tables, as the `columnDefs` arrays can be of any length, targeting the + * columns you specifically want. `columnDefs` may use any of the column + * options available: {@link DataTable.defaults.column}, but it _must_ + * have `targets` defined in each object in the array. Values in the `targets` + * array may be: + * <ul> + * <li>a string - class name will be matched on the TH for the column</li> + * <li>0 or a positive integer - column index counting from the left</li> + * <li>a negative integer - column index counting from the right</li> + * <li>the string "_all" - all columns (i.e. assign a default)</li> + * </ul> + * @member + * + * @name DataTable.defaults.columnDefs + */ + "aoColumnDefs": null, + + + /** + * Basically the same as `search`, this parameter defines the individual column + * filtering state at initialisation time. The array must be of the same size + * as the number of columns, and each element be an object with the parameters + * `search` and `escapeRegex` (the latter is optional). 'null' is also + * accepted and the default will be used. + * @type array + * @default [] + * + * @dtopt Option + * @name DataTable.defaults.searchCols + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "searchCols": [ + * null, + * { "search": "My filter" }, + * null, + * { "search": "^[0-9]", "escapeRegex": false } + * ] + * } ); + * } ) + */ + "aoSearchCols": [], + + + /** + * An array of CSS classes that should be applied to displayed rows. This + * array may be of any length, and DataTables will apply each class + * sequentially, looping when required. + * @type array + * @default null <i>Will take the values determined by the `oClasses.stripe*` + * options</i> + * + * @dtopt Option + * @name DataTable.defaults.stripeClasses + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "stripeClasses": [ 'strip1', 'strip2', 'strip3' ] + * } ); + * } ) + */ + "asStripeClasses": null, + + + /** + * Enable or disable automatic column width calculation. This can be disabled + * as an optimisation (it takes some time to calculate the widths) if the + * tables widths are passed in using `columns`. + * @type boolean + * @default true + * + * @dtopt Features + * @name DataTable.defaults.autoWidth + * + * @example + * $(document).ready( function () { + * $('#example').dataTable( { + * "autoWidth": false + * } ); + * } ); + */ + "bAutoWidth": true, + + + /** + * Deferred rendering can provide DataTables with a huge speed boost when you + * are using an Ajax or JS data source for the table. This option, when set to + * true, will cause DataTables to defer the creation of the table elements for + * each row until they are needed for a draw - saving a significant amount of + * time. + * @type boolean + * @default false + * + * @dtopt Features + * @name DataTable.defaults.deferRender + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "ajax": "sources/arrays.txt", + * "deferRender": true + * } ); + * } ); + */ + "bDeferRender": false, + + + /** + * Replace a DataTable which matches the given selector and replace it with + * one which has the properties of the new initialisation object passed. If no + * table matches the selector, then the new DataTable will be constructed as + * per normal. + * @type boolean + * @default false + * + * @dtopt Options + * @name DataTable.defaults.destroy + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "srollY": "200px", + * "paginate": false + * } ); + * + * // Some time later.... + * $('#example').dataTable( { + * "filter": false, + * "destroy": true + * } ); + * } ); + */ + "bDestroy": false, + + + /** + * Enable or disable filtering of data. Filtering in DataTables is "smart" in + * that it allows the end user to input multiple words (space separated) and + * will match a row containing those words, even if not in the order that was + * specified (this allow matching across multiple columns). Note that if you + * wish to use filtering in DataTables this must remain 'true' - to remove the + * default filtering input box and retain filtering abilities, please use + * {@link DataTable.defaults.dom}. + * @type boolean + * @default true + * + * @dtopt Features + * @name DataTable.defaults.searching + * + * @example + * $(document).ready( function () { + * $('#example').dataTable( { + * "searching": false + * } ); + * } ); + */ + "bFilter": true, + + + /** + * Enable or disable the table information display. This shows information + * about the data that is currently visible on the page, including information + * about filtered data if that action is being performed. + * @type boolean + * @default true + * + * @dtopt Features + * @name DataTable.defaults.info + * + * @example + * $(document).ready( function () { + * $('#example').dataTable( { + * "info": false + * } ); + * } ); + */ + "bInfo": true, + + + /** + * Enable jQuery UI ThemeRoller support (required as ThemeRoller requires some + * slightly different and additional mark-up from what DataTables has + * traditionally used). + * @type boolean + * @default false + * + * @dtopt Features + * @name DataTable.defaults.jQueryUI + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "jQueryUI": true + * } ); + * } ); + */ + "bJQueryUI": false, + + + /** + * Allows the end user to select the size of a formatted page from a select + * menu (sizes are 10, 25, 50 and 100). Requires pagination (`paginate`). + * @type boolean + * @default true + * + * @dtopt Features + * @name DataTable.defaults.lengthChange + * + * @example + * $(document).ready( function () { + * $('#example').dataTable( { + * "lengthChange": false + * } ); + * } ); + */ + "bLengthChange": true, + + + /** + * Enable or disable pagination. + * @type boolean + * @default true + * + * @dtopt Features + * @name DataTable.defaults.paging + * + * @example + * $(document).ready( function () { + * $('#example').dataTable( { + * "paging": false + * } ); + * } ); + */ + "bPaginate": true, + + + /** + * Enable or disable the display of a 'processing' indicator when the table is + * being processed (e.g. a sort). This is particularly useful for tables with + * large amounts of data where it can take a noticeable amount of time to sort + * the entries. + * @type boolean + * @default false + * + * @dtopt Features + * @name DataTable.defaults.processing + * + * @example + * $(document).ready( function () { + * $('#example').dataTable( { + * "processing": true + * } ); + * } ); + */ + "bProcessing": false, + + + /** + * Retrieve the DataTables object for the given selector. Note that if the + * table has already been initialised, this parameter will cause DataTables + * to simply return the object that has already been set up - it will not take + * account of any changes you might have made to the initialisation object + * passed to DataTables (setting this parameter to true is an acknowledgement + * that you understand this). `destroy` can be used to reinitialise a table if + * you need. + * @type boolean + * @default false + * + * @dtopt Options + * @name DataTable.defaults.retrieve + * + * @example + * $(document).ready( function() { + * initTable(); + * tableActions(); + * } ); + * + * function initTable () + * { + * return $('#example').dataTable( { + * "scrollY": "200px", + * "paginate": false, + * "retrieve": true + * } ); + * } + * + * function tableActions () + * { + * var table = initTable(); + * // perform API operations with oTable + * } + */ + "bRetrieve": false, + + + /** + * When vertical (y) scrolling is enabled, DataTables will force the height of + * the table's viewport to the given height at all times (useful for layout). + * However, this can look odd when filtering data down to a small data set, + * and the footer is left "floating" further down. This parameter (when + * enabled) will cause DataTables to collapse the table's viewport down when + * the result set will fit within the given Y height. + * @type boolean + * @default false + * + * @dtopt Options + * @name DataTable.defaults.scrollCollapse + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "scrollY": "200", + * "scrollCollapse": true + * } ); + * } ); + */ + "bScrollCollapse": false, + + + /** + * Configure DataTables to use server-side processing. Note that the + * `ajax` parameter must also be given in order to give DataTables a + * source to obtain the required data for each draw. + * @type boolean + * @default false + * + * @dtopt Features + * @dtopt Server-side + * @name DataTable.defaults.serverSide + * + * @example + * $(document).ready( function () { + * $('#example').dataTable( { + * "serverSide": true, + * "ajax": "xhr.php" + * } ); + * } ); + */ + "bServerSide": false, + + + /** + * Enable or disable sorting of columns. Sorting of individual columns can be + * disabled by the `sortable` option for each column. + * @type boolean + * @default true + * + * @dtopt Features + * @name DataTable.defaults.ordering + * + * @example + * $(document).ready( function () { + * $('#example').dataTable( { + * "ordering": false + * } ); + * } ); + */ + "bSort": true, + + + /** + * Enable or display DataTables' ability to sort multiple columns at the + * same time (activated by shift-click by the user). + * @type boolean + * @default true + * + * @dtopt Options + * @name DataTable.defaults.orderMulti + * + * @example + * // Disable multiple column sorting ability + * $(document).ready( function () { + * $('#example').dataTable( { + * "orderMulti": false + * } ); + * } ); + */ + "bSortMulti": true, + + + /** + * Allows control over whether DataTables should use the top (true) unique + * cell that is found for a single column, or the bottom (false - default). + * This is useful when using complex headers. + * @type boolean + * @default false + * + * @dtopt Options + * @name DataTable.defaults.orderCellsTop + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "orderCellsTop": true + * } ); + * } ); + */ + "bSortCellsTop": false, + + + /** + * Enable or disable the addition of the classes `sorting\_1`, `sorting\_2` and + * `sorting\_3` to the columns which are currently being sorted on. This is + * presented as a feature switch as it can increase processing time (while + * classes are removed and added) so for large data sets you might want to + * turn this off. + * @type boolean + * @default true + * + * @dtopt Features + * @name DataTable.defaults.orderClasses + * + * @example + * $(document).ready( function () { + * $('#example').dataTable( { + * "orderClasses": false + * } ); + * } ); + */ + "bSortClasses": true, + + + /** + * Enable or disable state saving. When enabled HTML5 `localStorage` will be + * used to save table display information such as pagination information, + * display length, filtering and sorting. As such when the end user reloads + * the page the display display will match what thy had previously set up. + * + * Due to the use of `localStorage` the default state saving is not supported + * in IE6 or 7. If state saving is required in those browsers, use + * `stateSaveCallback` to provide a storage solution such as cookies. + * @type boolean + * @default false + * + * @dtopt Features + * @name DataTable.defaults.stateSave + * + * @example + * $(document).ready( function () { + * $('#example').dataTable( { + * "stateSave": true + * } ); + * } ); + */ + "bStateSave": false, + + + /** + * This function is called when a TR element is created (and all TD child + * elements have been inserted), or registered if using a DOM source, allowing + * manipulation of the TR element (adding classes etc). + * @type function + * @param {node} row "TR" element for the current row + * @param {array} data Raw data array for this row + * @param {int} dataIndex The index of this row in the internal aoData array + * + * @dtopt Callbacks + * @name DataTable.defaults.createdRow + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "createdRow": function( row, data, dataIndex ) { + * // Bold the grade for all 'A' grade browsers + * if ( data[4] == "A" ) + * { + * $('td:eq(4)', row).html( '<b>A</b>' ); + * } + * } + * } ); + * } ); + */ + "fnCreatedRow": null, + + + /** + * This function is called on every 'draw' event, and allows you to + * dynamically modify any aspect you want about the created DOM. + * @type function + * @param {object} settings DataTables settings object + * + * @dtopt Callbacks + * @name DataTable.defaults.drawCallback + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "drawCallback": function( settings ) { + * alert( 'DataTables has redrawn the table' ); + * } + * } ); + * } ); + */ + "fnDrawCallback": null, + + + /** + * Identical to fnHeaderCallback() but for the table footer this function + * allows you to modify the table footer on every 'draw' event. + * @type function + * @param {node} foot "TR" element for the footer + * @param {array} data Full table data (as derived from the original HTML) + * @param {int} start Index for the current display starting point in the + * display array + * @param {int} end Index for the current display ending point in the + * display array + * @param {array int} display Index array to translate the visual position + * to the full data array + * + * @dtopt Callbacks + * @name DataTable.defaults.footerCallback + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "footerCallback": function( tfoot, data, start, end, display ) { + * tfoot.getElementsByTagName('th')[0].innerHTML = "Starting index is "+start; + * } + * } ); + * } ) + */ + "fnFooterCallback": null, + + + /** + * When rendering large numbers in the information element for the table + * (i.e. "Showing 1 to 10 of 57 entries") DataTables will render large numbers + * to have a comma separator for the 'thousands' units (e.g. 1 million is + * rendered as "1,000,000") to help readability for the end user. This + * function will override the default method DataTables uses. + * @type function + * @member + * @param {int} toFormat number to be formatted + * @returns {string} formatted string for DataTables to show the number + * + * @dtopt Callbacks + * @name DataTable.defaults.formatNumber + * + * @example + * // Format a number using a single quote for the separator (note that + * // this can also be done with the language.thousands option) + * $(document).ready( function() { + * $('#example').dataTable( { + * "formatNumber": function ( toFormat ) { + * return toFormat.toString().replace( + * /\B(?=(\d{3})+(?!\d))/g, "'" + * ); + * }; + * } ); + * } ); + */ + "fnFormatNumber": function ( toFormat ) { + return toFormat.toString().replace( + /\B(?=(\d{3})+(?!\d))/g, + this.oLanguage.sThousands + ); + }, + + + /** + * This function is called on every 'draw' event, and allows you to + * dynamically modify the header row. This can be used to calculate and + * display useful information about the table. + * @type function + * @param {node} head "TR" element for the header + * @param {array} data Full table data (as derived from the original HTML) + * @param {int} start Index for the current display starting point in the + * display array + * @param {int} end Index for the current display ending point in the + * display array + * @param {array int} display Index array to translate the visual position + * to the full data array + * + * @dtopt Callbacks + * @name DataTable.defaults.headerCallback + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "fheaderCallback": function( head, data, start, end, display ) { + * head.getElementsByTagName('th')[0].innerHTML = "Displaying "+(end-start)+" records"; + * } + * } ); + * } ) + */ + "fnHeaderCallback": null, + + + /** + * The information element can be used to convey information about the current + * state of the table. Although the internationalisation options presented by + * DataTables are quite capable of dealing with most customisations, there may + * be times where you wish to customise the string further. This callback + * allows you to do exactly that. + * @type function + * @param {object} oSettings DataTables settings object + * @param {int} start Starting position in data for the draw + * @param {int} end End position in data for the draw + * @param {int} max Total number of rows in the table (regardless of + * filtering) + * @param {int} total Total number of rows in the data set, after filtering + * @param {string} pre The string that DataTables has formatted using it's + * own rules + * @returns {string} The string to be displayed in the information element. + * + * @dtopt Callbacks + * @name DataTable.defaults.infoCallback + * + * @example + * $('#example').dataTable( { + * "infoCallback": function( settings, start, end, max, total, pre ) { + * return start +" to "+ end; + * } + * } ); + */ + "fnInfoCallback": null, + + + /** + * Called when the table has been initialised. Normally DataTables will + * initialise sequentially and there will be no need for this function, + * however, this does not hold true when using external language information + * since that is obtained using an async XHR call. + * @type function + * @param {object} settings DataTables settings object + * @param {object} json The JSON object request from the server - only + * present if client-side Ajax sourced data is used + * + * @dtopt Callbacks + * @name DataTable.defaults.initComplete + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "initComplete": function(settings, json) { + * alert( 'DataTables has finished its initialisation.' ); + * } + * } ); + * } ) + */ + "fnInitComplete": null, + + + /** + * Called at the very start of each table draw and can be used to cancel the + * draw by returning false, any other return (including undefined) results in + * the full draw occurring). + * @type function + * @param {object} settings DataTables settings object + * @returns {boolean} False will cancel the draw, anything else (including no + * return) will allow it to complete. + * + * @dtopt Callbacks + * @name DataTable.defaults.preDrawCallback + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "preDrawCallback": function( settings ) { + * if ( $('#test').val() == 1 ) { + * return false; + * } + * } + * } ); + * } ); + */ + "fnPreDrawCallback": null, + + + /** + * This function allows you to 'post process' each row after it have been + * generated for each table draw, but before it is rendered on screen. This + * function might be used for setting the row class name etc. + * @type function + * @param {node} row "TR" element for the current row + * @param {array} data Raw data array for this row + * @param {int} displayIndex The display index for the current table draw + * @param {int} displayIndexFull The index of the data in the full list of + * rows (after filtering) + * + * @dtopt Callbacks + * @name DataTable.defaults.rowCallback + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "rowCallback": function( row, data, displayIndex, displayIndexFull ) { + * // Bold the grade for all 'A' grade browsers + * if ( data[4] == "A" ) { + * $('td:eq(4)', row).html( '<b>A</b>' ); + * } + * } + * } ); + * } ); + */ + "fnRowCallback": null, + + + /** + * __Deprecated__ The functionality provided by this parameter has now been + * superseded by that provided through `ajax`, which should be used instead. + * + * This parameter allows you to override the default function which obtains + * the data from the server so something more suitable for your application. + * For example you could use POST data, or pull information from a Gears or + * AIR database. + * @type function + * @member + * @param {string} source HTTP source to obtain the data from (`ajax`) + * @param {array} data A key/value pair object containing the data to send + * to the server + * @param {function} callback to be called on completion of the data get + * process that will draw the data on the page. + * @param {object} settings DataTables settings object + * + * @dtopt Callbacks + * @dtopt Server-side + * @name DataTable.defaults.serverData + * + * @deprecated 1.10. Please use `ajax` for this functionality now. + */ + "fnServerData": null, + + + /** + * __Deprecated__ The functionality provided by this parameter has now been + * superseded by that provided through `ajax`, which should be used instead. + * + * It is often useful to send extra data to the server when making an Ajax + * request - for example custom filtering information, and this callback + * function makes it trivial to send extra information to the server. The + * passed in parameter is the data set that has been constructed by + * DataTables, and you can add to this or modify it as you require. + * @type function + * @param {array} data Data array (array of objects which are name/value + * pairs) that has been constructed by DataTables and will be sent to the + * server. In the case of Ajax sourced data with server-side processing + * this will be an empty array, for server-side processing there will be a + * significant number of parameters! + * @returns {undefined} Ensure that you modify the data array passed in, + * as this is passed by reference. + * + * @dtopt Callbacks + * @dtopt Server-side + * @name DataTable.defaults.serverParams + * + * @deprecated 1.10. Please use `ajax` for this functionality now. + */ + "fnServerParams": null, + + + /** + * Load the table state. With this function you can define from where, and how, the + * state of a table is loaded. By default DataTables will load from `localStorage` + * but you might wish to use a server-side database or cookies. + * @type function + * @member + * @param {object} settings DataTables settings object + * @return {object} The DataTables state object to be loaded + * + * @dtopt Callbacks + * @name DataTable.defaults.stateLoadCallback + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "stateSave": true, + * "stateLoadCallback": function (settings) { + * var o; + * + * // Send an Ajax request to the server to get the data. Note that + * // this is a synchronous request. + * $.ajax( { + * "url": "/state_load", + * "async": false, + * "dataType": "json", + * "success": function (json) { + * o = json; + * } + * } ); + * + * return o; + * } + * } ); + * } ); + */ + "fnStateLoadCallback": function ( settings ) { + try { + return JSON.parse( + (settings.iStateDuration === -1 ? sessionStorage : localStorage).getItem( + 'DataTables_'+settings.sInstance+'_'+location.pathname + ) + ); + } catch (e) {} + }, + + + /** + * Callback which allows modification of the saved state prior to loading that state. + * This callback is called when the table is loading state from the stored data, but + * prior to the settings object being modified by the saved state. Note that for + * plug-in authors, you should use the `stateLoadParams` event to load parameters for + * a plug-in. + * @type function + * @param {object} settings DataTables settings object + * @param {object} data The state object that is to be loaded + * + * @dtopt Callbacks + * @name DataTable.defaults.stateLoadParams + * + * @example + * // Remove a saved filter, so filtering is never loaded + * $(document).ready( function() { + * $('#example').dataTable( { + * "stateSave": true, + * "stateLoadParams": function (settings, data) { + * data.oSearch.sSearch = ""; + * } + * } ); + * } ); + * + * @example + * // Disallow state loading by returning false + * $(document).ready( function() { + * $('#example').dataTable( { + * "stateSave": true, + * "stateLoadParams": function (settings, data) { + * return false; + * } + * } ); + * } ); + */ + "fnStateLoadParams": null, + + + /** + * Callback that is called when the state has been loaded from the state saving method + * and the DataTables settings object has been modified as a result of the loaded state. + * @type function + * @param {object} settings DataTables settings object + * @param {object} data The state object that was loaded + * + * @dtopt Callbacks + * @name DataTable.defaults.stateLoaded + * + * @example + * // Show an alert with the filtering value that was saved + * $(document).ready( function() { + * $('#example').dataTable( { + * "stateSave": true, + * "stateLoaded": function (settings, data) { + * alert( 'Saved filter was: '+data.oSearch.sSearch ); + * } + * } ); + * } ); + */ + "fnStateLoaded": null, + + + /** + * Save the table state. This function allows you to define where and how the state + * information for the table is stored By default DataTables will use `localStorage` + * but you might wish to use a server-side database or cookies. + * @type function + * @member + * @param {object} settings DataTables settings object + * @param {object} data The state object to be saved + * + * @dtopt Callbacks + * @name DataTable.defaults.stateSaveCallback + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "stateSave": true, + * "stateSaveCallback": function (settings, data) { + * // Send an Ajax request to the server with the state object + * $.ajax( { + * "url": "/state_save", + * "data": data, + * "dataType": "json", + * "method": "POST" + * "success": function () {} + * } ); + * } + * } ); + * } ); + */ + "fnStateSaveCallback": function ( settings, data ) { + try { + (settings.iStateDuration === -1 ? sessionStorage : localStorage).setItem( + 'DataTables_'+settings.sInstance+'_'+location.pathname, + JSON.stringify( data ) + ); + } catch (e) {} + }, + + + /** + * Callback which allows modification of the state to be saved. Called when the table + * has changed state a new state save is required. This method allows modification of + * the state saving object prior to actually doing the save, including addition or + * other state properties or modification. Note that for plug-in authors, you should + * use the `stateSaveParams` event to save parameters for a plug-in. + * @type function + * @param {object} settings DataTables settings object + * @param {object} data The state object to be saved + * + * @dtopt Callbacks + * @name DataTable.defaults.stateSaveParams + * + * @example + * // Remove a saved filter, so filtering is never saved + * $(document).ready( function() { + * $('#example').dataTable( { + * "stateSave": true, + * "stateSaveParams": function (settings, data) { + * data.oSearch.sSearch = ""; + * } + * } ); + * } ); + */ + "fnStateSaveParams": null, + + + /** + * Duration for which the saved state information is considered valid. After this period + * has elapsed the state will be returned to the default. + * Value is given in seconds. + * @type int + * @default 7200 <i>(2 hours)</i> + * + * @dtopt Options + * @name DataTable.defaults.stateDuration + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "stateDuration": 60*60*24; // 1 day + * } ); + * } ) + */ + "iStateDuration": 7200, + + + /** + * When enabled DataTables will not make a request to the server for the first + * page draw - rather it will use the data already on the page (no sorting etc + * will be applied to it), thus saving on an XHR at load time. `deferLoading` + * is used to indicate that deferred loading is required, but it is also used + * to tell DataTables how many records there are in the full table (allowing + * the information element and pagination to be displayed correctly). In the case + * where a filtering is applied to the table on initial load, this can be + * indicated by giving the parameter as an array, where the first element is + * the number of records available after filtering and the second element is the + * number of records without filtering (allowing the table information element + * to be shown correctly). + * @type int | array + * @default null + * + * @dtopt Options + * @name DataTable.defaults.deferLoading + * + * @example + * // 57 records available in the table, no filtering applied + * $(document).ready( function() { + * $('#example').dataTable( { + * "serverSide": true, + * "ajax": "scripts/server_processing.php", + * "deferLoading": 57 + * } ); + * } ); + * + * @example + * // 57 records after filtering, 100 without filtering (an initial filter applied) + * $(document).ready( function() { + * $('#example').dataTable( { + * "serverSide": true, + * "ajax": "scripts/server_processing.php", + * "deferLoading": [ 57, 100 ], + * "search": { + * "search": "my_filter" + * } + * } ); + * } ); + */ + "iDeferLoading": null, + + + /** + * Number of rows to display on a single page when using pagination. If + * feature enabled (`lengthChange`) then the end user will be able to override + * this to a custom setting using a pop-up menu. + * @type int + * @default 10 + * + * @dtopt Options + * @name DataTable.defaults.pageLength + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "pageLength": 50 + * } ); + * } ) + */ + "iDisplayLength": 10, + + + /** + * Define the starting point for data display when using DataTables with + * pagination. Note that this parameter is the number of records, rather than + * the page number, so if you have 10 records per page and want to start on + * the third page, it should be "20". + * @type int + * @default 0 + * + * @dtopt Options + * @name DataTable.defaults.displayStart + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "displayStart": 20 + * } ); + * } ) + */ + "iDisplayStart": 0, + + + /** + * By default DataTables allows keyboard navigation of the table (sorting, paging, + * and filtering) by adding a `tabindex` attribute to the required elements. This + * allows you to tab through the controls and press the enter key to activate them. + * The tabindex is default 0, meaning that the tab follows the flow of the document. + * You can overrule this using this parameter if you wish. Use a value of -1 to + * disable built-in keyboard navigation. + * @type int + * @default 0 + * + * @dtopt Options + * @name DataTable.defaults.tabIndex + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "tabIndex": 1 + * } ); + * } ); + */ + "iTabIndex": 0, + + + /** + * Classes that DataTables assigns to the various components and features + * that it adds to the HTML table. This allows classes to be configured + * during initialisation in addition to through the static + * {@link DataTable.ext.oStdClasses} object). + * @namespace + * @name DataTable.defaults.classes + */ + "oClasses": {}, + + + /** + * All strings that DataTables uses in the user interface that it creates + * are defined in this object, allowing you to modified them individually or + * completely replace them all as required. + * @namespace + * @name DataTable.defaults.language + */ + "oLanguage": { + /** + * Strings that are used for WAI-ARIA labels and controls only (these are not + * actually visible on the page, but will be read by screenreaders, and thus + * must be internationalised as well). + * @namespace + * @name DataTable.defaults.language.aria + */ + "oAria": { + /** + * ARIA label that is added to the table headers when the column may be + * sorted ascending by activing the column (click or return when focused). + * Note that the column header is prefixed to this string. + * @type string + * @default : activate to sort column ascending + * + * @dtopt Language + * @name DataTable.defaults.language.aria.sortAscending + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "language": { + * "aria": { + * "sortAscending": " - click/return to sort ascending" + * } + * } + * } ); + * } ); + */ + "sSortAscending": ": activate to sort column ascending", + + /** + * ARIA label that is added to the table headers when the column may be + * sorted descending by activing the column (click or return when focused). + * Note that the column header is prefixed to this string. + * @type string + * @default : activate to sort column ascending + * + * @dtopt Language + * @name DataTable.defaults.language.aria.sortDescending + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "language": { + * "aria": { + * "sortDescending": " - click/return to sort descending" + * } + * } + * } ); + * } ); + */ + "sSortDescending": ": activate to sort column descending" + }, + + /** + * Pagination string used by DataTables for the built-in pagination + * control types. + * @namespace + * @name DataTable.defaults.language.paginate + */ + "oPaginate": { + /** + * Text to use when using the 'full_numbers' type of pagination for the + * button to take the user to the first page. + * @type string + * @default First + * + * @dtopt Language + * @name DataTable.defaults.language.paginate.first + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "language": { + * "paginate": { + * "first": "First page" + * } + * } + * } ); + * } ); + */ + "sFirst": "First", + + + /** + * Text to use when using the 'full_numbers' type of pagination for the + * button to take the user to the last page. + * @type string + * @default Last + * + * @dtopt Language + * @name DataTable.defaults.language.paginate.last + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "language": { + * "paginate": { + * "last": "Last page" + * } + * } + * } ); + * } ); + */ + "sLast": "Last", + + + /** + * Text to use for the 'next' pagination button (to take the user to the + * next page). + * @type string + * @default Next + * + * @dtopt Language + * @name DataTable.defaults.language.paginate.next + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "language": { + * "paginate": { + * "next": "Next page" + * } + * } + * } ); + * } ); + */ + "sNext": "Next", + + + /** + * Text to use for the 'previous' pagination button (to take the user to + * the previous page). + * @type string + * @default Previous + * + * @dtopt Language + * @name DataTable.defaults.language.paginate.previous + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "language": { + * "paginate": { + * "previous": "Previous page" + * } + * } + * } ); + * } ); + */ + "sPrevious": "Previous" + }, + + /** + * This string is shown in preference to `zeroRecords` when the table is + * empty of data (regardless of filtering). Note that this is an optional + * parameter - if it is not given, the value of `zeroRecords` will be used + * instead (either the default or given value). + * @type string + * @default No data available in table + * + * @dtopt Language + * @name DataTable.defaults.language.emptyTable + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "language": { + * "emptyTable": "No data available in table" + * } + * } ); + * } ); + */ + "sEmptyTable": "No data available in table", + + + /** + * This string gives information to the end user about the information + * that is current on display on the page. The following tokens can be + * used in the string and will be dynamically replaced as the table + * display updates. This tokens can be placed anywhere in the string, or + * removed as needed by the language requires: + * + * * `\_START\_` - Display index of the first record on the current page + * * `\_END\_` - Display index of the last record on the current page + * * `\_TOTAL\_` - Number of records in the table after filtering + * * `\_MAX\_` - Number of records in the table without filtering + * * `\_PAGE\_` - Current page number + * * `\_PAGES\_` - Total number of pages of data in the table + * + * @type string + * @default Showing _START_ to _END_ of _TOTAL_ entries + * + * @dtopt Language + * @name DataTable.defaults.language.info + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "language": { + * "info": "Showing page _PAGE_ of _PAGES_" + * } + * } ); + * } ); + */ + "sInfo": "Showing _START_ to _END_ of _TOTAL_ entries", + + + /** + * Display information string for when the table is empty. Typically the + * format of this string should match `info`. + * @type string + * @default Showing 0 to 0 of 0 entries + * + * @dtopt Language + * @name DataTable.defaults.language.infoEmpty + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "language": { + * "infoEmpty": "No entries to show" + * } + * } ); + * } ); + */ + "sInfoEmpty": "Showing 0 to 0 of 0 entries", + + + /** + * When a user filters the information in a table, this string is appended + * to the information (`info`) to give an idea of how strong the filtering + * is. The variable _MAX_ is dynamically updated. + * @type string + * @default (filtered from _MAX_ total entries) + * + * @dtopt Language + * @name DataTable.defaults.language.infoFiltered + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "language": { + * "infoFiltered": " - filtering from _MAX_ records" + * } + * } ); + * } ); + */ + "sInfoFiltered": "(filtered from _MAX_ total entries)", + + + /** + * If can be useful to append extra information to the info string at times, + * and this variable does exactly that. This information will be appended to + * the `info` (`infoEmpty` and `infoFiltered` in whatever combination they are + * being used) at all times. + * @type string + * @default <i>Empty string</i> + * + * @dtopt Language + * @name DataTable.defaults.language.infoPostFix + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "language": { + * "infoPostFix": "All records shown are derived from real information." + * } + * } ); + * } ); + */ + "sInfoPostFix": "", + + + /** + * This decimal place operator is a little different from the other + * language options since DataTables doesn't output floating point + * numbers, so it won't ever use this for display of a number. Rather, + * what this parameter does is modify the sort methods of the table so + * that numbers which are in a format which has a character other than + * a period (`.`) as a decimal place will be sorted numerically. + * + * Note that numbers with different decimal places cannot be shown in + * the same table and still be sortable, the table must be consistent. + * However, multiple different tables on the page can use different + * decimal place characters. + * @type string + * @default + * + * @dtopt Language + * @name DataTable.defaults.language.decimal + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "language": { + * "decimal": "," + * "thousands": "." + * } + * } ); + * } ); + */ + "sDecimal": "", + + + /** + * DataTables has a build in number formatter (`formatNumber`) which is + * used to format large numbers that are used in the table information. + * By default a comma is used, but this can be trivially changed to any + * character you wish with this parameter. + * @type string + * @default , + * + * @dtopt Language + * @name DataTable.defaults.language.thousands + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "language": { + * "thousands": "'" + * } + * } ); + * } ); + */ + "sThousands": ",", + + + /** + * Detail the action that will be taken when the drop down menu for the + * pagination length option is changed. The '_MENU_' variable is replaced + * with a default select list of 10, 25, 50 and 100, and can be replaced + * with a custom select box if required. + * @type string + * @default Show _MENU_ entries + * + * @dtopt Language + * @name DataTable.defaults.language.lengthMenu + * + * @example + * // Language change only + * $(document).ready( function() { + * $('#example').dataTable( { + * "language": { + * "lengthMenu": "Display _MENU_ records" + * } + * } ); + * } ); + * + * @example + * // Language and options change + * $(document).ready( function() { + * $('#example').dataTable( { + * "language": { + * "lengthMenu": 'Display <select>'+ + * '<option value="10">10</option>'+ + * '<option value="20">20</option>'+ + * '<option value="30">30</option>'+ + * '<option value="40">40</option>'+ + * '<option value="50">50</option>'+ + * '<option value="-1">All</option>'+ + * '</select> records' + * } + * } ); + * } ); + */ + "sLengthMenu": "Show _MENU_ entries", + + + /** + * When using Ajax sourced data and during the first draw when DataTables is + * gathering the data, this message is shown in an empty row in the table to + * indicate to the end user the the data is being loaded. Note that this + * parameter is not used when loading data by server-side processing, just + * Ajax sourced data with client-side processing. + * @type string + * @default Loading... + * + * @dtopt Language + * @name DataTable.defaults.language.loadingRecords + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "language": { + * "loadingRecords": "Please wait - loading..." + * } + * } ); + * } ); + */ + "sLoadingRecords": "Loading...", + + + /** + * Text which is displayed when the table is processing a user action + * (usually a sort command or similar). + * @type string + * @default Processing... + * + * @dtopt Language + * @name DataTable.defaults.language.processing + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "language": { + * "processing": "DataTables is currently busy" + * } + * } ); + * } ); + */ + "sProcessing": "Processing...", + + + /** + * Details the actions that will be taken when the user types into the + * filtering input text box. The variable "_INPUT_", if used in the string, + * is replaced with the HTML text box for the filtering input allowing + * control over where it appears in the string. If "_INPUT_" is not given + * then the input box is appended to the string automatically. + * @type string + * @default Search: + * + * @dtopt Language + * @name DataTable.defaults.language.search + * + * @example + * // Input text box will be appended at the end automatically + * $(document).ready( function() { + * $('#example').dataTable( { + * "language": { + * "search": "Filter records:" + * } + * } ); + * } ); + * + * @example + * // Specify where the filter should appear + * $(document).ready( function() { + * $('#example').dataTable( { + * "language": { + * "search": "Apply filter _INPUT_ to table" + * } + * } ); + * } ); + */ + "sSearch": "Search:", + + + /** + * All of the language information can be stored in a file on the + * server-side, which DataTables will look up if this parameter is passed. + * It must store the URL of the language file, which is in a JSON format, + * and the object has the same properties as the oLanguage object in the + * initialiser object (i.e. the above parameters). Please refer to one of + * the example language files to see how this works in action. + * @type string + * @default <i>Empty string - i.e. disabled</i> + * + * @dtopt Language + * @name DataTable.defaults.language.url + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "language": { + * "url": "http://www.sprymedia.co.uk/dataTables/lang.txt" + * } + * } ); + * } ); + */ + "sUrl": "", + + + /** + * Text shown inside the table records when the is no information to be + * displayed after filtering. `emptyTable` is shown when there is simply no + * information in the table at all (regardless of filtering). + * @type string + * @default No matching records found + * + * @dtopt Language + * @name DataTable.defaults.language.zeroRecords + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "language": { + * "zeroRecords": "No records to display" + * } + * } ); + * } ); + */ + "sZeroRecords": "No matching records found" + }, + + + /** + * This parameter allows you to have define the global filtering state at + * initialisation time. As an object the `search` parameter must be + * defined, but all other parameters are optional. When `regex` is true, + * the search string will be treated as a regular expression, when false + * (default) it will be treated as a straight string. When `smart` + * DataTables will use it's smart filtering methods (to word match at + * any point in the data), when false this will not be done. + * @namespace + * @extends DataTable.models.oSearch + * + * @dtopt Options + * @name DataTable.defaults.search + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "search": {"search": "Initial search"} + * } ); + * } ) + */ + "oSearch": $.extend( {}, DataTable.models.oSearch ), + + + /** + * __Deprecated__ The functionality provided by this parameter has now been + * superseded by that provided through `ajax`, which should be used instead. + * + * By default DataTables will look for the property `data` (or `aaData` for + * compatibility with DataTables 1.9-) when obtaining data from an Ajax + * source or for server-side processing - this parameter allows that + * property to be changed. You can use Javascript dotted object notation to + * get a data source for multiple levels of nesting. + * @type string + * @default data + * + * @dtopt Options + * @dtopt Server-side + * @name DataTable.defaults.ajaxDataProp + * + * @deprecated 1.10. Please use `ajax` for this functionality now. + */ + "sAjaxDataProp": "data", + + + /** + * __Deprecated__ The functionality provided by this parameter has now been + * superseded by that provided through `ajax`, which should be used instead. + * + * You can instruct DataTables to load data from an external + * source using this parameter (use aData if you want to pass data in you + * already have). Simply provide a url a JSON object can be obtained from. + * @type string + * @default null + * + * @dtopt Options + * @dtopt Server-side + * @name DataTable.defaults.ajaxSource + * + * @deprecated 1.10. Please use `ajax` for this functionality now. + */ + "sAjaxSource": null, + + + /** + * This initialisation variable allows you to specify exactly where in the + * DOM you want DataTables to inject the various controls it adds to the page + * (for example you might want the pagination controls at the top of the + * table). DIV elements (with or without a custom class) can also be added to + * aid styling. The follow syntax is used: + * <ul> + * <li>The following options are allowed: + * <ul> + * <li>'l' - Length changing</li> + * <li>'f' - Filtering input</li> + * <li>'t' - The table!</li> + * <li>'i' - Information</li> + * <li>'p' - Pagination</li> + * <li>'r' - pRocessing</li> + * </ul> + * </li> + * <li>The following constants are allowed: + * <ul> + * <li>'H' - jQueryUI theme "header" classes ('fg-toolbar ui-widget-header ui-corner-tl ui-corner-tr ui-helper-clearfix')</li> + * <li>'F' - jQueryUI theme "footer" classes ('fg-toolbar ui-widget-header ui-corner-bl ui-corner-br ui-helper-clearfix')</li> + * </ul> + * </li> + * <li>The following syntax is expected: + * <ul> + * <li>'<' and '>' - div elements</li> + * <li>'<"class" and '>' - div with a class</li> + * <li>'<"#id" and '>' - div with an ID</li> + * </ul> + * </li> + * <li>Examples: + * <ul> + * <li>'<"wrapper"flipt>'</li> + * <li>'<lf<t>ip>'</li> + * </ul> + * </li> + * </ul> + * @type string + * @default lfrtip <i>(when `jQueryUI` is false)</i> <b>or</b> + * <"H"lfr>t<"F"ip> <i>(when `jQueryUI` is true)</i> + * + * @dtopt Options + * @name DataTable.defaults.dom + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "dom": '<"top"i>rt<"bottom"flp><"clear">' + * } ); + * } ); + */ + "sDom": "lfrtip", + + + /** + * DataTables features four different built-in options for the buttons to + * display for pagination control: + * + * * `simple` - 'Previous' and 'Next' buttons only + * * 'simple_numbers` - 'Previous' and 'Next' buttons, plus page numbers + * * `full` - 'First', 'Previous', 'Next' and 'Last' buttons + * * `full_numbers` - 'First', 'Previous', 'Next' and 'Last' buttons, plus + * page numbers + * + * Further methods can be added using {@link DataTable.ext.oPagination}. + * @type string + * @default simple_numbers + * + * @dtopt Options + * @name DataTable.defaults.pagingType + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "pagingType": "full_numbers" + * } ); + * } ) + */ + "sPaginationType": "simple_numbers", + + + /** + * Enable horizontal scrolling. When a table is too wide to fit into a + * certain layout, or you have a large number of columns in the table, you + * can enable x-scrolling to show the table in a viewport, which can be + * scrolled. This property can be `true` which will allow the table to + * scroll horizontally when needed, or any CSS unit, or a number (in which + * case it will be treated as a pixel measurement). Setting as simply `true` + * is recommended. + * @type boolean|string + * @default <i>blank string - i.e. disabled</i> + * + * @dtopt Features + * @name DataTable.defaults.scrollX + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "scrollX": true, + * "scrollCollapse": true + * } ); + * } ); + */ + "sScrollX": "", + + + /** + * This property can be used to force a DataTable to use more width than it + * might otherwise do when x-scrolling is enabled. For example if you have a + * table which requires to be well spaced, this parameter is useful for + * "over-sizing" the table, and thus forcing scrolling. This property can by + * any CSS unit, or a number (in which case it will be treated as a pixel + * measurement). + * @type string + * @default <i>blank string - i.e. disabled</i> + * + * @dtopt Options + * @name DataTable.defaults.scrollXInner + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "scrollX": "100%", + * "scrollXInner": "110%" + * } ); + * } ); + */ + "sScrollXInner": "", + + + /** + * Enable vertical scrolling. Vertical scrolling will constrain the DataTable + * to the given height, and enable scrolling for any data which overflows the + * current viewport. This can be used as an alternative to paging to display + * a lot of data in a small area (although paging and scrolling can both be + * enabled at the same time). This property can be any CSS unit, or a number + * (in which case it will be treated as a pixel measurement). + * @type string + * @default <i>blank string - i.e. disabled</i> + * + * @dtopt Features + * @name DataTable.defaults.scrollY + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "scrollY": "200px", + * "paginate": false + * } ); + * } ); + */ + "sScrollY": "", + + + /** + * __Deprecated__ The functionality provided by this parameter has now been + * superseded by that provided through `ajax`, which should be used instead. + * + * Set the HTTP method that is used to make the Ajax call for server-side + * processing or Ajax sourced data. + * @type string + * @default GET + * + * @dtopt Options + * @dtopt Server-side + * @name DataTable.defaults.serverMethod + * + * @deprecated 1.10. Please use `ajax` for this functionality now. + */ + "sServerMethod": "GET", + + + /** + * DataTables makes use of renderers when displaying HTML elements for + * a table. These renderers can be added or modified by plug-ins to + * generate suitable mark-up for a site. For example the Bootstrap + * integration plug-in for DataTables uses a paging button renderer to + * display pagination buttons in the mark-up required by Bootstrap. + * + * For further information about the renderers available see + * DataTable.ext.renderer + * @type string|object + * @default null + * + * @name DataTable.defaults.renderer + * + */ + "renderer": null + }; + + _fnHungarianMap( DataTable.defaults ); + + + + /* + * Developer note - See note in model.defaults.js about the use of Hungarian + * notation and camel case. + */ + + /** + * Column options that can be given to DataTables at initialisation time. + * @namespace + */ + DataTable.defaults.column = { + /** + * Define which column(s) an order will occur on for this column. This + * allows a column's ordering to take multiple columns into account when + * doing a sort or use the data from a different column. For example first + * name / last name columns make sense to do a multi-column sort over the + * two columns. + * @type array|int + * @default null <i>Takes the value of the column index automatically</i> + * + * @name DataTable.defaults.column.orderData + * @dtopt Columns + * + * @example + * // Using `columnDefs` + * $(document).ready( function() { + * $('#example').dataTable( { + * "columnDefs": [ + * { "orderData": [ 0, 1 ], "targets": [ 0 ] }, + * { "orderData": [ 1, 0 ], "targets": [ 1 ] }, + * { "orderData": 2, "targets": [ 2 ] } + * ] + * } ); + * } ); + * + * @example + * // Using `columns` + * $(document).ready( function() { + * $('#example').dataTable( { + * "columns": [ + * { "orderData": [ 0, 1 ] }, + * { "orderData": [ 1, 0 ] }, + * { "orderData": 2 }, + * null, + * null + * ] + * } ); + * } ); + */ + "aDataSort": null, + "iDataSort": -1, + + + /** + * You can control the default ordering direction, and even alter the + * behaviour of the sort handler (i.e. only allow ascending ordering etc) + * using this parameter. + * @type array + * @default [ 'asc', 'desc' ] + * + * @name DataTable.defaults.column.orderSequence + * @dtopt Columns + * + * @example + * // Using `columnDefs` + * $(document).ready( function() { + * $('#example').dataTable( { + * "columnDefs": [ + * { "orderSequence": [ "asc" ], "targets": [ 1 ] }, + * { "orderSequence": [ "desc", "asc", "asc" ], "targets": [ 2 ] }, + * { "orderSequence": [ "desc" ], "targets": [ 3 ] } + * ] + * } ); + * } ); + * + * @example + * // Using `columns` + * $(document).ready( function() { + * $('#example').dataTable( { + * "columns": [ + * null, + * { "orderSequence": [ "asc" ] }, + * { "orderSequence": [ "desc", "asc", "asc" ] }, + * { "orderSequence": [ "desc" ] }, + * null + * ] + * } ); + * } ); + */ + "asSorting": [ 'asc', 'desc' ], + + + /** + * Enable or disable filtering on the data in this column. + * @type boolean + * @default true + * + * @name DataTable.defaults.column.searchable + * @dtopt Columns + * + * @example + * // Using `columnDefs` + * $(document).ready( function() { + * $('#example').dataTable( { + * "columnDefs": [ + * { "searchable": false, "targets": [ 0 ] } + * ] } ); + * } ); + * + * @example + * // Using `columns` + * $(document).ready( function() { + * $('#example').dataTable( { + * "columns": [ + * { "searchable": false }, + * null, + * null, + * null, + * null + * ] } ); + * } ); + */ + "bSearchable": true, + + + /** + * Enable or disable ordering on this column. + * @type boolean + * @default true + * + * @name DataTable.defaults.column.orderable + * @dtopt Columns + * + * @example + * // Using `columnDefs` + * $(document).ready( function() { + * $('#example').dataTable( { + * "columnDefs": [ + * { "orderable": false, "targets": [ 0 ] } + * ] } ); + * } ); + * + * @example + * // Using `columns` + * $(document).ready( function() { + * $('#example').dataTable( { + * "columns": [ + * { "orderable": false }, + * null, + * null, + * null, + * null + * ] } ); + * } ); + */ + "bSortable": true, + + + /** + * Enable or disable the display of this column. + * @type boolean + * @default true + * + * @name DataTable.defaults.column.visible + * @dtopt Columns + * + * @example + * // Using `columnDefs` + * $(document).ready( function() { + * $('#example').dataTable( { + * "columnDefs": [ + * { "visible": false, "targets": [ 0 ] } + * ] } ); + * } ); + * + * @example + * // Using `columns` + * $(document).ready( function() { + * $('#example').dataTable( { + * "columns": [ + * { "visible": false }, + * null, + * null, + * null, + * null + * ] } ); + * } ); + */ + "bVisible": true, + + + /** + * Developer definable function that is called whenever a cell is created (Ajax source, + * etc) or processed for input (DOM source). This can be used as a compliment to mRender + * allowing you to modify the DOM element (add background colour for example) when the + * element is available. + * @type function + * @param {element} td The TD node that has been created + * @param {*} cellData The Data for the cell + * @param {array|object} rowData The data for the whole row + * @param {int} row The row index for the aoData data store + * @param {int} col The column index for aoColumns + * + * @name DataTable.defaults.column.createdCell + * @dtopt Columns + * + * @example + * $(document).ready( function() { + * $('#example').dataTable( { + * "columnDefs": [ { + * "targets": [3], + * "createdCell": function (td, cellData, rowData, row, col) { + * if ( cellData == "1.7" ) { + * $(td).css('color', 'blue') + * } + * } + * } ] + * }); + * } ); + */ + "fnCreatedCell": null, + + + /** + * This parameter has been replaced by `data` in DataTables to ensure naming + * consistency. `dataProp` can still be used, as there is backwards + * compatibility in DataTables for this option, but it is strongly + * recommended that you use `data` in preference to `dataProp`. + * @name DataTable.defaults.column.dataProp + */ + + + /** + * This property can be used to read data from any data source property, + * including deeply nested objects / properties. `data` can be given in a + * number of different ways which effect its behaviour: + * + * * `integer` - treated as an array index for the data source. This is the + * default that DataTables uses (incrementally increased for each column). + * * `string` - read an object property from the data source. There are + * three 'special' options that can be used in the string to alter how + * DataTables reads the data from the source object: + * * `.` - Dotted Javascript notation. Just as you use a `.` in + * Javascript to read from nested objects, so to can the options + * specified in `data`. For example: `browser.version` or + * `browser.name`. If your object parameter name contains a period, use + * `\\` to escape it - i.e. `first\\.name`. + * * `[]` - Array notation. DataTables can automatically combine data + * from and array source, joining the data with the characters provided + * between the two brackets. For example: `name[, ]` would provide a + * comma-space separated list from the source array. If no characters + * are provided between the brackets, the original array source is + * returned. + * * `()` - Function notation. Adding `()` to the end of a parameter will + * execute a function of the name given. For example: `browser()` for a + * simple function on the data source, `browser.version()` for a + * function in a nested property or even `browser().version` to get an + * object property if the function called returns an object. Note that + * function notation is recommended for use in `render` rather than + * `data` as it is much simpler to use as a renderer. + * * `null` - use the original data source for the row rather than plucking + * data directly from it. This action has effects on two other + * initialisation options: + * * `defaultContent` - When null is given as the `data` option and + * `defaultContent` is specified for the column, the value defined by + * `defaultContent` will be used for the cell. + * * `render` - When null is used for the `data` option and the `render` + * option is specified for the column, the whole data source for the + * row is used for the renderer. + * * `function` - the function given will be executed whenever DataTables + * needs to set or get the data for a cell in the column. The function + * takes three parameters: + * * Parameters: + * * `{array|object}` The data source for the row + * * `{string}` The type call data requested - this will be 'set' when + * setting data or 'filter', 'display', 'type', 'sort' or undefined + * when gathering data. Note that when `undefined` is given for the + * type DataTables expects to get the raw data for the object back< + * * `{*}` Data to set when the second parameter is 'set'. + * * Return: + * * The return value from the function is not required when 'set' is + * the type of call, but otherwise the return is what will be used + * for the data requested. + * + * Note that `data` is a getter and setter option. If you just require + * formatting of data for output, you will likely want to use `render` which + * is simply a getter and thus simpler to use. + * + * Note that prior to DataTables 1.9.2 `data` was called `mDataProp`. The + * name change reflects the flexibility of this property and is consistent + * with the naming of mRender. If 'mDataProp' is given, then it will still + * be used by DataTables, as it automatically maps the old name to the new + * if required. + * + * @type string|int|function|null + * @default null <i>Use automatically calculated column index</i> + * + * @name DataTable.defaults.column.data + * @dtopt Columns + * + * @example + * // Read table data from objects + * // JSON structure for each row: + * // { + * // "engine": {value}, + * // "browser": {value}, + * // "platform": {value}, + * // "version": {value}, + * // "grade": {value} + * // } + * $(document).ready( function() { + * $('#example').dataTable( { + * "ajaxSource": "sources/objects.txt", + * "columns": [ + * { "data": "engine" }, + * { "data": "browser" }, + * { "data": "platform" }, + * { "data": "version" }, + * { "data": "grade" } + * ] + * } ); + * } ); + * + * @example + * // Read information from deeply nested objects + * // JSON structure for each row: + * // { + * // "engine": {value}, + * // "browser": {value}, + * // "platform": { + * // "inner": {value} + * // }, + * // "details": [ + * // {value}, {value} + * // ] + * // } + * $(document).ready( function() { + * $('#example').dataTable( { + * "ajaxSource": "sources/deep.txt", + * "columns": [ + * { "data": "engine" }, + * { "data": "browser" }, + * { "data": "platform.inner" }, + * { "data": "platform.details.0" }, + * { "data": "platform.details.1" } + * ] + * } ); + * } ); + * + * @example + * // Using `data` as a function to provide different information for + * // sorting, filtering and display. In this case, currency (price) + * $(document).ready( function() { + * $('#example').dataTable( { + * "columnDefs": [ { + * "targets": [ 0 ], + * "data": function ( source, type, val ) { + * if (type === 'set') { + * source.price = val; + * // Store the computed dislay and filter values for efficiency + * source.price_display = val=="" ? "" : "$"+numberFormat(val); + * source.price_filter = val=="" ? "" : "$"+numberFormat(val)+" "+val; + * return; + * } + * else if (type === 'display') { + * return source.price_display; + * } + * else if (type === 'filter') { + * return source.price_filter; + * } + * // 'sort', 'type' and undefined all just use the integer + * return source.price; + * } + * } ] + * } ); + * } ); + * + * @example + * // Using default content + * $(document).ready( function() { + * $('#example').dataTable( { + * "columnDefs": [ { + * "targets": [ 0 ], + * "data": null, + * "defaultContent": "Click to edit" + * } ] + * } ); + * } ); + * + * @example + * // Using array notation - outputting a list from an array + * $(document).ready( function() { + * $('#example').dataTable( { + * "columnDefs": [ { + * "targets": [ 0 ], + * "data": "name[, ]" + * } ] + * } ); + * } ); + * + */ + "mData": null, + + + /** + * This property is the rendering partner to `data` and it is suggested that + * when you want to manipulate data for display (including filtering, + * sorting etc) without altering the underlying data for the table, use this + * property. `render` can be considered to be the the read only companion to + * `data` which is read / write (then as such more complex). Like `data` + * this option can be given in a number of different ways to effect its + * behaviour: + * + * * `integer` - treated as an array index for the data source. This is the + * default that DataTables uses (incrementally increased for each column). + * * `string` - read an object property from the data source. There are + * three 'special' options that can be used in the string to alter how + * DataTables reads the data from the source object: + * * `.` - Dotted Javascript notation. Just as you use a `.` in + * Javascript to read from nested objects, so to can the options + * specified in `data`. For example: `browser.version` or + * `browser.name`. If your object parameter name contains a period, use + * `\\` to escape it - i.e. `first\\.name`. + * * `[]` - Array notation. DataTables can automatically combine data + * from and array source, joining the data with the characters provided + * between the two brackets. For example: `name[, ]` would provide a + * comma-space separated list from the source array. If no characters + * are provided between the brackets, the original array source is + * returned. + * * `()` - Function notation. Adding `()` to the end of a parameter will + * execute a function of the name given. For example: `browser()` for a + * simple function on the data source, `browser.version()` for a + * function in a nested property or even `browser().version` to get an + * object property if the function called returns an object. + * * `object` - use different data for the different data types requested by + * DataTables ('filter', 'display', 'type' or 'sort'). The property names + * of the object is the data type the property refers to and the value can + * defined using an integer, string or function using the same rules as + * `render` normally does. Note that an `_` option _must_ be specified. + * This is the default value to use if you haven't specified a value for + * the data type requested by DataTables. + * * `function` - the function given will be executed whenever DataTables + * needs to set or get the data for a cell in the column. The function + * takes three parameters: + * * Parameters: + * * {array|object} The data source for the row (based on `data`) + * * {string} The type call data requested - this will be 'filter', + * 'display', 'type' or 'sort'. + * * {array|object} The full data source for the row (not based on + * `data`) + * * Return: + * * The return value from the function is what will be used for the + * data requested. + * + * @type string|int|function|object|null + * @default null Use the data source value. + * + * @name DataTable.defaults.column.render + * @dtopt Columns + * + * @example + * // Create a comma separated list from an array of objects + * $(document).ready( function() { + * $('#example').dataTable( { + * "ajaxSource": "sources/deep.txt", + * "columns": [ + * { "data": "engine" }, + * { "data": "browser" }, + * { + * "data": "platform", + * "render": "[, ].name" + * } + * ] + * } ); + * } ); + * + * @example + * // Execute a function to obtain data + * $(document).ready( function() { + * $('#example').dataTable( { + * "columnDefs": [ { + * "targets": [ 0 ], + * "data": null, // Use the full data source object for the renderer's source + * "render": "browserName()" + * } ] + * } ); + * } ); + * + * @example + * // As an object, extracting different data for the different types + * // This would be used with a data source such as: + * // { "phone": 5552368, "phone_filter": "5552368 555-2368", "phone_display": "555-2368" } + * // Here the `phone` integer is used for sorting and type detection, while `phone_filter` + * // (which has both forms) is used for filtering for if a user inputs either format, while + * // the formatted phone number is the one that is shown in the table. + * $(document).ready( function() { + * $('#example').dataTable( { + * "columnDefs": [ { + * "targets": [ 0 ], + * "data": null, // Use the full data source object for the renderer's source + * "render": { + * "_": "phone", + * "filter": "phone_filter", + * "display": "phone_display" + * } + * } ] + * } ); + * } ); + * + * @example + * // Use as a function to create a link from the data source + * $(document).ready( function() { + * $('#example').dataTable( { + * "columnDefs": [ { + * "targets": [ 0 ], + * "data": "download_link", + * "render": function ( data, type, full ) { + * return '<a href="'+data+'">Download</a>'; + * } + * } ] + * } ); + * } ); + */ + "mRender": null, + + + /** + * Change the cell type created for the column - either TD cells or TH cells. This + * can be useful as TH cells have semantic meaning in the table body, allowing them + * to act as a header for a row (you may wish to add scope='row' to the TH elements). + * @type string + * @default td + * + * @name DataTable.defaults.column.cellType + * @dtopt Columns + * + * @example + * // Make the first column use TH cells + * $(document).ready( function() { + * $('#example').dataTable( { + * "columnDefs": [ { + * "targets": [ 0 ], + * "cellType": "th" + * } ] + * } ); + * } ); + */ + "sCellType": "td", + + + /** + * Class to give to each cell in this column. + * @type string + * @default <i>Empty string</i> + * + * @name DataTable.defaults.column.class + * @dtopt Columns + * + * @example + * // Using `columnDefs` + * $(document).ready( function() { + * $('#example').dataTable( { + * "columnDefs": [ + * { "class": "my_class", "targets": [ 0 ] } + * ] + * } ); + * } ); + * + * @example + * // Using `columns` + * $(document).ready( function() { + * $('#example').dataTable( { + * "columns": [ + * { "class": "my_class" }, + * null, + * null, + * null, + * null + * ] + * } ); + * } ); + */ + "sClass": "", + + /** + * When DataTables calculates the column widths to assign to each column, + * it finds the longest string in each column and then constructs a + * temporary table and reads the widths from that. The problem with this + * is that "mmm" is much wider then "iiii", but the latter is a longer + * string - thus the calculation can go wrong (doing it properly and putting + * it into an DOM object and measuring that is horribly(!) slow). Thus as + * a "work around" we provide this option. It will append its value to the + * text that is found to be the longest string for the column - i.e. padding. + * Generally you shouldn't need this! + * @type string + * @default <i>Empty string<i> + * + * @name DataTable.defaults.column.contentPadding + * @dtopt Columns + * + * @example + * // Using `columns` + * $(document).ready( function() { + * $('#example').dataTable( { + * "columns": [ + * null, + * null, + * null, + * { + * "contentPadding": "mmm" + * } + * ] + * } ); + * } ); + */ + "sContentPadding": "", + + + /** + * Allows a default value to be given for a column's data, and will be used + * whenever a null data source is encountered (this can be because `data` + * is set to null, or because the data source itself is null). + * @type string + * @default null + * + * @name DataTable.defaults.column.defaultContent + * @dtopt Columns + * + * @example + * // Using `columnDefs` + * $(document).ready( function() { + * $('#example').dataTable( { + * "columnDefs": [ + * { + * "data": null, + * "defaultContent": "Edit", + * "targets": [ -1 ] + * } + * ] + * } ); + * } ); + * + * @example + * // Using `columns` + * $(document).ready( function() { + * $('#example').dataTable( { + * "columns": [ + * null, + * null, + * null, + * { + * "data": null, + * "defaultContent": "Edit" + * } + * ] + * } ); + * } ); + */ + "sDefaultContent": null, + + + /** + * This parameter is only used in DataTables' server-side processing. It can + * be exceptionally useful to know what columns are being displayed on the + * client side, and to map these to database fields. When defined, the names + * also allow DataTables to reorder information from the server if it comes + * back in an unexpected order (i.e. if you switch your columns around on the + * client-side, your server-side code does not also need updating). + * @type string + * @default <i>Empty string</i> + * + * @name DataTable.defaults.column.name + * @dtopt Columns + * + * @example + * // Using `columnDefs` + * $(document).ready( function() { + * $('#example').dataTable( { + * "columnDefs": [ + * { "name": "engine", "targets": [ 0 ] }, + * { "name": "browser", "targets": [ 1 ] }, + * { "name": "platform", "targets": [ 2 ] }, + * { "name": "version", "targets": [ 3 ] }, + * { "name": "grade", "targets": [ 4 ] } + * ] + * } ); + * } ); + * + * @example + * // Using `columns` + * $(document).ready( function() { + * $('#example').dataTable( { + * "columns": [ + * { "name": "engine" }, + * { "name": "browser" }, + * { "name": "platform" }, + * { "name": "version" }, + * { "name": "grade" } + * ] + * } ); + * } ); + */ + "sName": "", + + + /** + * Defines a data source type for the ordering which can be used to read + * real-time information from the table (updating the internally cached + * version) prior to ordering. This allows ordering to occur on user + * editable elements such as form inputs. + * @type string + * @default std + * + * @name DataTable.defaults.column.orderDataType + * @dtopt Columns + * + * @example + * // Using `columnDefs` + * $(document).ready( function() { + * $('#example').dataTable( { + * "columnDefs": [ + * { "orderDataType": "dom-text", "targets": [ 2, 3 ] }, + * { "type": "numeric", "targets": [ 3 ] }, + * { "orderDataType": "dom-select", "targets": [ 4 ] }, + * { "orderDataType": "dom-checkbox", "targets": [ 5 ] } + * ] + * } ); + * } ); + * + * @example + * // Using `columns` + * $(document).ready( function() { + * $('#example').dataTable( { + * "columns": [ + * null, + * null, + * { "orderDataType": "dom-text" }, + * { "orderDataType": "dom-text", "type": "numeric" }, + * { "orderDataType": "dom-select" }, + * { "orderDataType": "dom-checkbox" } + * ] + * } ); + * } ); + */ + "sSortDataType": "std", + + + /** + * The title of this column. + * @type string + * @default null <i>Derived from the 'TH' value for this column in the + * original HTML table.</i> + * + * @name DataTable.defaults.column.title + * @dtopt Columns + * + * @example + * // Using `columnDefs` + * $(document).ready( function() { + * $('#example').dataTable( { + * "columnDefs": [ + * { "title": "My column title", "targets": [ 0 ] } + * ] + * } ); + * } ); + * + * @example + * // Using `columns` + * $(document).ready( function() { + * $('#example').dataTable( { + * "columns": [ + * { "title": "My column title" }, + * null, + * null, + * null, + * null + * ] + * } ); + * } ); + */ + "sTitle": null, + + + /** + * The type allows you to specify how the data for this column will be + * ordered. Four types (string, numeric, date and html (which will strip + * HTML tags before ordering)) are currently available. Note that only date + * formats understood by Javascript's Date() object will be accepted as type + * date. For example: "Mar 26, 2008 5:03 PM". May take the values: 'string', + * 'numeric', 'date' or 'html' (by default). Further types can be adding + * through plug-ins. + * @type string + * @default null <i>Auto-detected from raw data</i> + * + * @name DataTable.defaults.column.type + * @dtopt Columns + * + * @example + * // Using `columnDefs` + * $(document).ready( function() { + * $('#example').dataTable( { + * "columnDefs": [ + * { "type": "html", "targets": [ 0 ] } + * ] + * } ); + * } ); + * + * @example + * // Using `columns` + * $(document).ready( function() { + * $('#example').dataTable( { + * "columns": [ + * { "type": "html" }, + * null, + * null, + * null, + * null + * ] + * } ); + * } ); + */ + "sType": null, + + + /** + * Defining the width of the column, this parameter may take any CSS value + * (3em, 20px etc). DataTables applies 'smart' widths to columns which have not + * been given a specific width through this interface ensuring that the table + * remains readable. + * @type string + * @default null <i>Automatic</i> + * + * @name DataTable.defaults.column.width + * @dtopt Columns + * + * @example + * // Using `columnDefs` + * $(document).ready( function() { + * $('#example').dataTable( { + * "columnDefs": [ + * { "width": "20%", "targets": [ 0 ] } + * ] + * } ); + * } ); + * + * @example + * // Using `columns` + * $(document).ready( function() { + * $('#example').dataTable( { + * "columns": [ + * { "width": "20%" }, + * null, + * null, + * null, + * null + * ] + * } ); + * } ); + */ + "sWidth": null + }; + + _fnHungarianMap( DataTable.defaults.column ); + + + + /** + * DataTables settings object - this holds all the information needed for a + * given table, including configuration, data and current application of the + * table options. DataTables does not have a single instance for each DataTable + * with the settings attached to that instance, but rather instances of the + * DataTable "class" are created on-the-fly as needed (typically by a + * $().dataTable() call) and the settings object is then applied to that + * instance. + * + * Note that this object is related to {@link DataTable.defaults} but this + * one is the internal data store for DataTables's cache of columns. It should + * NOT be manipulated outside of DataTables. Any configuration should be done + * through the initialisation options. + * @namespace + * @todo Really should attach the settings object to individual instances so we + * don't need to create new instances on each $().dataTable() call (if the + * table already exists). It would also save passing oSettings around and + * into every single function. However, this is a very significant + * architecture change for DataTables and will almost certainly break + * backwards compatibility with older installations. This is something that + * will be done in 2.0. + */ + DataTable.models.oSettings = { + /** + * Primary features of DataTables and their enablement state. + * @namespace + */ + "oFeatures": { + + /** + * Flag to say if DataTables should automatically try to calculate the + * optimum table and columns widths (true) or not (false). + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + * @type boolean + */ + "bAutoWidth": null, + + /** + * Delay the creation of TR and TD elements until they are actually + * needed by a driven page draw. This can give a significant speed + * increase for Ajax source and Javascript source data, but makes no + * difference at all fro DOM and server-side processing tables. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + * @type boolean + */ + "bDeferRender": null, + + /** + * Enable filtering on the table or not. Note that if this is disabled + * then there is no filtering at all on the table, including fnFilter. + * To just remove the filtering input use sDom and remove the 'f' option. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + * @type boolean + */ + "bFilter": null, + + /** + * Table information element (the 'Showing x of y records' div) enable + * flag. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + * @type boolean + */ + "bInfo": null, + + /** + * Present a user control allowing the end user to change the page size + * when pagination is enabled. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + * @type boolean + */ + "bLengthChange": null, + + /** + * Pagination enabled or not. Note that if this is disabled then length + * changing must also be disabled. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + * @type boolean + */ + "bPaginate": null, + + /** + * Processing indicator enable flag whenever DataTables is enacting a + * user request - typically an Ajax request for server-side processing. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + * @type boolean + */ + "bProcessing": null, + + /** + * Server-side processing enabled flag - when enabled DataTables will + * get all data from the server for every draw - there is no filtering, + * sorting or paging done on the client-side. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + * @type boolean + */ + "bServerSide": null, + + /** + * Sorting enablement flag. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + * @type boolean + */ + "bSort": null, + + /** + * Multi-column sorting + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + * @type boolean + */ + "bSortMulti": null, + + /** + * Apply a class to the columns which are being sorted to provide a + * visual highlight or not. This can slow things down when enabled since + * there is a lot of DOM interaction. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + * @type boolean + */ + "bSortClasses": null, + + /** + * State saving enablement flag. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + * @type boolean + */ + "bStateSave": null + }, + + + /** + * Scrolling settings for a table. + * @namespace + */ + "oScroll": { + /** + * When the table is shorter in height than sScrollY, collapse the + * table container down to the height of the table (when true). + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + * @type boolean + */ + "bCollapse": null, + + /** + * Width of the scrollbar for the web-browser's platform. Calculated + * during table initialisation. + * @type int + * @default 0 + */ + "iBarWidth": 0, + + /** + * Viewport width for horizontal scrolling. Horizontal scrolling is + * disabled if an empty string. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + * @type string + */ + "sX": null, + + /** + * Width to expand the table to when using x-scrolling. Typically you + * should not need to use this. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + * @type string + * @deprecated + */ + "sXInner": null, + + /** + * Viewport height for vertical scrolling. Vertical scrolling is disabled + * if an empty string. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + * @type string + */ + "sY": null + }, + + /** + * Language information for the table. + * @namespace + * @extends DataTable.defaults.oLanguage + */ + "oLanguage": { + /** + * Information callback function. See + * {@link DataTable.defaults.fnInfoCallback} + * @type function + * @default null + */ + "fnInfoCallback": null + }, + + /** + * Browser support parameters + * @namespace + */ + "oBrowser": { + /** + * Indicate if the browser incorrectly calculates width:100% inside a + * scrolling element (IE6/7) + * @type boolean + * @default false + */ + "bScrollOversize": false, + + /** + * Determine if the vertical scrollbar is on the right or left of the + * scrolling container - needed for rtl language layout, although not + * all browsers move the scrollbar (Safari). + * @type boolean + * @default false + */ + "bScrollbarLeft": false + }, + + + "ajax": null, + + + /** + * Array referencing the nodes which are used for the features. The + * parameters of this object match what is allowed by sDom - i.e. + * <ul> + * <li>'l' - Length changing</li> + * <li>'f' - Filtering input</li> + * <li>'t' - The table!</li> + * <li>'i' - Information</li> + * <li>'p' - Pagination</li> + * <li>'r' - pRocessing</li> + * </ul> + * @type array + * @default [] + */ + "aanFeatures": [], + + /** + * Store data information - see {@link DataTable.models.oRow} for detailed + * information. + * @type array + * @default [] + */ + "aoData": [], + + /** + * Array of indexes which are in the current display (after filtering etc) + * @type array + * @default [] + */ + "aiDisplay": [], + + /** + * Array of indexes for display - no filtering + * @type array + * @default [] + */ + "aiDisplayMaster": [], + + /** + * Store information about each column that is in use + * @type array + * @default [] + */ + "aoColumns": [], + + /** + * Store information about the table's header + * @type array + * @default [] + */ + "aoHeader": [], + + /** + * Store information about the table's footer + * @type array + * @default [] + */ + "aoFooter": [], + + /** + * Store the applied global search information in case we want to force a + * research or compare the old search to a new one. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + * @namespace + * @extends DataTable.models.oSearch + */ + "oPreviousSearch": {}, + + /** + * Store the applied search for each column - see + * {@link DataTable.models.oSearch} for the format that is used for the + * filtering information for each column. + * @type array + * @default [] + */ + "aoPreSearchCols": [], + + /** + * Sorting that is applied to the table. Note that the inner arrays are + * used in the following manner: + * <ul> + * <li>Index 0 - column number</li> + * <li>Index 1 - current sorting direction</li> + * </ul> + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + * @type array + * @todo These inner arrays should really be objects + */ + "aaSorting": null, + + /** + * Sorting that is always applied to the table (i.e. prefixed in front of + * aaSorting). + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + * @type array + * @default [] + */ + "aaSortingFixed": [], + + /** + * Classes to use for the striping of a table. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + * @type array + * @default [] + */ + "asStripeClasses": null, + + /** + * If restoring a table - we should restore its striping classes as well + * @type array + * @default [] + */ + "asDestroyStripes": [], + + /** + * If restoring a table - we should restore its width + * @type int + * @default 0 + */ + "sDestroyWidth": 0, + + /** + * Callback functions array for every time a row is inserted (i.e. on a draw). + * @type array + * @default [] + */ + "aoRowCallback": [], + + /** + * Callback functions for the header on each draw. + * @type array + * @default [] + */ + "aoHeaderCallback": [], + + /** + * Callback function for the footer on each draw. + * @type array + * @default [] + */ + "aoFooterCallback": [], + + /** + * Array of callback functions for draw callback functions + * @type array + * @default [] + */ + "aoDrawCallback": [], + + /** + * Array of callback functions for row created function + * @type array + * @default [] + */ + "aoRowCreatedCallback": [], + + /** + * Callback functions for just before the table is redrawn. A return of + * false will be used to cancel the draw. + * @type array + * @default [] + */ + "aoPreDrawCallback": [], + + /** + * Callback functions for when the table has been initialised. + * @type array + * @default [] + */ + "aoInitComplete": [], + + + /** + * Callbacks for modifying the settings to be stored for state saving, prior to + * saving state. + * @type array + * @default [] + */ + "aoStateSaveParams": [], + + /** + * Callbacks for modifying the settings that have been stored for state saving + * prior to using the stored values to restore the state. + * @type array + * @default [] + */ + "aoStateLoadParams": [], + + /** + * Callbacks for operating on the settings object once the saved state has been + * loaded + * @type array + * @default [] + */ + "aoStateLoaded": [], + + /** + * Cache the table ID for quick access + * @type string + * @default <i>Empty string</i> + */ + "sTableId": "", + + /** + * The TABLE node for the main table + * @type node + * @default null + */ + "nTable": null, + + /** + * Permanent ref to the thead element + * @type node + * @default null + */ + "nTHead": null, + + /** + * Permanent ref to the tfoot element - if it exists + * @type node + * @default null + */ + "nTFoot": null, + + /** + * Permanent ref to the tbody element + * @type node + * @default null + */ + "nTBody": null, + + /** + * Cache the wrapper node (contains all DataTables controlled elements) + * @type node + * @default null + */ + "nTableWrapper": null, + + /** + * Indicate if when using server-side processing the loading of data + * should be deferred until the second draw. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + * @type boolean + * @default false + */ + "bDeferLoading": false, + + /** + * Indicate if all required information has been read in + * @type boolean + * @default false + */ + "bInitialised": false, + + /** + * Information about open rows. Each object in the array has the parameters + * 'nTr' and 'nParent' + * @type array + * @default [] + */ + "aoOpenRows": [], + + /** + * Dictate the positioning of DataTables' control elements - see + * {@link DataTable.model.oInit.sDom}. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + * @type string + * @default null + */ + "sDom": null, + + /** + * Which type of pagination should be used. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + * @type string + * @default two_button + */ + "sPaginationType": "two_button", + + /** + * The state duration (for `stateSave`) in seconds. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + * @type int + * @default 0 + */ + "iStateDuration": 0, + + /** + * Array of callback functions for state saving. Each array element is an + * object with the following parameters: + * <ul> + * <li>function:fn - function to call. Takes two parameters, oSettings + * and the JSON string to save that has been thus far created. Returns + * a JSON string to be inserted into a json object + * (i.e. '"param": [ 0, 1, 2]')</li> + * <li>string:sName - name of callback</li> + * </ul> + * @type array + * @default [] + */ + "aoStateSave": [], + + /** + * Array of callback functions for state loading. Each array element is an + * object with the following parameters: + * <ul> + * <li>function:fn - function to call. Takes two parameters, oSettings + * and the object stored. May return false to cancel state loading</li> + * <li>string:sName - name of callback</li> + * </ul> + * @type array + * @default [] + */ + "aoStateLoad": [], + + /** + * State that was loaded. Useful for back reference + * @type object + * @default null + */ + "oLoadedState": null, + + /** + * Source url for AJAX data for the table. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + * @type string + * @default null + */ + "sAjaxSource": null, + + /** + * Property from a given object from which to read the table data from. This + * can be an empty string (when not server-side processing), in which case + * it is assumed an an array is given directly. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + * @type string + */ + "sAjaxDataProp": null, + + /** + * Note if draw should be blocked while getting data + * @type boolean + * @default true + */ + "bAjaxDataGet": true, + + /** + * The last jQuery XHR object that was used for server-side data gathering. + * This can be used for working with the XHR information in one of the + * callbacks + * @type object + * @default null + */ + "jqXHR": null, + + /** + * JSON returned from the server in the last Ajax request + * @type object + * @default undefined + */ + "json": undefined, + + /** + * Data submitted as part of the last Ajax request + * @type object + * @default undefined + */ + "oAjaxData": undefined, + + /** + * Function to get the server-side data. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + * @type function + */ + "fnServerData": null, + + /** + * Functions which are called prior to sending an Ajax request so extra + * parameters can easily be sent to the server + * @type array + * @default [] + */ + "aoServerParams": [], + + /** + * Send the XHR HTTP method - GET or POST (could be PUT or DELETE if + * required). + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + * @type string + */ + "sServerMethod": null, + + /** + * Format numbers for display. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + * @type function + */ + "fnFormatNumber": null, + + /** + * List of options that can be used for the user selectable length menu. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + * @type array + * @default [] + */ + "aLengthMenu": null, + + /** + * Counter for the draws that the table does. Also used as a tracker for + * server-side processing + * @type int + * @default 0 + */ + "iDraw": 0, + + /** + * Indicate if a redraw is being done - useful for Ajax + * @type boolean + * @default false + */ + "bDrawing": false, + + /** + * Draw index (iDraw) of the last error when parsing the returned data + * @type int + * @default -1 + */ + "iDrawError": -1, + + /** + * Paging display length + * @type int + * @default 10 + */ + "_iDisplayLength": 10, + + /** + * Paging start point - aiDisplay index + * @type int + * @default 0 + */ + "_iDisplayStart": 0, + + /** + * Server-side processing - number of records in the result set + * (i.e. before filtering), Use fnRecordsTotal rather than + * this property to get the value of the number of records, regardless of + * the server-side processing setting. + * @type int + * @default 0 + * @private + */ + "_iRecordsTotal": 0, + + /** + * Server-side processing - number of records in the current display set + * (i.e. after filtering). Use fnRecordsDisplay rather than + * this property to get the value of the number of records, regardless of + * the server-side processing setting. + * @type boolean + * @default 0 + * @private + */ + "_iRecordsDisplay": 0, + + /** + * Flag to indicate if jQuery UI marking and classes should be used. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + * @type boolean + */ + "bJUI": null, + + /** + * The classes to use for the table + * @type object + * @default {} + */ + "oClasses": {}, + + /** + * Flag attached to the settings object so you can check in the draw + * callback if filtering has been done in the draw. Deprecated in favour of + * events. + * @type boolean + * @default false + * @deprecated + */ + "bFiltered": false, + + /** + * Flag attached to the settings object so you can check in the draw + * callback if sorting has been done in the draw. Deprecated in favour of + * events. + * @type boolean + * @default false + * @deprecated + */ + "bSorted": false, + + /** + * Indicate that if multiple rows are in the header and there is more than + * one unique cell per column, if the top one (true) or bottom one (false) + * should be used for sorting / title by DataTables. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + * @type boolean + */ + "bSortCellsTop": null, + + /** + * Initialisation object that is used for the table + * @type object + * @default null + */ + "oInit": null, + + /** + * Destroy callback functions - for plug-ins to attach themselves to the + * destroy so they can clean up markup and events. + * @type array + * @default [] + */ + "aoDestroyCallback": [], + + + /** + * Get the number of records in the current record set, before filtering + * @type function + */ + "fnRecordsTotal": function () + { + return _fnDataSource( this ) == 'ssp' ? + this._iRecordsTotal * 1 : + this.aiDisplayMaster.length; + }, + + /** + * Get the number of records in the current record set, after filtering + * @type function + */ + "fnRecordsDisplay": function () + { + return _fnDataSource( this ) == 'ssp' ? + this._iRecordsDisplay * 1 : + this.aiDisplay.length; + }, + + /** + * Get the display end point - aiDisplay index + * @type function + */ + "fnDisplayEnd": function () + { + var + len = this._iDisplayLength, + start = this._iDisplayStart, + calc = start + len, + records = this.aiDisplay.length, + features = this.oFeatures, + paginate = features.bPaginate; + + if ( features.bServerSide ) { + return paginate === false || len === -1 ? + start + records : + Math.min( start+len, this._iRecordsDisplay ); + } + else { + return ! paginate || calc>records || len===-1 ? + records : + calc; + } + }, + + /** + * The DataTables object for this table + * @type object + * @default null + */ + "oInstance": null, + + /** + * Unique identifier for each instance of the DataTables object. If there + * is an ID on the table node, then it takes that value, otherwise an + * incrementing internal counter is used. + * @type string + * @default null + */ + "sInstance": null, + + /** + * tabindex attribute value that is added to DataTables control elements, allowing + * keyboard navigation of the table and its controls. + */ + "iTabIndex": 0, + + /** + * DIV container for the footer scrolling table if scrolling + */ + "nScrollHead": null, + + /** + * DIV container for the footer scrolling table if scrolling + */ + "nScrollFoot": null, + + /** + * Last applied sort + * @type array + * @default [] + */ + "aLastSort": [], + + /** + * Stored plug-in instances + * @type object + * @default {} + */ + "oPlugins": {} + }; + + /** + * Extension object for DataTables that is used to provide all extension + * options. + * + * Note that the `DataTable.ext` object is available through + * `jQuery.fn.dataTable.ext` where it may be accessed and manipulated. It is + * also aliased to `jQuery.fn.dataTableExt` for historic reasons. + * @namespace + * @extends DataTable.models.ext + */ + + + /** + * DataTables extensions + * + * This namespace acts as a collection area for plug-ins that can be used to + * extend DataTables capabilities. Indeed many of the build in methods + * use this method to provide their own capabilities (sorting methods for + * example). + * + * Note that this namespace is aliased to `jQuery.fn.dataTableExt` for legacy + * reasons + * + * @namespace + */ + DataTable.ext = _ext = { + /** + * Element class names + * + * @type object + * @default {} + */ + classes: {}, + + + /** + * Error reporting. + * + * How should DataTables report an error. Can take the value 'alert' or + * 'throw' + * + * @type string + * @default alert + */ + errMode: "alert", + + + /** + * Feature plug-ins. + * + * This is an array of objects which describe the feature plug-ins that are + * available to DataTables. These feature plug-ins are then available for + * use through the `dom` initialisation option. + * + * Each feature plug-in is described by an object which must have the + * following properties: + * + * * `fnInit` - function that is used to initialise the plug-in, + * * `cFeature` - a character so the feature can be enabled by the `dom` + * instillation option. This is case sensitive. + * + * The `fnInit` function has the following input parameters: + * + * 1. `{object}` DataTables settings object: see + * {@link DataTable.models.oSettings} + * + * And the following return is expected: + * + * * {node|null} The element which contains your feature. Note that the + * return may also be void if your plug-in does not require to inject any + * DOM elements into DataTables control (`dom`) - for example this might + * be useful when developing a plug-in which allows table control via + * keyboard entry + * + * @type array + * + * @example + * $.fn.dataTable.ext.features.push( { + * "fnInit": function( oSettings ) { + * return new TableTools( { "oDTSettings": oSettings } ); + * }, + * "cFeature": "T" + * } ); + */ + feature: [], + + + /** + * Row searching. + * + * This method of searching is complimentary to the default type based + * searching, and a lot more comprehensive as it allows you complete control + * over the searching logic. Each element in this array is a function + * (parameters described below) that is called for every row in the table, + * and your logic decides if it should be included in the searching data set + * or not. + * + * Searching functions have the following input parameters: + * + * 1. `{object}` DataTables settings object: see + * {@link DataTable.models.oSettings} + * 2. `{array|object}` Data for the row to be processed (same as the + * original format that was passed in as the data source, or an array + * from a DOM data source + * 3. `{int}` Row index ({@link DataTable.models.oSettings.aoData}), which + * can be useful to retrieve the `TR` element if you need DOM interaction. + * + * And the following return is expected: + * + * * {boolean} Include the row in the searched result set (true) or not + * (false) + * + * Note that as with the main search ability in DataTables, technically this + * is "filtering", since it is subtractive. However, for consistency in + * naming we call it searching here. + * + * @type array + * @default [] + * + * @example + * // The following example shows custom search being applied to the + * // fourth column (i.e. the data[3] index) based on two input values + * // from the end-user, matching the data in a certain range. + * $.fn.dataTable.ext.search.push( + * function( settings, data, dataIndex ) { + * var min = document.getElementById('min').value * 1; + * var max = document.getElementById('max').value * 1; + * var version = data[3] == "-" ? 0 : data[3]*1; + * + * if ( min == "" && max == "" ) { + * return true; + * } + * else if ( min == "" && version < max ) { + * return true; + * } + * else if ( min < version && "" == max ) { + * return true; + * } + * else if ( min < version && version < max ) { + * return true; + * } + * return false; + * } + * ); + */ + search: [], + + + /** + * Internal functions, exposed for used in plug-ins. + * + * Please note that you should not need to use the internal methods for + * anything other than a plug-in (and even then, try to avoid if possible). + * The internal function may change between releases. + * + * @type object + * @default {} + */ + internal: {}, + + + /** + * Legacy configuration options. Enable and disable legacy options that + * are available in DataTables. + * + * @type object + */ + legacy: { + /** + * Enable / disable DataTables 1.9 compatible server-side processing + * requests + * + * @type boolean + * @default null + */ + ajax: null + }, + + + /** + * Pagination plug-in methods. + * + * Each entry in this object is a function and defines which buttons should + * be shown by the pagination rendering method that is used for the table: + * {@link DataTable.ext.renderer.pageButton}. The renderer addresses how the + * buttons are displayed in the document, while the functions here tell it + * what buttons to display. This is done by returning an array of button + * descriptions (what each button will do). + * + * Pagination types (the four built in options and any additional plug-in + * options defined here) can be used through the `paginationType` + * initialisation parameter. + * + * The functions defined take two parameters: + * + * 1. `{int} page` The current page index + * 2. `{int} pages` The number of pages in the table + * + * Each function is expected to return an array where each element of the + * array can be one of: + * + * * `first` - Jump to first page when activated + * * `last` - Jump to last page when activated + * * `previous` - Show previous page when activated + * * `next` - Show next page when activated + * * `{int}` - Show page of the index given + * * `{array}` - A nested array containing the above elements to add a + * containing 'DIV' element (might be useful for styling). + * + * Note that DataTables v1.9- used this object slightly differently whereby + * an object with two functions would be defined for each plug-in. That + * ability is still supported by DataTables 1.10+ to provide backwards + * compatibility, but this option of use is now decremented and no longer + * documented in DataTables 1.10+. + * + * @type object + * @default {} + * + * @example + * // Show previous, next and current page buttons only + * $.fn.dataTableExt.oPagination.current = function ( page, pages ) { + * return [ 'previous', page, 'next' ]; + * }; + */ + pager: {}, + + + renderer: { + pageButton: {}, + header: {} + }, + + + /** + * Ordering plug-ins - custom data source + * + * The extension options for ordering of data available here is complimentary + * to the default type based ordering that DataTables typically uses. It + * allows much greater control over the the data that is being used to + * order a column, but is necessarily therefore more complex. + * + * This type of ordering is useful if you want to do ordering based on data + * live from the DOM (for example the contents of an 'input' element) rather + * than just the static string that DataTables knows of. + * + * The way these plug-ins work is that you create an array of the values you + * wish to be ordering for the column in question and then return that + * array. The data in the array much be in the index order of the rows in + * the table (not the currently ordering order!). Which order data gathering + * function is run here depends on the `dt-init columns.orderDataType` + * parameter that is used for the column (if any). + * + * The functions defined take two parameters: + * + * 1. `{object}` DataTables settings object: see + * {@link DataTable.models.oSettings} + * 2. `{int}` Target column index + * + * Each function is expected to return an array: + * + * * `{array}` Data for the column to be ordering upon + * + * @type array + * + * @example + * // Ordering using `input` node values + * $.fn.dataTable.ext.order['dom-text'] = function ( settings, col ) + * { + * return this.api().column( col, {order:'index'} ).nodes().map( function ( td, i ) { + * return $('input', td).val(); + * } ); + * } + */ + order: {}, + + + /** + * Type based plug-ins. + * + * Each column in DataTables has a type assigned to it, either by automatic + * detection or by direct assignment using the `type` option for the column. + * The type of a column will effect how it is ordering and search (plug-ins + * can also make use of the column type if required). + * + * @namespace + */ + type: { + /** + * Type detection functions. + * + * The functions defined in this object are used to automatically detect + * a column's type, making initialisation of DataTables super easy, even + * when complex data is in the table. + * + * The functions defined take two parameters: + * + * 1. `{*}` Data from the column cell to be analysed + * 2. `{settings}` DataTables settings object. This can be used to + * perform context specific type detection - for example detection + * based on language settings such as using a comma for a decimal + * place. Generally speaking the options from the settings will not + * be required + * + * Each function is expected to return: + * + * * `{string|null}` Data type detected, or null if unknown (and thus + * pass it on to the other type detection functions. + * + * @type array + * + * @example + * // Currency type detection plug-in: + * $.fn.dataTable.ext.type.detect.push( + * function ( data, settings ) { + * // Check the numeric part + * if ( ! $.isNumeric( data.substring(1) ) ) { + * return null; + * } + * + * // Check prefixed by currency + * if ( data.charAt(0) == '$' || data.charAt(0) == '£' ) { + * return 'currency'; + * } + * return null; + * } + * ); + */ + detect: [], + + + /** + * Type based search formatting. + * + * The type based searching functions can be used to pre-format the + * data to be search on. For example, it can be used to strip HTML + * tags or to de-format telephone numbers for numeric only searching. + * + * Note that is a search is not defined for a column of a given type, + * no search formatting will be performed. + * + * Pre-processing of searching data plug-ins - When you assign the sType + * for a column (or have it automatically detected for you by DataTables + * or a type detection plug-in), you will typically be using this for + * custom sorting, but it can also be used to provide custom searching + * by allowing you to pre-processing the data and returning the data in + * the format that should be searched upon. This is done by adding + * functions this object with a parameter name which matches the sType + * for that target column. This is the corollary of <i>afnSortData</i> + * for searching data. + * + * The functions defined take a single parameter: + * + * 1. `{*}` Data from the column cell to be prepared for searching + * + * Each function is expected to return: + * + * * `{string|null}` Formatted string that will be used for the searching. + * + * @type object + * @default {} + * + * @example + * $.fn.dataTable.ext.type.search['title-numeric'] = function ( d ) { + * return d.replace(/\n/g," ").replace( /<.*?>/g, "" ); + * } + */ + search: {}, + + + /** + * Type based ordering. + * + * The column type tells DataTables what ordering to apply to the table + * when a column is sorted upon. The order for each type that is defined, + * is defined by the functions available in this object. + * + * Each ordering option can be described by three properties added to + * this object: + * + * * `{type}-pre` - Pre-formatting function + * * `{type}-asc` - Ascending order function + * * `{type}-desc` - Descending order function + * + * All three can be used together, only `{type}-pre` or only + * `{type}-asc` and `{type}-desc` together. It is generally recommended + * that only `{type}-pre` is used, as this provides the optimal + * implementation in terms of speed, although the others are provided + * for compatibility with existing Javascript sort functions. + * + * `{type}-pre`: Functions defined take a single parameter: + * + * 1. `{*}` Data from the column cell to be prepared for ordering + * + * And return: + * + * * `{*}` Data to be sorted upon + * + * `{type}-asc` and `{type}-desc`: Functions are typical Javascript sort + * functions, taking two parameters: + * + * 1. `{*}` Data to compare to the second parameter + * 2. `{*}` Data to compare to the first parameter + * + * And returning: + * + * * `{*}` Ordering match: <0 if first parameter should be sorted lower + * than the second parameter, ===0 if the two parameters are equal and + * >0 if the first parameter should be sorted height than the second + * parameter. + * + * @type object + * @default {} + * + * @example + * // Numeric ordering of formatted numbers with a pre-formatter + * $.extend( $.fn.dataTable.ext.type.order, { + * "string-pre": function(x) { + * a = (a === "-" || a === "") ? 0 : a.replace( /[^\d\-\.]/g, "" ); + * return parseFloat( a ); + * } + * } ); + * + * @example + * // Case-sensitive string ordering, with no pre-formatting method + * $.extend( $.fn.dataTable.ext.order, { + * "string-case-asc": function(x,y) { + * return ((x < y) ? -1 : ((x > y) ? 1 : 0)); + * }, + * "string-case-desc": function(x,y) { + * return ((x < y) ? 1 : ((x > y) ? -1 : 0)); + * } + * } ); + */ + order: {} + }, + + /** + * Unique DataTables instance counter + * + * @type int + * @private + */ + _unique: 0, + + + // + // Depreciated + // The following properties are retained for backwards compatiblity only. + // The should not be used in new projects and will be removed in a future + // version + // + + /** + * Version check function. + * @type function + * @depreciated Since 1.10 + */ + fnVersionCheck: DataTable.fnVersionCheck, + + + /** + * Index for what 'this' index API functions should use + * @type int + * @deprecated Since v1.10 + */ + iApiIndex: 0, + + + /** + * jQuery UI class container + * @type object + * @deprecated Since v1.10 + */ + oJUIClasses: {}, + + + /** + * Software version + * @type string + * @deprecated Since v1.10 + */ + sVersion: DataTable.version + }; + + + // + // Backwards compatibility. Alias to pre 1.10 Hungarian notation counter parts + // + $.extend( _ext, { + afnFiltering: _ext.search, + aTypes: _ext.type.detect, + ofnSearch: _ext.type.search, + oSort: _ext.type.order, + afnSortData: _ext.order, + aoFeatures: _ext.feature, + oApi: _ext.internal, + oStdClasses: _ext.classes, + oPagination: _ext.pager + } ); + + + $.extend( DataTable.ext.classes, { + "sTable": "dataTable", + "sNoFooter": "no-footer", + + /* Paging buttons */ + "sPageButton": "paginate_button", + "sPageButtonActive": "current", + "sPageButtonDisabled": "disabled", + + /* Striping classes */ + "sStripeOdd": "odd", + "sStripeEven": "even", + + /* Empty row */ + "sRowEmpty": "dataTables_empty", + + /* Features */ + "sWrapper": "dataTables_wrapper", + "sFilter": "dataTables_filter", + "sInfo": "dataTables_info", + "sPaging": "dataTables_paginate paging_", /* Note that the type is postfixed */ + "sLength": "dataTables_length", + "sProcessing": "dataTables_processing", + + /* Sorting */ + "sSortAsc": "sorting_asc", + "sSortDesc": "sorting_desc", + "sSortable": "sorting", /* Sortable in both directions */ + "sSortableAsc": "sorting_asc_disabled", + "sSortableDesc": "sorting_desc_disabled", + "sSortableNone": "sorting_disabled", + "sSortColumn": "sorting_", /* Note that an int is postfixed for the sorting order */ + + /* Filtering */ + "sFilterInput": "", + + /* Page length */ + "sLengthSelect": "", + + /* Scrolling */ + "sScrollWrapper": "dataTables_scroll", + "sScrollHead": "dataTables_scrollHead", + "sScrollHeadInner": "dataTables_scrollHeadInner", + "sScrollBody": "dataTables_scrollBody", + "sScrollFoot": "dataTables_scrollFoot", + "sScrollFootInner": "dataTables_scrollFootInner", + + /* Misc */ + "sHeaderTH": "", + "sFooterTH": "", + + // Deprecated + "sSortJUIAsc": "", + "sSortJUIDesc": "", + "sSortJUI": "", + "sSortJUIAscAllowed": "", + "sSortJUIDescAllowed": "", + "sSortJUIWrapper": "", + "sSortIcon": "", + "sJUIHeader": "", + "sJUIFooter": "" + } ); + + + (function() { + + // Reused strings for better compression. Closure compiler appears to have a + // weird edge case where it is trying to expand strings rather than use the + // variable version. This results in about 200 bytes being added, for very + // little preference benefit since it this run on script load only. + var _empty = ''; + _empty = ''; + + var _stateDefault = _empty + 'ui-state-default'; + var _sortIcon = _empty + 'css_right ui-icon ui-icon-'; + var _headerFooter = _empty + 'fg-toolbar ui-toolbar ui-widget-header ui-helper-clearfix'; + + $.extend( DataTable.ext.oJUIClasses, DataTable.ext.classes, { + /* Full numbers paging buttons */ + "sPageButton": "fg-button ui-button "+_stateDefault, + "sPageButtonActive": "ui-state-disabled", + "sPageButtonDisabled": "ui-state-disabled", + + /* Features */ + "sPaging": "dataTables_paginate fg-buttonset ui-buttonset fg-buttonset-multi "+ + "ui-buttonset-multi paging_", /* Note that the type is postfixed */ + + /* Sorting */ + "sSortAsc": _stateDefault+" sorting_asc", + "sSortDesc": _stateDefault+" sorting_desc", + "sSortable": _stateDefault+" sorting", + "sSortableAsc": _stateDefault+" sorting_asc_disabled", + "sSortableDesc": _stateDefault+" sorting_desc_disabled", + "sSortableNone": _stateDefault+" sorting_disabled", + "sSortJUIAsc": _sortIcon+"triangle-1-n", + "sSortJUIDesc": _sortIcon+"triangle-1-s", + "sSortJUI": _sortIcon+"carat-2-n-s", + "sSortJUIAscAllowed": _sortIcon+"carat-1-n", + "sSortJUIDescAllowed": _sortIcon+"carat-1-s", + "sSortJUIWrapper": "DataTables_sort_wrapper", + "sSortIcon": "DataTables_sort_icon", + + /* Scrolling */ + "sScrollHead": "dataTables_scrollHead "+_stateDefault, + "sScrollFoot": "dataTables_scrollFoot "+_stateDefault, + + /* Misc */ + "sHeaderTH": _stateDefault, + "sFooterTH": _stateDefault, + "sJUIHeader": _headerFooter+" ui-corner-tl ui-corner-tr", + "sJUIFooter": _headerFooter+" ui-corner-bl ui-corner-br" + } ); + + }()); + + + + var extPagination = DataTable.ext.pager; + + function _numbers ( page, pages ) { + var + numbers = [], + buttons = extPagination.numbers_length, + half = Math.floor( buttons / 2 ), + i = 1; + + if ( pages <= buttons ) { + numbers = _range( 0, pages ); + } + else if ( page <= half ) { + numbers = _range( 0, buttons-2 ); + numbers.push( 'ellipsis' ); + numbers.push( pages-1 ); + } + else if ( page >= pages - 1 - half ) { + numbers = _range( pages-(buttons-2), pages ); + numbers.splice( 0, 0, 'ellipsis' ); // no unshift in ie6 + numbers.splice( 0, 0, 0 ); + } + else { + numbers = _range( page-1, page+2 ); + numbers.push( 'ellipsis' ); + numbers.push( pages-1 ); + numbers.splice( 0, 0, 'ellipsis' ); + numbers.splice( 0, 0, 0 ); + } + + numbers.DT_el = 'span'; + return numbers; + } + + + $.extend( extPagination, { + simple: function ( page, pages ) { + return [ 'previous', 'next' ]; + }, + + full: function ( page, pages ) { + return [ 'first', 'previous', 'next', 'last' ]; + }, + + simple_numbers: function ( page, pages ) { + return [ 'previous', _numbers(page, pages), 'next' ]; + }, + + full_numbers: function ( page, pages ) { + return [ 'first', 'previous', _numbers(page, pages), 'next', 'last' ]; + }, + + // For testing and plug-ins to use + _numbers: _numbers, + numbers_length: 7 + } ); + + + $.extend( true, DataTable.ext.renderer, { + pageButton: { + _: function ( settings, host, idx, buttons, page, pages ) { + var classes = settings.oClasses; + var lang = settings.oLanguage.oPaginate; + var btnDisplay, btnClass, counter=0; + + var attach = function( container, buttons ) { + var i, ien, node, button; + var clickHandler = function ( e ) { + _fnPageChange( settings, e.data.action, true ); + }; + + for ( i=0, ien=buttons.length ; i<ien ; i++ ) { + button = buttons[i]; + + if ( $.isArray( button ) ) { + var inner = $( '<'+(button.DT_el || 'div')+'/>' ) + .appendTo( container ); + attach( inner, button ); + } + else { + btnDisplay = ''; + btnClass = ''; + + switch ( button ) { + case 'ellipsis': + container.append('<span>…</span>'); + break; + + case 'first': + btnDisplay = lang.sFirst; + btnClass = button + (page > 0 ? + '' : ' '+classes.sPageButtonDisabled); + break; + + case 'previous': + btnDisplay = lang.sPrevious; + btnClass = button + (page > 0 ? + '' : ' '+classes.sPageButtonDisabled); + break; + + case 'next': + btnDisplay = lang.sNext; + btnClass = button + (page < pages-1 ? + '' : ' '+classes.sPageButtonDisabled); + break; + + case 'last': + btnDisplay = lang.sLast; + btnClass = button + (page < pages-1 ? + '' : ' '+classes.sPageButtonDisabled); + break; + + default: + btnDisplay = button + 1; + btnClass = page === button ? + classes.sPageButtonActive : ''; + break; + } + + if ( btnDisplay ) { + node = $('<a>', { + 'class': classes.sPageButton+' '+btnClass, + 'aria-controls': settings.sTableId, + 'data-dt-idx': counter, + 'tabindex': settings.iTabIndex, + 'id': idx === 0 && typeof button === 'string' ? + settings.sTableId +'_'+ button : + null + } ) + .html( btnDisplay ) + .appendTo( container ); + + _fnBindAction( + node, {action: button}, clickHandler + ); + + counter++; + } + } + } + }; + + // Because this approach is destroying and recreating the paging + // elements, focus is lost on the select button which is bad for + // accessibility. So we want to restore focus once the draw has + // completed + var activeEl = $(document.activeElement).data('dt-idx'); + + attach( $(host).empty(), buttons ); + + if ( activeEl !== null ) { + $(host).find( '[data-dt-idx='+activeEl+']' ).focus(); + } + } + } + } ); + + + + var __numericReplace = function ( d, decimalPlace, re1, re2 ) { + if ( !d || d === '-' ) { + return -Infinity; + } + + // If a decimal place other than `.` is used, it needs to be given to the + // function so we can detect it and replace with a `.` which is the only + // decimal place Javascript recognises - it is not locale aware. + if ( decimalPlace ) { + d = _numToDecimal( d, decimalPlace ); + } + + if ( d.replace ) { + if ( re1 ) { + d = d.replace( re1, '' ); + } + + if ( re2 ) { + d = d.replace( re2, '' ); + } + } + + return d * 1; + }; + + + // Add the numeric 'deformatting' functions for sorting. This is done in a + // function to provide an easy ability for the language options to add + // additional methods if a non-period decimal place is used. + function _addNumericSort ( decimalPlace ) { + $.each( + { + // Plain numbers + "num": function ( d ) { + return __numericReplace( d, decimalPlace ); + }, + + // Formatted numbers + "num-fmt": function ( d ) { + return __numericReplace( d, decimalPlace, _re_formatted_numeric ); + }, + + // HTML numeric + "html-num": function ( d ) { + return __numericReplace( d, decimalPlace, _re_html ); + }, + + // HTML numeric, formatted + "html-num-fmt": function ( d ) { + return __numericReplace( d, decimalPlace, _re_html, _re_formatted_numeric ); + } + }, + function ( key, fn ) { + _ext.type.order[ key+decimalPlace+'-pre' ] = fn; + } + ); + } + + + // Default sort methods + $.extend( _ext.type.order, { + // Dates + "date-pre": function ( d ) { + return Date.parse( d ) || 0; + }, + + // html + "html-pre": function ( a ) { + return ! a ? + '' : + a.replace ? + a.replace( /<.*?>/g, "" ).toLowerCase() : + a+''; + }, + + // string + "string-pre": function ( a ) { + return typeof a === 'string' ? + a.toLowerCase() : + ! a || ! a.toString ? + '' : + a.toString(); + }, + + // string-asc and -desc are retained only for compatibility with the old + // sort methods + "string-asc": function ( x, y ) { + return ((x < y) ? -1 : ((x > y) ? 1 : 0)); + }, + + "string-desc": function ( x, y ) { + return ((x < y) ? 1 : ((x > y) ? -1 : 0)); + } + } ); + + + // Numeric sorting types - order doesn't matter here + _addNumericSort( '' ); + + + // Built in type detection. See model.ext.aTypes for information about + // what is required from this methods. + $.extend( DataTable.ext.type.detect, [ + // Plain numbers - first since V8 detects some plain numbers as dates + // e.g. Date.parse('55') (but not all, e.g. Date.parse('22')...). + function ( d, settings ) + { + var decimal = settings.oLanguage.sDecimal; + return _isNumber( d, decimal ) ? 'num'+decimal : null; + }, + + // Dates (only those recognised by the browser's Date.parse) + function ( d, settings ) + { + // V8 will remove any unknown characters at the start of the expression, + // leading to false matches such as `$245.12` being a valid date. See + // forum thread 18941 for detail. + if ( d && ! _re_date_start.test(d) ) { + return null; + } + var parsed = Date.parse(d); + return (parsed !== null && !isNaN(parsed)) || _empty(d) ? 'date' : null; + }, + + // Formatted numbers + function ( d, settings ) + { + var decimal = settings.oLanguage.sDecimal; + return _isNumber( d, decimal, true ) ? 'num-fmt'+decimal : null; + }, + + // HTML numeric + function ( d, settings ) + { + var decimal = settings.oLanguage.sDecimal; + return _htmlNumeric( d, decimal ) ? 'html-num'+decimal : null; + }, + + // HTML numeric, formatted + function ( d, settings ) + { + var decimal = settings.oLanguage.sDecimal; + return _htmlNumeric( d, decimal, true ) ? 'html-num-fmt'+decimal : null; + }, + + // HTML (this is strict checking - there must be html) + function ( d, settings ) + { + return _empty( d ) || (typeof d === 'string' && d.indexOf('<') !== -1) ? + 'html' : null; + } + ] ); + + + + // Filter formatting functions. See model.ext.ofnSearch for information about + // what is required from these methods. + + + $.extend( DataTable.ext.type.search, { + html: function ( data ) { + return _empty(data) ? + '' : + typeof data === 'string' ? + data + .replace( _re_new_lines, " " ) + .replace( _re_html, "" ) : + ''; + }, + + string: function ( data ) { + return _empty(data) ? + '' : + typeof data === 'string' ? + data.replace( _re_new_lines, " " ) : + data; + } + } ); + + + + $.extend( true, DataTable.ext.renderer, { + header: { + _: function ( settings, cell, column, classes ) { + // No additional mark-up required + // Attach a sort listener to update on sort - note that using the + // `DT` namespace will allow the event to be removed automatically + // on destroy, while the `dt` namespaced event is the one we are + // listening for + $(settings.nTable).on( 'order.dt.DT', function ( e, settings, sorting, columns ) { + var colIdx = column.idx; + + cell + .removeClass( + column.sSortingClass +' '+ + classes.sSortAsc +' '+ + classes.sSortDesc + ) + .addClass( columns[ colIdx ] == 'asc' ? + classes.sSortAsc : columns[ colIdx ] == 'desc' ? + classes.sSortDesc : + column.sSortingClass + ); + } ); + }, + + jqueryui: function ( settings, cell, column, classes ) { + var colIdx = column.idx; + + $('<div/>') + .addClass( classes.sSortJUIWrapper ) + .append( cell.contents() ) + .append( $('<span/>') + .addClass( classes.sSortIcon+' '+column.sSortingClassJUI ) + ) + .appendTo( cell ); + + // Attach a sort listener to update on sort + $(settings.nTable).on( 'order.dt.DT', function ( e, settings, sorting, columns ) { + cell + .removeClass( classes.sSortAsc +" "+classes.sSortDesc ) + .addClass( columns[ colIdx ] == 'asc' ? + classes.sSortAsc : columns[ colIdx ] == 'desc' ? + classes.sSortDesc : + column.sSortingClass + ); + + cell + .find( 'span.'+classes.sSortIcon ) + .removeClass( + classes.sSortJUIAsc +" "+ + classes.sSortJUIDesc +" "+ + classes.sSortJUI +" "+ + classes.sSortJUIAscAllowed +" "+ + classes.sSortJUIDescAllowed + ) + .addClass( columns[ colIdx ] == 'asc' ? + classes.sSortJUIAsc : columns[ colIdx ] == 'desc' ? + classes.sSortJUIDesc : + column.sSortingClassJUI + ); + } ); + } + } + } ); + + /* + * Public helper functions. These aren't used internally by DataTables, or + * called by any of the options passed into DataTables, but they can be used + * externally by developers working with DataTables. They are helper functions + * to make working with DataTables a little bit easier. + */ + + /** + * Helpers for `columns.render`. + * + * The options defined here can be used with the `columns.render` initialisation + * option to provide a display renderer. The following functions are defined: + * + * * `number` - Will format numeric data (defined by `columns.data`) for + * display, retaining the original unformatted data for sorting and filtering. + * It takes 4 parameters: + * * `string` - Thousands grouping separator + * * `string` - Decimal point indicator + * * `integer` - Number of decimal points to show + * * `string` (optional) - Prefix. + * + * @example + * // Column definition using the number renderer + * { + * data: "salary", + * render: $.fn.dataTable.render.number( '\'', '.', 0, '$' ) + * } + * + * @namespace + */ + DataTable.render = { + number: function ( thousands, decimal, precision, prefix ) { + return { + display: function ( d ) { + d = parseFloat( d ); + var intPart = parseInt( d, 10 ); + var floatPart = precision ? + (decimal+(d - intPart).toFixed( precision )).substring( 2 ): + ''; + + return (prefix||'') + + intPart.toString().replace( + /\B(?=(\d{3})+(?!\d))/g, thousands + ) + + floatPart; + } + }; + } + }; + + + /* + * This is really a good bit rubbish this method of exposing the internal methods + * publicly... - To be fixed in 2.0 using methods on the prototype + */ + + + /** + * Create a wrapper function for exporting an internal functions to an external API. + * @param {string} fn API function name + * @returns {function} wrapped function + * @memberof DataTable#internal + */ + function _fnExternApiFunc (fn) + { + return function() { + var args = [_fnSettingsFromNode( this[DataTable.ext.iApiIndex] )].concat( + Array.prototype.slice.call(arguments) + ); + return DataTable.ext.internal[fn].apply( this, args ); + }; + } + + + /** + * Reference to internal functions for use by plug-in developers. Note that + * these methods are references to internal functions and are considered to be + * private. If you use these methods, be aware that they are liable to change + * between versions. + * @namespace + */ + $.extend( DataTable.ext.internal, { + _fnExternApiFunc: _fnExternApiFunc, + _fnBuildAjax: _fnBuildAjax, + _fnAjaxUpdate: _fnAjaxUpdate, + _fnAjaxParameters: _fnAjaxParameters, + _fnAjaxUpdateDraw: _fnAjaxUpdateDraw, + _fnAjaxDataSrc: _fnAjaxDataSrc, + _fnAddColumn: _fnAddColumn, + _fnColumnOptions: _fnColumnOptions, + _fnAdjustColumnSizing: _fnAdjustColumnSizing, + _fnVisibleToColumnIndex: _fnVisibleToColumnIndex, + _fnColumnIndexToVisible: _fnColumnIndexToVisible, + _fnVisbleColumns: _fnVisbleColumns, + _fnGetColumns: _fnGetColumns, + _fnColumnTypes: _fnColumnTypes, + _fnApplyColumnDefs: _fnApplyColumnDefs, + _fnHungarianMap: _fnHungarianMap, + _fnCamelToHungarian: _fnCamelToHungarian, + _fnLanguageCompat: _fnLanguageCompat, + _fnBrowserDetect: _fnBrowserDetect, + _fnAddData: _fnAddData, + _fnAddTr: _fnAddTr, + _fnNodeToDataIndex: _fnNodeToDataIndex, + _fnNodeToColumnIndex: _fnNodeToColumnIndex, + _fnGetCellData: _fnGetCellData, + _fnSetCellData: _fnSetCellData, + _fnSplitObjNotation: _fnSplitObjNotation, + _fnGetObjectDataFn: _fnGetObjectDataFn, + _fnSetObjectDataFn: _fnSetObjectDataFn, + _fnGetDataMaster: _fnGetDataMaster, + _fnClearTable: _fnClearTable, + _fnDeleteIndex: _fnDeleteIndex, + _fnInvalidateRow: _fnInvalidateRow, + _fnGetRowElements: _fnGetRowElements, + _fnCreateTr: _fnCreateTr, + _fnBuildHead: _fnBuildHead, + _fnDrawHead: _fnDrawHead, + _fnDraw: _fnDraw, + _fnReDraw: _fnReDraw, + _fnAddOptionsHtml: _fnAddOptionsHtml, + _fnDetectHeader: _fnDetectHeader, + _fnGetUniqueThs: _fnGetUniqueThs, + _fnFeatureHtmlFilter: _fnFeatureHtmlFilter, + _fnFilterComplete: _fnFilterComplete, + _fnFilterCustom: _fnFilterCustom, + _fnFilterColumn: _fnFilterColumn, + _fnFilter: _fnFilter, + _fnFilterCreateSearch: _fnFilterCreateSearch, + _fnEscapeRegex: _fnEscapeRegex, + _fnFilterData: _fnFilterData, + _fnFeatureHtmlInfo: _fnFeatureHtmlInfo, + _fnUpdateInfo: _fnUpdateInfo, + _fnInfoMacros: _fnInfoMacros, + _fnInitialise: _fnInitialise, + _fnInitComplete: _fnInitComplete, + _fnLengthChange: _fnLengthChange, + _fnFeatureHtmlLength: _fnFeatureHtmlLength, + _fnFeatureHtmlPaginate: _fnFeatureHtmlPaginate, + _fnPageChange: _fnPageChange, + _fnFeatureHtmlProcessing: _fnFeatureHtmlProcessing, + _fnProcessingDisplay: _fnProcessingDisplay, + _fnFeatureHtmlTable: _fnFeatureHtmlTable, + _fnScrollDraw: _fnScrollDraw, + _fnApplyToChildren: _fnApplyToChildren, + _fnCalculateColumnWidths: _fnCalculateColumnWidths, + _fnThrottle: _fnThrottle, + _fnConvertToWidth: _fnConvertToWidth, + _fnScrollingWidthAdjust: _fnScrollingWidthAdjust, + _fnGetWidestNode: _fnGetWidestNode, + _fnGetMaxLenString: _fnGetMaxLenString, + _fnStringToCss: _fnStringToCss, + _fnScrollBarWidth: _fnScrollBarWidth, + _fnSortFlatten: _fnSortFlatten, + _fnSort: _fnSort, + _fnSortAria: _fnSortAria, + _fnSortListener: _fnSortListener, + _fnSortAttachListener: _fnSortAttachListener, + _fnSortingClasses: _fnSortingClasses, + _fnSortData: _fnSortData, + _fnSaveState: _fnSaveState, + _fnLoadState: _fnLoadState, + _fnSettingsFromNode: _fnSettingsFromNode, + _fnLog: _fnLog, + _fnMap: _fnMap, + _fnBindAction: _fnBindAction, + _fnCallbackReg: _fnCallbackReg, + _fnCallbackFire: _fnCallbackFire, + _fnLengthOverflow: _fnLengthOverflow, + _fnRenderer: _fnRenderer, + _fnDataSource: _fnDataSource, + _fnRowAttributes: _fnRowAttributes, + _fnCalculateEnd: function () {} // Used by a lot of plug-ins, but redundant + // in 1.10, so this dead-end function is + // added to prevent errors + } ); + + + // jQuery access + $.fn.dataTable = DataTable; + + // Legacy aliases + $.fn.dataTableSettings = DataTable.settings; + $.fn.dataTableExt = DataTable.ext; + + // With a capital `D` we return a DataTables API instance rather than a + // jQuery object + $.fn.DataTable = function ( opts ) { + return $(this).dataTable( opts ).api(); + }; + + // All properties that are available to $.fn.dataTable should also be + // available on $.fn.DataTable + $.each( DataTable, function ( prop, val ) { + $.fn.DataTable[ prop ] = val; + } ); + + + // Information about events fired by DataTables - for documentation. + /** + * Draw event, fired whenever the table is redrawn on the page, at the same + * point as fnDrawCallback. This may be useful for binding events or + * performing calculations when the table is altered at all. + * @name DataTable#draw.dt + * @event + * @param {event} e jQuery event object + * @param {object} o DataTables settings object {@link DataTable.models.oSettings} + */ + + /** + * Search event, fired when the searching applied to the table (using the + * built-in global search, or column filters) is altered. + * @name DataTable#search.dt + * @event + * @param {event} e jQuery event object + * @param {object} o DataTables settings object {@link DataTable.models.oSettings} + */ + + /** + * Page change event, fired when the paging of the table is altered. + * @name DataTable#page.dt + * @event + * @param {event} e jQuery event object + * @param {object} o DataTables settings object {@link DataTable.models.oSettings} + */ + + /** + * Order event, fired when the ordering applied to the table is altered. + * @name DataTable#order.dt + * @event + * @param {event} e jQuery event object + * @param {object} o DataTables settings object {@link DataTable.models.oSettings} + */ + + /** + * DataTables initialisation complete event, fired when the table is fully + * drawn, including Ajax data loaded, if Ajax data is required. + * @name DataTable#init.dt + * @event + * @param {event} e jQuery event object + * @param {object} oSettings DataTables settings object + * @param {object} json The JSON object request from the server - only + * present if client-side Ajax sourced data is used</li></ol> + */ + + /** + * State save event, fired when the table has changed state a new state save + * is required. This event allows modification of the state saving object + * prior to actually doing the save, including addition or other state + * properties (for plug-ins) or modification of a DataTables core property. + * @name DataTable#stateSaveParams.dt + * @event + * @param {event} e jQuery event object + * @param {object} oSettings DataTables settings object + * @param {object} json The state information to be saved + */ + + /** + * State load event, fired when the table is loading state from the stored + * data, but prior to the settings object being modified by the saved state + * - allowing modification of the saved state is required or loading of + * state for a plug-in. + * @name DataTable#stateLoadParams.dt + * @event + * @param {event} e jQuery event object + * @param {object} oSettings DataTables settings object + * @param {object} json The saved state information + */ + + /** + * State loaded event, fired when state has been loaded from stored data and + * the settings object has been modified by the loaded data. + * @name DataTable#stateLoaded.dt + * @event + * @param {event} e jQuery event object + * @param {object} oSettings DataTables settings object + * @param {object} json The saved state information + */ + + /** + * Processing event, fired when DataTables is doing some kind of processing + * (be it, order, searcg or anything else). It can be used to indicate to + * the end user that there is something happening, or that something has + * finished. + * @name DataTable#processing.dt + * @event + * @param {event} e jQuery event object + * @param {object} oSettings DataTables settings object + * @param {boolean} bShow Flag for if DataTables is doing processing or not + */ + + /** + * Ajax (XHR) event, fired whenever an Ajax request is completed from a + * request to made to the server for new data. This event is called before + * DataTables processed the returned data, so it can also be used to pre- + * process the data returned from the server, if needed. + * + * Note that this trigger is called in `fnServerData`, if you override + * `fnServerData` and which to use this event, you need to trigger it in you + * success function. + * @name DataTable#xhr.dt + * @event + * @param {event} e jQuery event object + * @param {object} o DataTables settings object {@link DataTable.models.oSettings} + * @param {object} json JSON returned from the server + * + * @example + * // Use a custom property returned from the server in another DOM element + * $('#table').dataTable().on('xhr.dt', function (e, settings, json) { + * $('#status').html( json.status ); + * } ); + * + * @example + * // Pre-process the data returned from the server + * $('#table').dataTable().on('xhr.dt', function (e, settings, json) { + * for ( var i=0, ien=json.aaData.length ; i<ien ; i++ ) { + * json.aaData[i].sum = json.aaData[i].one + json.aaData[i].two; + * } + * // Note no return - manipulate the data directly in the JSON object. + * } ); + */ + + /** + * Destroy event, fired when the DataTable is destroyed by calling fnDestroy + * or passing the bDestroy:true parameter in the initialisation object. This + * can be used to remove bound events, added DOM nodes, etc. + * @name DataTable#destroy.dt + * @event + * @param {event} e jQuery event object + * @param {object} o DataTables settings object {@link DataTable.models.oSettings} + */ + + /** + * Page length change event, fired when number of records to show on each + * page (the length) is changed. + * @name DataTable#length.dt + * @event + * @param {event} e jQuery event object + * @param {object} o DataTables settings object {@link DataTable.models.oSettings} + * @param {integer} len New length + */ + + /** + * Column sizing has changed. + * @name DataTable#column-sizing.dt + * @event + * @param {event} e jQuery event object + * @param {object} o DataTables settings object {@link DataTable.models.oSettings} + */ + + /** + * Column visibility has changed. + * @name DataTable#column-visibility.dt + * @event + * @param {event} e jQuery event object + * @param {object} o DataTables settings object {@link DataTable.models.oSettings} + * @param {int} column Column index + * @param {bool} vis `false` if column now hidden, or `true` if visible + */ + + return $.fn.dataTable; +})); + +}(window, document)); + diff --git a/snf-admin-app/synnefo_admin/admin/static/js/jquery.js b/snf-admin-app/synnefo_admin/admin/static/js/jquery.js new file mode 100644 index 0000000000000000000000000000000000000000..73f33fb3aa529308d1f3f2f4fc253c4abed95374 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/js/jquery.js @@ -0,0 +1,4 @@ +/*! jQuery v1.11.0 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k="".trim,l={},m="1.11.0",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(n.isPlainObject(c)||(b=n.isArray(c)))?(b?(b=!1,f=a&&n.isArray(a)?a:[]):f=a&&n.isPlainObject(a)?a:{},g[d]=n.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray||function(a){return"array"===n.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){return a-parseFloat(a)>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==n.type(a)||a.nodeType||n.isWindow(a))return!1;try{if(a.constructor&&!j.call(a,"constructor")&&!j.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(l.ownLast)for(b in a)return j.call(a,b);for(b in a);return void 0===b||j.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(b){b&&n.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:k&&!k.call("\ufeff\xa0")?function(a){return null==a?"":k.call(a)}:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(g)return g.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(f=a[b],b=a,a=f),n.isFunction(a)?(c=d.call(arguments,2),e=function(){return a.apply(b||this,c.concat(d.call(arguments)))},e.guid=a.guid=a.guid||n.guid++,e):void 0},now:function(){return+new Date},support:l}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s="sizzle"+-new Date,t=a.document,u=0,v=0,w=eb(),x=eb(),y=eb(),z=function(a,b){return a===b&&(j=!0),0},A="undefined",B=1<<31,C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=D.indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]===a)return b;return-1},J="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",K="[\\x20\\t\\r\\n\\f]",L="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",M=L.replace("w","w#"),N="\\["+K+"*("+L+")"+K+"*(?:([*^$|!~]?=)"+K+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+M+")|)|)"+K+"*\\]",O=":("+L+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+N.replace(3,8)+")*)|.*)\\)|)",P=new RegExp("^"+K+"+|((?:^|[^\\\\])(?:\\\\.)*)"+K+"+$","g"),Q=new RegExp("^"+K+"*,"+K+"*"),R=new RegExp("^"+K+"*([>+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(O),U=new RegExp("^"+M+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L.replace("w","w*")+")"),ATTR:new RegExp("^"+N),PSEUDO:new RegExp("^"+O),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=/'|\\/g,ab=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),bb=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)};try{G.apply(D=H.call(t.childNodes),t.childNodes),D[t.childNodes.length].nodeType}catch(cb){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function db(a,b,d,e){var f,g,h,i,j,m,p,q,u,v;if((b?b.ownerDocument||b:t)!==l&&k(b),b=b||l,d=d||[],!a||"string"!=typeof a)return d;if(1!==(i=b.nodeType)&&9!==i)return[];if(n&&!e){if(f=Z.exec(a))if(h=f[1]){if(9===i){if(g=b.getElementById(h),!g||!g.parentNode)return d;if(g.id===h)return d.push(g),d}else if(b.ownerDocument&&(g=b.ownerDocument.getElementById(h))&&r(b,g)&&g.id===h)return d.push(g),d}else{if(f[2])return G.apply(d,b.getElementsByTagName(a)),d;if((h=f[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(h)),d}if(c.qsa&&(!o||!o.test(a))){if(q=p=s,u=b,v=9===i&&a,1===i&&"object"!==b.nodeName.toLowerCase()){m=ob(a),(p=b.getAttribute("id"))?q=p.replace(_,"\\$&"):b.setAttribute("id",q),q="[id='"+q+"'] ",j=m.length;while(j--)m[j]=q+pb(m[j]);u=$.test(a)&&mb(b.parentNode)||b,v=m.join(",")}if(v)try{return G.apply(d,u.querySelectorAll(v)),d}catch(w){}finally{p||b.removeAttribute("id")}}}return xb(a.replace(P,"$1"),b,d,e)}function eb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function fb(a){return a[s]=!0,a}function gb(a){var b=l.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function hb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function ib(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||B)-(~a.sourceIndex||B);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function jb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function kb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function lb(a){return fb(function(b){return b=+b,fb(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function mb(a){return a&&typeof a.getElementsByTagName!==A&&a}c=db.support={},f=db.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},k=db.setDocument=function(a){var b,e=a?a.ownerDocument||a:t,g=e.defaultView;return e!==l&&9===e.nodeType&&e.documentElement?(l=e,m=e.documentElement,n=!f(e),g&&g!==g.top&&(g.addEventListener?g.addEventListener("unload",function(){k()},!1):g.attachEvent&&g.attachEvent("onunload",function(){k()})),c.attributes=gb(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=gb(function(a){return a.appendChild(e.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(e.getElementsByClassName)&&gb(function(a){return a.innerHTML="<div class='a'></div><div class='a i'></div>",a.firstChild.className="i",2===a.getElementsByClassName("i").length}),c.getById=gb(function(a){return m.appendChild(a).id=s,!e.getElementsByName||!e.getElementsByName(s).length}),c.getById?(d.find.ID=function(a,b){if(typeof b.getElementById!==A&&n){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ab,bb);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ab,bb);return function(a){var c=typeof a.getAttributeNode!==A&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return typeof b.getElementsByTagName!==A?b.getElementsByTagName(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return typeof b.getElementsByClassName!==A&&n?b.getElementsByClassName(a):void 0},p=[],o=[],(c.qsa=Y.test(e.querySelectorAll))&&(gb(function(a){a.innerHTML="<select t=''><option selected=''></option></select>",a.querySelectorAll("[t^='']").length&&o.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||o.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll(":checked").length||o.push(":checked")}),gb(function(a){var b=e.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&o.push("name"+K+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||o.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),o.push(",.*:")})),(c.matchesSelector=Y.test(q=m.webkitMatchesSelector||m.mozMatchesSelector||m.oMatchesSelector||m.msMatchesSelector))&&gb(function(a){c.disconnectedMatch=q.call(a,"div"),q.call(a,"[s!='']:x"),p.push("!=",O)}),o=o.length&&new RegExp(o.join("|")),p=p.length&&new RegExp(p.join("|")),b=Y.test(m.compareDocumentPosition),r=b||Y.test(m.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},z=b?function(a,b){if(a===b)return j=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===e||a.ownerDocument===t&&r(t,a)?-1:b===e||b.ownerDocument===t&&r(t,b)?1:i?I.call(i,a)-I.call(i,b):0:4&d?-1:1)}:function(a,b){if(a===b)return j=!0,0;var c,d=0,f=a.parentNode,g=b.parentNode,h=[a],k=[b];if(!f||!g)return a===e?-1:b===e?1:f?-1:g?1:i?I.call(i,a)-I.call(i,b):0;if(f===g)return ib(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)k.unshift(c);while(h[d]===k[d])d++;return d?ib(h[d],k[d]):h[d]===t?-1:k[d]===t?1:0},e):l},db.matches=function(a,b){return db(a,null,null,b)},db.matchesSelector=function(a,b){if((a.ownerDocument||a)!==l&&k(a),b=b.replace(S,"='$1']"),!(!c.matchesSelector||!n||p&&p.test(b)||o&&o.test(b)))try{var d=q.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return db(b,l,null,[a]).length>0},db.contains=function(a,b){return(a.ownerDocument||a)!==l&&k(a),r(a,b)},db.attr=function(a,b){(a.ownerDocument||a)!==l&&k(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!n):void 0;return void 0!==f?f:c.attributes||!n?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},db.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},db.uniqueSort=function(a){var b,d=[],e=0,f=0;if(j=!c.detectDuplicates,i=!c.sortStable&&a.slice(0),a.sort(z),j){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return i=null,a},e=db.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=db.selectors={cacheLength:50,createPseudo:fb,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ab,bb),a[3]=(a[4]||a[5]||"").replace(ab,bb),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||db.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&db.error(a[0]),a},PSEUDO:function(a){var b,c=!a[5]&&a[2];return V.CHILD.test(a[0])?null:(a[3]&&void 0!==a[4]?a[2]=a[4]:c&&T.test(c)&&(b=ob(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ab,bb).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=w[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&w(a,function(a){return b.test("string"==typeof a.className&&a.className||typeof a.getAttribute!==A&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=db.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),t=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&t){k=q[s]||(q[s]={}),j=k[a]||[],n=j[0]===u&&j[1],m=j[0]===u&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[u,n,m];break}}else if(t&&(j=(b[s]||(b[s]={}))[a])&&j[0]===u)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(t&&((l[s]||(l[s]={}))[a]=[u,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||db.error("unsupported pseudo: "+a);return e[s]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?fb(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I.call(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:fb(function(a){var b=[],c=[],d=g(a.replace(P,"$1"));return d[s]?fb(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:fb(function(a){return function(b){return db(a,b).length>0}}),contains:fb(function(a){return function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:fb(function(a){return U.test(a||"")||db.error("unsupported lang: "+a),a=a.replace(ab,bb).toLowerCase(),function(b){var c;do if(c=n?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===m},focus:function(a){return a===l.activeElement&&(!l.hasFocus||l.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:lb(function(){return[0]}),last:lb(function(a,b){return[b-1]}),eq:lb(function(a,b,c){return[0>c?c+b:c]}),even:lb(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:lb(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:lb(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:lb(function(a,b,c){for(var d=0>c?c+b:c;++d<b;)a.push(d);return a})}},d.pseudos.nth=d.pseudos.eq;for(b in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})d.pseudos[b]=jb(b);for(b in{submit:!0,reset:!0})d.pseudos[b]=kb(b);function nb(){}nb.prototype=d.filters=d.pseudos,d.setFilters=new nb;function ob(a,b){var c,e,f,g,h,i,j,k=x[a+" "];if(k)return b?0:k.slice(0);h=a,i=[],j=d.preFilter;while(h){(!c||(e=Q.exec(h)))&&(e&&(h=h.slice(e[0].length)||h),i.push(f=[])),c=!1,(e=R.exec(h))&&(c=e.shift(),f.push({value:c,type:e[0].replace(P," ")}),h=h.slice(c.length));for(g in d.filter)!(e=V[g].exec(h))||j[g]&&!(e=j[g](e))||(c=e.shift(),f.push({value:c,type:g,matches:e}),h=h.slice(c.length));if(!c)break}return b?h.length:h?db.error(a):x(a,i).slice(0)}function pb(a){for(var b=0,c=a.length,d="";c>b;b++)d+=a[b].value;return d}function qb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=v++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[u,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[s]||(b[s]={}),(h=i[d])&&h[0]===u&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function rb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function sb(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function tb(a,b,c,d,e,f){return d&&!d[s]&&(d=tb(d)),e&&!e[s]&&(e=tb(e,f)),fb(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||wb(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:sb(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=sb(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?I.call(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=sb(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ub(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],i=g||d.relative[" "],j=g?1:0,k=qb(function(a){return a===b},i,!0),l=qb(function(a){return I.call(b,a)>-1},i,!0),m=[function(a,c,d){return!g&&(d||c!==h)||((b=c).nodeType?k(a,c,d):l(a,c,d))}];f>j;j++)if(c=d.relative[a[j].type])m=[qb(rb(m),c)];else{if(c=d.filter[a[j].type].apply(null,a[j].matches),c[s]){for(e=++j;f>e;e++)if(d.relative[a[e].type])break;return tb(j>1&&rb(m),j>1&&pb(a.slice(0,j-1).concat({value:" "===a[j-2].type?"*":""})).replace(P,"$1"),c,e>j&&ub(a.slice(j,e)),f>e&&ub(a=a.slice(e)),f>e&&pb(a))}m.push(c)}return rb(m)}function vb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,i,j,k){var m,n,o,p=0,q="0",r=f&&[],s=[],t=h,v=f||e&&d.find.TAG("*",k),w=u+=null==t?1:Math.random()||.1,x=v.length;for(k&&(h=g!==l&&g);q!==x&&null!=(m=v[q]);q++){if(e&&m){n=0;while(o=a[n++])if(o(m,g,i)){j.push(m);break}k&&(u=w)}c&&((m=!o&&m)&&p--,f&&r.push(m))}if(p+=q,c&&q!==p){n=0;while(o=b[n++])o(r,s,g,i);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=E.call(j));s=sb(s)}G.apply(j,s),k&&!f&&s.length>0&&p+b.length>1&&db.uniqueSort(j)}return k&&(u=w,h=t),r};return c?fb(f):f}g=db.compile=function(a,b){var c,d=[],e=[],f=y[a+" "];if(!f){b||(b=ob(a)),c=b.length;while(c--)f=ub(b[c]),f[s]?d.push(f):e.push(f);f=y(a,vb(e,d))}return f};function wb(a,b,c){for(var d=0,e=b.length;e>d;d++)db(a,b[d],c);return c}function xb(a,b,e,f){var h,i,j,k,l,m=ob(a);if(!f&&1===m.length){if(i=m[0]=m[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&c.getById&&9===b.nodeType&&n&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(ab,bb),b)||[])[0],!b)return e;a=a.slice(i.shift().value.length)}h=V.needsContext.test(a)?0:i.length;while(h--){if(j=i[h],d.relative[k=j.type])break;if((l=d.find[k])&&(f=l(j.matches[0].replace(ab,bb),$.test(i[0].type)&&mb(b.parentNode)||b))){if(i.splice(h,1),a=f.length&&pb(i),!a)return G.apply(e,f),e;break}}}return g(a,m)(f,b,!n,e,$.test(a)&&mb(b.parentNode)||b),e}return c.sortStable=s.split("").sort(z).join("")===s,c.detectDuplicates=!!j,k(),c.sortDetached=gb(function(a){return 1&a.compareDocumentPosition(l.createElement("div"))}),gb(function(a){return a.innerHTML="<a href='#'></a>","#"===a.firstChild.getAttribute("href")})||hb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&gb(function(a){return a.innerHTML="<input/>",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||hb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),gb(function(a){return null==a.getAttribute("disabled")})||hb(J,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),db}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=n.expr.match.needsContext,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^.[^:#\[\.,]*$/;function x(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(w.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return n.inArray(a,b)>=0!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;e>b;b++)if(n.contains(d[b],this))return!0}));for(b=0;e>b;b++)n.find(a,d[b],c);return c=this.pushStack(e>1?n.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(x(this,a||[],!1))},not:function(a){return this.pushStack(x(this,a||[],!0))},is:function(a){return!!x(this,"string"==typeof a&&u.test(a)?n(a):a||[],!1).length}});var y,z=a.document,A=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,B=n.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:A.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||y).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:z,!0)),v.test(c[1])&&n.isPlainObject(b))for(c in b)n.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}if(d=z.getElementById(c[2]),d&&d.parentNode){if(d.id!==c[2])return y.find(a);this.length=1,this[0]=d}return this.context=z,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof y.ready?y.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};B.prototype=n.fn,y=n(z);var C=/^(?:parents|prev(?:Until|All))/,D={children:!0,contents:!0,next:!0,prev:!0};n.extend({dir:function(a,b,c){var d=[],e=a[b];while(e&&9!==e.nodeType&&(void 0===c||1!==e.nodeType||!n(e).is(c)))1===e.nodeType&&d.push(e),e=e[b];return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),n.fn.extend({has:function(a){var b,c=n(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(n.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=u.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.unique(f):f)},index:function(a){return a?"string"==typeof a?n.inArray(this[0],n(a)):n.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.unique(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function E(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return n.dir(a,"parentNode")},parentsUntil:function(a,b,c){return n.dir(a,"parentNode",c)},next:function(a){return E(a,"nextSibling")},prev:function(a){return E(a,"previousSibling")},nextAll:function(a){return n.dir(a,"nextSibling")},prevAll:function(a){return n.dir(a,"previousSibling")},nextUntil:function(a,b,c){return n.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return n.dir(a,"previousSibling",c)},siblings:function(a){return n.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return n.sibling(a.firstChild)},contents:function(a){return n.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(D[a]||(e=n.unique(e)),C.test(a)&&(e=e.reverse())),this.pushStack(e)}});var F=/\S+/g,G={};function H(a){var b=G[a]={};return n.each(a.match(F)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?G[a]||H(a):n.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(c=a.memory&&l,d=!0,f=g||0,g=0,e=h.length,b=!0;h&&e>f;f++)if(h[f].apply(l[0],l[1])===!1&&a.stopOnFalse){c=!1;break}b=!1,h&&(i?i.length&&j(i.shift()):c?h=[]:k.disable())},k={add:function(){if(h){var d=h.length;!function f(b){n.each(b,function(b,c){var d=n.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&f(c)})}(arguments),b?e=h.length:c&&(g=d,j(c))}return this},remove:function(){return h&&n.each(arguments,function(a,c){var d;while((d=n.inArray(c,h,d))>-1)h.splice(d,1),b&&(e>=d&&e--,f>=d&&f--)}),this},has:function(a){return a?n.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],e=0,this},disable:function(){return h=i=c=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,c||k.disable(),this},locked:function(){return!i},fireWith:function(a,c){return!h||d&&!i||(c=c||[],c=[a,c.slice?c.slice():c],b?i.push(c):j(c)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!d}};return k},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&n.isFunction(a.promise)?e:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var I;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){if(a===!0?!--n.readyWait:!n.isReady){if(!z.body)return setTimeout(n.ready);n.isReady=!0,a!==!0&&--n.readyWait>0||(I.resolveWith(z,[n]),n.fn.trigger&&n(z).trigger("ready").off("ready"))}}});function J(){z.addEventListener?(z.removeEventListener("DOMContentLoaded",K,!1),a.removeEventListener("load",K,!1)):(z.detachEvent("onreadystatechange",K),a.detachEvent("onload",K))}function K(){(z.addEventListener||"load"===event.type||"complete"===z.readyState)&&(J(),n.ready())}n.ready.promise=function(b){if(!I)if(I=n.Deferred(),"complete"===z.readyState)setTimeout(n.ready);else if(z.addEventListener)z.addEventListener("DOMContentLoaded",K,!1),a.addEventListener("load",K,!1);else{z.attachEvent("onreadystatechange",K),a.attachEvent("onload",K);var c=!1;try{c=null==a.frameElement&&z.documentElement}catch(d){}c&&c.doScroll&&!function e(){if(!n.isReady){try{c.doScroll("left")}catch(a){return setTimeout(e,50)}J(),n.ready()}}()}return I.promise(b)};var L="undefined",M;for(M in n(l))break;l.ownLast="0"!==M,l.inlineBlockNeedsLayout=!1,n(function(){var a,b,c=z.getElementsByTagName("body")[0];c&&(a=z.createElement("div"),a.style.cssText="border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px",b=z.createElement("div"),c.appendChild(a).appendChild(b),typeof b.style.zoom!==L&&(b.style.cssText="border:0;margin:0;width:1px;padding:1px;display:inline;zoom:1",(l.inlineBlockNeedsLayout=3===b.offsetWidth)&&(c.style.zoom=1)),c.removeChild(a),a=b=null)}),function(){var a=z.createElement("div");if(null==l.deleteExpando){l.deleteExpando=!0;try{delete a.test}catch(b){l.deleteExpando=!1}}a=null}(),n.acceptData=function(a){var b=n.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b};var N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(O,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}n.data(a,b,c)}else c=void 0}return c}function Q(a){var b;for(b in a)if(("data"!==b||!n.isEmptyObject(a[b]))&&"toJSON"!==b)return!1;return!0}function R(a,b,d,e){if(n.acceptData(a)){var f,g,h=n.expando,i=a.nodeType,j=i?n.cache:a,k=i?a[h]:a[h]&&h;if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||n.guid++:h),j[k]||(j[k]=i?{}:{toJSON:n.noop}),("object"==typeof b||"function"==typeof b)&&(e?j[k]=n.extend(j[k],b):j[k].data=n.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[n.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[n.camelCase(b)])):f=g,f +}}function S(a,b,c){if(n.acceptData(a)){var d,e,f=a.nodeType,g=f?n.cache:a,h=f?a[n.expando]:n.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){n.isArray(b)?b=b.concat(n.map(b,n.camelCase)):b in d?b=[b]:(b=n.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!Q(d):!n.isEmptyObject(d))return}(c||(delete g[h].data,Q(g[h])))&&(f?n.cleanData([a],!0):l.deleteExpando||g!=g.window?delete g[h]:g[h]=null)}}}n.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?n.cache[a[n.expando]]:a[n.expando],!!a&&!Q(a)},data:function(a,b,c){return R(a,b,c)},removeData:function(a,b){return S(a,b)},_data:function(a,b,c){return R(a,b,c,!0)},_removeData:function(a,b){return S(a,b,!0)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=n.data(f),1===f.nodeType&&!n._data(f,"parsedAttrs"))){c=g.length;while(c--)d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d]));n._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){n.data(this,a)}):arguments.length>1?this.each(function(){n.data(this,a,b)}):f?P(f,a,n.data(f,a)):void 0},removeData:function(a){return this.each(function(){n.removeData(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=n._data(a,b),c&&(!d||n.isArray(c)?d=n._data(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return n._data(a,c)||n._data(a,c,{empty:n.Callbacks("once memory").add(function(){n._removeData(a,b+"queue"),n._removeData(a,c)})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?n.queue(this[0],a):void 0===b?this:this.each(function(){var c=n.queue(this,a,b);n._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&n.dequeue(this,a)})},dequeue:function(a){return this.each(function(){n.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=n.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=n._data(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}});var T=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,U=["Top","Right","Bottom","Left"],V=function(a,b){return a=b||a,"none"===n.css(a,"display")||!n.contains(a.ownerDocument,a)},W=n.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)n.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},X=/^(?:checkbox|radio)$/i;!function(){var a=z.createDocumentFragment(),b=z.createElement("div"),c=z.createElement("input");if(b.setAttribute("className","t"),b.innerHTML=" <link/><table></table><a href='/a'>a</a>",l.leadingWhitespace=3===b.firstChild.nodeType,l.tbody=!b.getElementsByTagName("tbody").length,l.htmlSerialize=!!b.getElementsByTagName("link").length,l.html5Clone="<:nav></:nav>"!==z.createElement("nav").cloneNode(!0).outerHTML,c.type="checkbox",c.checked=!0,a.appendChild(c),l.appendChecked=c.checked,b.innerHTML="<textarea>x</textarea>",l.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue,a.appendChild(b),b.innerHTML="<input type='radio' checked='checked' name='t'/>",l.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,l.noCloneEvent=!0,b.attachEvent&&(b.attachEvent("onclick",function(){l.noCloneEvent=!1}),b.cloneNode(!0).click()),null==l.deleteExpando){l.deleteExpando=!0;try{delete b.test}catch(d){l.deleteExpando=!1}}a=b=c=null}(),function(){var b,c,d=z.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(l[b+"Bubbles"]=c in a)||(d.setAttribute(c,"t"),l[b+"Bubbles"]=d.attributes[c].expando===!1);d=null}();var Y=/^(?:input|select|textarea)$/i,Z=/^key/,$=/^(?:mouse|contextmenu)|click/,_=/^(?:focusinfocus|focusoutblur)$/,ab=/^([^.]*)(?:\.(.+)|)$/;function bb(){return!0}function cb(){return!1}function db(){try{return z.activeElement}catch(a){}}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=n.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return typeof n===L||a&&n.event.triggered===a.type?void 0:n.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(F)||[""],h=b.length;while(h--)f=ab.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=n.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=n.event.special[o]||{},l=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},i),(m=g[o])||(m=g[o]=[],m.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,l):m.push(l),n.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n.hasData(a)&&n._data(a);if(r&&(k=r.events)){b=(b||"").match(F)||[""],j=b.length;while(j--)if(h=ab.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=m.length;while(f--)g=m[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(m.splice(f,1),g.selector&&m.delegateCount--,l.remove&&l.remove.call(a,g));i&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(k)&&(delete r.handle,n._removeData(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,l,m,o=[d||z],p=j.call(b,"type")?b.type:b,q=j.call(b,"namespace")?b.namespace.split("."):[];if(h=l=d=d||z,3!==d.nodeType&&8!==d.nodeType&&!_.test(p+n.event.triggered)&&(p.indexOf(".")>=0&&(q=p.split("."),p=q.shift(),q.sort()),g=p.indexOf(":")<0&&"on"+p,b=b[n.expando]?b:new n.Event(p,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=q.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:n.makeArray(c,[b]),k=n.event.special[p]||{},e||!k.trigger||k.trigger.apply(d,c)!==!1)){if(!e&&!k.noBubble&&!n.isWindow(d)){for(i=k.delegateType||p,_.test(i+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),l=h;l===(d.ownerDocument||z)&&o.push(l.defaultView||l.parentWindow||a)}m=0;while((h=o[m++])&&!b.isPropagationStopped())b.type=m>1?i:k.bindType||p,f=(n._data(h,"events")||{})[b.type]&&n._data(h,"handle"),f&&f.apply(h,c),f=g&&h[g],f&&f.apply&&n.acceptData(h)&&(b.result=f.apply(h,c),b.result===!1&&b.preventDefault());if(b.type=p,!e&&!b.isDefaultPrevented()&&(!k._default||k._default.apply(o.pop(),c)===!1)&&n.acceptData(d)&&g&&d[p]&&!n.isWindow(d)){l=d[g],l&&(d[g]=null),n.event.triggered=p;try{d[p]()}catch(r){}n.event.triggered=void 0,l&&(d[g]=l)}return b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(n._data(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,g=0;while((e=f.handlers[g++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(e.namespace))&&(a.handleObj=e,a.data=e.data,c=((n.event.special[e.origType]||{}).handle||e.handler).apply(f.elem,i),void 0!==c&&(a.result=c)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(e=[],f=0;h>f;f++)d=b[f],c=d.selector+" ",void 0===e[c]&&(e[c]=d.needsContext?n(c,this).index(i)>=0:n.find(c,this,null,[i]).length),e[c]&&e.push(d);e.length&&g.push({elem:i,handlers:e})}return h<b.length&&g.push({elem:this,handlers:b.slice(h)}),g},fix:function(a){if(a[n.expando])return a;var b,c,d,e=a.type,f=a,g=this.fixHooks[e];g||(this.fixHooks[e]=g=$.test(e)?this.mouseHooks:Z.test(e)?this.keyHooks:{}),d=g.props?this.props.concat(g.props):this.props,a=new n.Event(f),b=d.length;while(b--)c=d[b],a[c]=f[c];return a.target||(a.target=f.srcElement||z),3===a.target.nodeType&&(a.target=a.target.parentNode),a.metaKey=!!a.metaKey,g.filter?g.filter(a,f):a},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){return null==a.which&&(a.which=null!=b.charCode?b.charCode:b.keyCode),a}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,b){var c,d,e,f=b.button,g=b.fromElement;return null==a.pageX&&null!=b.clientX&&(d=a.target.ownerDocument||z,e=d.documentElement,c=d.body,a.pageX=b.clientX+(e&&e.scrollLeft||c&&c.scrollLeft||0)-(e&&e.clientLeft||c&&c.clientLeft||0),a.pageY=b.clientY+(e&&e.scrollTop||c&&c.scrollTop||0)-(e&&e.clientTop||c&&c.clientTop||0)),!a.relatedTarget&&g&&(a.relatedTarget=g===a.target?b.toElement:g),a.which||void 0===f||(a.which=1&f?1:2&f?3:4&f?2:0),a}},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==db()&&this.focus)try{return this.focus(),!1}catch(a){}},delegateType:"focusin"},blur:{trigger:function(){return this===db()&&this.blur?(this.blur(),!1):void 0},delegateType:"focusout"},click:{trigger:function(){return n.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):void 0},_default:function(a){return n.nodeName(a.target,"a")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&(a.originalEvent.returnValue=a.result)}}},simulate:function(a,b,c,d){var e=n.extend(new n.Event,c,{type:a,isSimulated:!0,originalEvent:{}});d?n.event.trigger(e,null,b):n.event.dispatch.call(b,e),e.isDefaultPrevented()&&c.preventDefault()}},n.removeEvent=z.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)}:function(a,b,c){var d="on"+b;a.detachEvent&&(typeof a[d]===L&&(a[d]=null),a.detachEvent(d,c))},n.Event=function(a,b){return this instanceof n.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&(a.returnValue===!1||a.getPreventDefault&&a.getPreventDefault())?bb:cb):this.type=a,b&&n.extend(this,b),this.timeStamp=a&&a.timeStamp||n.now(),void(this[n.expando]=!0)):new n.Event(a,b)},n.Event.prototype={isDefaultPrevented:cb,isPropagationStopped:cb,isImmediatePropagationStopped:cb,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=bb,a&&(a.preventDefault?a.preventDefault():a.returnValue=!1)},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=bb,a&&(a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=bb,this.stopPropagation()}},n.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){n.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return(!e||e!==d&&!n.contains(d,e))&&(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),l.submitBubbles||(n.event.special.submit={setup:function(){return n.nodeName(this,"form")?!1:void n.event.add(this,"click._submit keypress._submit",function(a){var b=a.target,c=n.nodeName(b,"input")||n.nodeName(b,"button")?b.form:void 0;c&&!n._data(c,"submitBubbles")&&(n.event.add(c,"submit._submit",function(a){a._submit_bubble=!0}),n._data(c,"submitBubbles",!0))})},postDispatch:function(a){a._submit_bubble&&(delete a._submit_bubble,this.parentNode&&!a.isTrigger&&n.event.simulate("submit",this.parentNode,a,!0))},teardown:function(){return n.nodeName(this,"form")?!1:void n.event.remove(this,"._submit")}}),l.changeBubbles||(n.event.special.change={setup:function(){return Y.test(this.nodeName)?(("checkbox"===this.type||"radio"===this.type)&&(n.event.add(this,"propertychange._change",function(a){"checked"===a.originalEvent.propertyName&&(this._just_changed=!0)}),n.event.add(this,"click._change",function(a){this._just_changed&&!a.isTrigger&&(this._just_changed=!1),n.event.simulate("change",this,a,!0)})),!1):void n.event.add(this,"beforeactivate._change",function(a){var b=a.target;Y.test(b.nodeName)&&!n._data(b,"changeBubbles")&&(n.event.add(b,"change._change",function(a){!this.parentNode||a.isSimulated||a.isTrigger||n.event.simulate("change",this.parentNode,a,!0)}),n._data(b,"changeBubbles",!0))})},handle:function(a){var b=a.target;return this!==b||a.isSimulated||a.isTrigger||"radio"!==b.type&&"checkbox"!==b.type?a.handleObj.handler.apply(this,arguments):void 0},teardown:function(){return n.event.remove(this,"._change"),!Y.test(this.nodeName)}}),l.focusinBubbles||n.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){n.event.simulate(b,a.target,n.event.fix(a),!0)};n.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=n._data(d,b);e||d.addEventListener(a,c,!0),n._data(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=n._data(d,b)-1;e?n._data(d,b,e):(d.removeEventListener(a,c,!0),n._removeData(d,b))}}}),n.fn.extend({on:function(a,b,c,d,e){var f,g;if("object"==typeof a){"string"!=typeof b&&(c=c||b,b=void 0);for(f in a)this.on(f,b,c,a[f],e);return this}if(null==c&&null==d?(d=b,c=b=void 0):null==d&&("string"==typeof b?(d=c,c=void 0):(d=c,c=b,b=void 0)),d===!1)d=cb;else if(!d)return this;return 1===e&&(g=d,d=function(a){return n().off(a),g.apply(this,arguments)},d.guid=g.guid||(g.guid=n.guid++)),this.each(function(){n.event.add(this,a,d,c,b)})},one:function(a,b,c,d){return this.on(a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.preventDefault&&a.handleObj)return d=a.handleObj,n(a.delegateTarget).off(d.namespace?d.origType+"."+d.namespace:d.origType,d.selector,d.handler),this;if("object"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return(b===!1||"function"==typeof b)&&(c=b,b=void 0),c===!1&&(c=cb),this.each(function(){n.event.remove(this,a,c,b)})},trigger:function(a,b){return this.each(function(){n.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];return c?n.event.trigger(a,b,c,!0):void 0}});function eb(a){var b=fb.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}var fb="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",gb=/ jQuery\d+="(?:null|\d+)"/g,hb=new RegExp("<(?:"+fb+")[\\s/>]","i"),ib=/^\s+/,jb=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,kb=/<([\w:]+)/,lb=/<tbody/i,mb=/<|&#?\w+;/,nb=/<(?:script|style|link)/i,ob=/checked\s*(?:[^=]|=\s*.checked.)/i,pb=/^$|\/(?:java|ecma)script/i,qb=/^true\/(.*)/,rb=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,sb={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],area:[1,"<map>","</map>"],param:[1,"<object>","</object>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:l.htmlSerialize?[0,"",""]:[1,"X<div>","</div>"]},tb=eb(z),ub=tb.appendChild(z.createElement("div"));sb.optgroup=sb.option,sb.tbody=sb.tfoot=sb.colgroup=sb.caption=sb.thead,sb.th=sb.td;function vb(a,b){var c,d,e=0,f=typeof a.getElementsByTagName!==L?a.getElementsByTagName(b||"*"):typeof a.querySelectorAll!==L?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||n.nodeName(d,b)?f.push(d):n.merge(f,vb(d,b));return void 0===b||b&&n.nodeName(a,b)?n.merge([a],f):f}function wb(a){X.test(a.type)&&(a.defaultChecked=a.checked)}function xb(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function yb(a){return a.type=(null!==n.find.attr(a,"type"))+"/"+a.type,a}function zb(a){var b=qb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ab(a,b){for(var c,d=0;null!=(c=a[d]);d++)n._data(c,"globalEval",!b||n._data(b[d],"globalEval"))}function Bb(a,b){if(1===b.nodeType&&n.hasData(a)){var c,d,e,f=n._data(a),g=n._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)n.event.add(b,c,h[c][d])}g.data&&(g.data=n.extend({},g.data))}}function Cb(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!l.noCloneEvent&&b[n.expando]){e=n._data(b);for(d in e.events)n.removeEvent(b,d,e.handle);b.removeAttribute(n.expando)}"script"===c&&b.text!==a.text?(yb(b).text=a.text,zb(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),l.html5Clone&&a.innerHTML&&!n.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&X.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}}n.extend({clone:function(a,b,c){var d,e,f,g,h,i=n.contains(a.ownerDocument,a);if(l.html5Clone||n.isXMLDoc(a)||!hb.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(ub.innerHTML=a.outerHTML,ub.removeChild(f=ub.firstChild)),!(l.noCloneEvent&&l.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(d=vb(f),h=vb(a),g=0;null!=(e=h[g]);++g)d[g]&&Cb(e,d[g]);if(b)if(c)for(h=h||vb(a),d=d||vb(f),g=0;null!=(e=h[g]);g++)Bb(e,d[g]);else Bb(a,f);return d=vb(f,"script"),d.length>0&&Ab(d,!i&&vb(a,"script")),d=h=e=null,f},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k,m=a.length,o=eb(b),p=[],q=0;m>q;q++)if(f=a[q],f||0===f)if("object"===n.type(f))n.merge(p,f.nodeType?[f]:f);else if(mb.test(f)){h=h||o.appendChild(b.createElement("div")),i=(kb.exec(f)||["",""])[1].toLowerCase(),k=sb[i]||sb._default,h.innerHTML=k[1]+f.replace(jb,"<$1></$2>")+k[2],e=k[0];while(e--)h=h.lastChild;if(!l.leadingWhitespace&&ib.test(f)&&p.push(b.createTextNode(ib.exec(f)[0])),!l.tbody){f="table"!==i||lb.test(f)?"<table>"!==k[1]||lb.test(f)?0:h:h.firstChild,e=f&&f.childNodes.length;while(e--)n.nodeName(j=f.childNodes[e],"tbody")&&!j.childNodes.length&&f.removeChild(j)}n.merge(p,h.childNodes),h.textContent="";while(h.firstChild)h.removeChild(h.firstChild);h=o.lastChild}else p.push(b.createTextNode(f));h&&o.removeChild(h),l.appendChecked||n.grep(vb(p,"input"),wb),q=0;while(f=p[q++])if((!d||-1===n.inArray(f,d))&&(g=n.contains(f.ownerDocument,f),h=vb(o.appendChild(f),"script"),g&&Ab(h),c)){e=0;while(f=h[e++])pb.test(f.type||"")&&c.push(f)}return h=null,o},cleanData:function(a,b){for(var d,e,f,g,h=0,i=n.expando,j=n.cache,k=l.deleteExpando,m=n.event.special;null!=(d=a[h]);h++)if((b||n.acceptData(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)m[e]?n.event.remove(d,e):n.removeEvent(d,e,g.handle);j[f]&&(delete j[f],k?delete d[i]:typeof d.removeAttribute!==L?d.removeAttribute(i):d[i]=null,c.push(f))}}}),n.fn.extend({text:function(a){return W(this,function(a){return void 0===a?n.text(this):this.empty().append((this[0]&&this[0].ownerDocument||z).createTextNode(a))},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=xb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=xb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?n.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||n.cleanData(vb(c)),c.parentNode&&(b&&n.contains(c.ownerDocument,c)&&Ab(vb(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&n.cleanData(vb(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&n.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return W(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(gb,""):void 0;if(!("string"!=typeof a||nb.test(a)||!l.htmlSerialize&&hb.test(a)||!l.leadingWhitespace&&ib.test(a)||sb[(kb.exec(a)||["",""])[1].toLowerCase()])){a=a.replace(jb,"<$1></$2>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(vb(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,n.cleanData(vb(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,k=this.length,m=this,o=k-1,p=a[0],q=n.isFunction(p);if(q||k>1&&"string"==typeof p&&!l.checkClone&&ob.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(k&&(i=n.buildFragment(a,this[0].ownerDocument,!1,this),c=i.firstChild,1===i.childNodes.length&&(i=c),c)){for(g=n.map(vb(i,"script"),yb),f=g.length;k>j;j++)d=i,j!==o&&(d=n.clone(d,!0,!0),f&&n.merge(g,vb(d,"script"))),b.call(this[j],d,j);if(f)for(h=g[g.length-1].ownerDocument,n.map(g,zb),j=0;f>j;j++)d=g[j],pb.test(d.type||"")&&!n._data(d,"globalEval")&&n.contains(h,d)&&(d.src?n._evalUrl&&n._evalUrl(d.src):n.globalEval((d.text||d.textContent||d.innerHTML||"").replace(rb,"")));i=c=null}return this}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=0,e=[],g=n(a),h=g.length-1;h>=d;d++)c=d===h?this:this.clone(!0),n(g[d])[b](c),f.apply(e,c.get());return this.pushStack(e)}});var Db,Eb={};function Fb(b,c){var d=n(c.createElement(b)).appendTo(c.body),e=a.getDefaultComputedStyle?a.getDefaultComputedStyle(d[0]).display:n.css(d[0],"display");return d.detach(),e}function Gb(a){var b=z,c=Eb[a];return c||(c=Fb(a,b),"none"!==c&&c||(Db=(Db||n("<iframe frameborder='0' width='0' height='0'/>")).appendTo(b.documentElement),b=(Db[0].contentWindow||Db[0].contentDocument).document,b.write(),b.close(),c=Fb(a,b),Db.detach()),Eb[a]=c),c}!function(){var a,b,c=z.createElement("div"),d="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;padding:0;margin:0;border:0";c.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",a=c.getElementsByTagName("a")[0],a.style.cssText="float:left;opacity:.5",l.opacity=/^0.5/.test(a.style.opacity),l.cssFloat=!!a.style.cssFloat,c.style.backgroundClip="content-box",c.cloneNode(!0).style.backgroundClip="",l.clearCloneStyle="content-box"===c.style.backgroundClip,a=c=null,l.shrinkWrapBlocks=function(){var a,c,e,f;if(null==b){if(a=z.getElementsByTagName("body")[0],!a)return;f="border:0;width:0;height:0;position:absolute;top:0;left:-9999px",c=z.createElement("div"),e=z.createElement("div"),a.appendChild(c).appendChild(e),b=!1,typeof e.style.zoom!==L&&(e.style.cssText=d+";width:1px;padding:1px;zoom:1",e.innerHTML="<div></div>",e.firstChild.style.width="5px",b=3!==e.offsetWidth),a.removeChild(c),a=c=e=null}return b}}();var Hb=/^margin/,Ib=new RegExp("^("+T+")(?!px)[a-z%]+$","i"),Jb,Kb,Lb=/^(top|right|bottom|left)$/;a.getComputedStyle?(Jb=function(a){return a.ownerDocument.defaultView.getComputedStyle(a,null)},Kb=function(a,b,c){var d,e,f,g,h=a.style;return c=c||Jb(a),g=c?c.getPropertyValue(b)||c[b]:void 0,c&&(""!==g||n.contains(a.ownerDocument,a)||(g=n.style(a,b)),Ib.test(g)&&Hb.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f)),void 0===g?g:g+""}):z.documentElement.currentStyle&&(Jb=function(a){return a.currentStyle},Kb=function(a,b,c){var d,e,f,g,h=a.style;return c=c||Jb(a),g=c?c[b]:void 0,null==g&&h&&h[b]&&(g=h[b]),Ib.test(g)&&!Lb.test(b)&&(d=h.left,e=a.runtimeStyle,f=e&&e.left,f&&(e.left=a.currentStyle.left),h.left="fontSize"===b?"1em":g,g=h.pixelLeft+"px",h.left=d,f&&(e.left=f)),void 0===g?g:g+""||"auto"});function Mb(a,b){return{get:function(){var c=a();if(null!=c)return c?void delete this.get:(this.get=b).apply(this,arguments)}}}!function(){var b,c,d,e,f,g,h=z.createElement("div"),i="border:0;width:0;height:0;position:absolute;top:0;left:-9999px",j="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;padding:0;margin:0;border:0";h.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",b=h.getElementsByTagName("a")[0],b.style.cssText="float:left;opacity:.5",l.opacity=/^0.5/.test(b.style.opacity),l.cssFloat=!!b.style.cssFloat,h.style.backgroundClip="content-box",h.cloneNode(!0).style.backgroundClip="",l.clearCloneStyle="content-box"===h.style.backgroundClip,b=h=null,n.extend(l,{reliableHiddenOffsets:function(){if(null!=c)return c;var a,b,d,e=z.createElement("div"),f=z.getElementsByTagName("body")[0];if(f)return e.setAttribute("className","t"),e.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",a=z.createElement("div"),a.style.cssText=i,f.appendChild(a).appendChild(e),e.innerHTML="<table><tr><td></td><td>t</td></tr></table>",b=e.getElementsByTagName("td"),b[0].style.cssText="padding:0;margin:0;border:0;display:none",d=0===b[0].offsetHeight,b[0].style.display="",b[1].style.display="none",c=d&&0===b[0].offsetHeight,f.removeChild(a),e=f=null,c},boxSizing:function(){return null==d&&k(),d},boxSizingReliable:function(){return null==e&&k(),e},pixelPosition:function(){return null==f&&k(),f},reliableMarginRight:function(){var b,c,d,e;if(null==g&&a.getComputedStyle){if(b=z.getElementsByTagName("body")[0],!b)return;c=z.createElement("div"),d=z.createElement("div"),c.style.cssText=i,b.appendChild(c).appendChild(d),e=d.appendChild(z.createElement("div")),e.style.cssText=d.style.cssText=j,e.style.marginRight=e.style.width="0",d.style.width="1px",g=!parseFloat((a.getComputedStyle(e,null)||{}).marginRight),b.removeChild(c)}return g}});function k(){var b,c,h=z.getElementsByTagName("body")[0];h&&(b=z.createElement("div"),c=z.createElement("div"),b.style.cssText=i,h.appendChild(b).appendChild(c),c.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;position:absolute;display:block;padding:1px;border:1px;width:4px;margin-top:1%;top:1%",n.swap(h,null!=h.style.zoom?{zoom:1}:{},function(){d=4===c.offsetWidth}),e=!0,f=!1,g=!0,a.getComputedStyle&&(f="1%"!==(a.getComputedStyle(c,null)||{}).top,e="4px"===(a.getComputedStyle(c,null)||{width:"4px"}).width),h.removeChild(b),c=h=null)}}(),n.swap=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e};var Nb=/alpha\([^)]*\)/i,Ob=/opacity\s*=\s*([^)]*)/,Pb=/^(none|table(?!-c[ea]).+)/,Qb=new RegExp("^("+T+")(.*)$","i"),Rb=new RegExp("^([+-])=("+T+")","i"),Sb={position:"absolute",visibility:"hidden",display:"block"},Tb={letterSpacing:0,fontWeight:400},Ub=["Webkit","O","Moz","ms"];function Vb(a,b){if(b in a)return b;var c=b.charAt(0).toUpperCase()+b.slice(1),d=b,e=Ub.length;while(e--)if(b=Ub[e]+c,b in a)return b;return d}function Wb(a,b){for(var c,d,e,f=[],g=0,h=a.length;h>g;g++)d=a[g],d.style&&(f[g]=n._data(d,"olddisplay"),c=d.style.display,b?(f[g]||"none"!==c||(d.style.display=""),""===d.style.display&&V(d)&&(f[g]=n._data(d,"olddisplay",Gb(d.nodeName)))):f[g]||(e=V(d),(c&&"none"!==c||!e)&&n._data(d,"olddisplay",e?c:n.css(d,"display"))));for(g=0;h>g;g++)d=a[g],d.style&&(b&&"none"!==d.style.display&&""!==d.style.display||(d.style.display=b?f[g]||"":"none"));return a}function Xb(a,b,c){var d=Qb.exec(b);return d?Math.max(0,d[1]-(c||0))+(d[2]||"px"):b}function Yb(a,b,c,d,e){for(var f=c===(d?"border":"content")?4:"width"===b?1:0,g=0;4>f;f+=2)"margin"===c&&(g+=n.css(a,c+U[f],!0,e)),d?("content"===c&&(g-=n.css(a,"padding"+U[f],!0,e)),"margin"!==c&&(g-=n.css(a,"border"+U[f]+"Width",!0,e))):(g+=n.css(a,"padding"+U[f],!0,e),"padding"!==c&&(g+=n.css(a,"border"+U[f]+"Width",!0,e)));return g}function Zb(a,b,c){var d=!0,e="width"===b?a.offsetWidth:a.offsetHeight,f=Jb(a),g=l.boxSizing()&&"border-box"===n.css(a,"boxSizing",!1,f);if(0>=e||null==e){if(e=Kb(a,b,f),(0>e||null==e)&&(e=a.style[b]),Ib.test(e))return e;d=g&&(l.boxSizingReliable()||e===a.style[b]),e=parseFloat(e)||0}return e+Yb(a,b,c||(g?"border":"content"),d,f)+"px"}n.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=Kb(a,"opacity");return""===c?"1":c}}}},cssNumber:{columnCount:!0,fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":l.cssFloat?"cssFloat":"styleFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=n.camelCase(b),i=a.style;if(b=n.cssProps[h]||(n.cssProps[h]=Vb(i,h)),g=n.cssHooks[b]||n.cssHooks[h],void 0===c)return g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:i[b];if(f=typeof c,"string"===f&&(e=Rb.exec(c))&&(c=(e[1]+1)*e[2]+parseFloat(n.css(a,b)),f="number"),null!=c&&c===c&&("number"!==f||n.cssNumber[h]||(c+="px"),l.clearCloneStyle||""!==c||0!==b.indexOf("background")||(i[b]="inherit"),!(g&&"set"in g&&void 0===(c=g.set(a,c,d)))))try{i[b]="",i[b]=c}catch(j){}}},css:function(a,b,c,d){var e,f,g,h=n.camelCase(b);return b=n.cssProps[h]||(n.cssProps[h]=Vb(a.style,h)),g=n.cssHooks[b]||n.cssHooks[h],g&&"get"in g&&(f=g.get(a,!0,c)),void 0===f&&(f=Kb(a,b,d)),"normal"===f&&b in Tb&&(f=Tb[b]),""===c||c?(e=parseFloat(f),c===!0||n.isNumeric(e)?e||0:f):f}}),n.each(["height","width"],function(a,b){n.cssHooks[b]={get:function(a,c,d){return c?0===a.offsetWidth&&Pb.test(n.css(a,"display"))?n.swap(a,Sb,function(){return Zb(a,b,d)}):Zb(a,b,d):void 0},set:function(a,c,d){var e=d&&Jb(a);return Xb(a,c,d?Yb(a,b,d,l.boxSizing()&&"border-box"===n.css(a,"boxSizing",!1,e),e):0)}}}),l.opacity||(n.cssHooks.opacity={get:function(a,b){return Ob.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=n.isNumeric(b)?"alpha(opacity="+100*b+")":"",f=d&&d.filter||c.filter||"";c.zoom=1,(b>=1||""===b)&&""===n.trim(f.replace(Nb,""))&&c.removeAttribute&&(c.removeAttribute("filter"),""===b||d&&!d.filter)||(c.filter=Nb.test(f)?f.replace(Nb,e):f+" "+e)}}),n.cssHooks.marginRight=Mb(l.reliableMarginRight,function(a,b){return b?n.swap(a,{display:"inline-block"},Kb,[a,"marginRight"]):void 0}),n.each({margin:"",padding:"",border:"Width"},function(a,b){n.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];4>d;d++)e[a+U[d]+b]=f[d]||f[d-2]||f[0];return e}},Hb.test(a)||(n.cssHooks[a+b].set=Xb)}),n.fn.extend({css:function(a,b){return W(this,function(a,b,c){var d,e,f={},g=0;if(n.isArray(b)){for(d=Jb(a),e=b.length;e>g;g++)f[b[g]]=n.css(a,b[g],!1,d);return f}return void 0!==c?n.style(a,b,c):n.css(a,b) +},a,b,arguments.length>1)},show:function(){return Wb(this,!0)},hide:function(){return Wb(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){V(this)?n(this).show():n(this).hide()})}});function $b(a,b,c,d,e){return new $b.prototype.init(a,b,c,d,e)}n.Tween=$b,$b.prototype={constructor:$b,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||"swing",this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(n.cssNumber[c]?"":"px")},cur:function(){var a=$b.propHooks[this.prop];return a&&a.get?a.get(this):$b.propHooks._default.get(this)},run:function(a){var b,c=$b.propHooks[this.prop];return this.pos=b=this.options.duration?n.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):$b.propHooks._default.set(this),this}},$b.prototype.init.prototype=$b.prototype,$b.propHooks={_default:{get:function(a){var b;return null==a.elem[a.prop]||a.elem.style&&null!=a.elem.style[a.prop]?(b=n.css(a.elem,a.prop,""),b&&"auto"!==b?b:0):a.elem[a.prop]},set:function(a){n.fx.step[a.prop]?n.fx.step[a.prop](a):a.elem.style&&(null!=a.elem.style[n.cssProps[a.prop]]||n.cssHooks[a.prop])?n.style(a.elem,a.prop,a.now+a.unit):a.elem[a.prop]=a.now}}},$b.propHooks.scrollTop=$b.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},n.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2}},n.fx=$b.prototype.init,n.fx.step={};var _b,ac,bc=/^(?:toggle|show|hide)$/,cc=new RegExp("^(?:([+-])=|)("+T+")([a-z%]*)$","i"),dc=/queueHooks$/,ec=[jc],fc={"*":[function(a,b){var c=this.createTween(a,b),d=c.cur(),e=cc.exec(b),f=e&&e[3]||(n.cssNumber[a]?"":"px"),g=(n.cssNumber[a]||"px"!==f&&+d)&&cc.exec(n.css(c.elem,a)),h=1,i=20;if(g&&g[3]!==f){f=f||g[3],e=e||[],g=+d||1;do h=h||".5",g/=h,n.style(c.elem,a,g+f);while(h!==(h=c.cur()/d)&&1!==h&&--i)}return e&&(g=c.start=+g||+d||0,c.unit=f,c.end=e[1]?g+(e[1]+1)*e[2]:+e[2]),c}]};function gc(){return setTimeout(function(){_b=void 0}),_b=n.now()}function hc(a,b){var c,d={height:a},e=0;for(b=b?1:0;4>e;e+=2-b)c=U[e],d["margin"+c]=d["padding"+c]=a;return b&&(d.opacity=d.width=a),d}function ic(a,b,c){for(var d,e=(fc[b]||[]).concat(fc["*"]),f=0,g=e.length;g>f;f++)if(d=e[f].call(c,b,a))return d}function jc(a,b,c){var d,e,f,g,h,i,j,k,m=this,o={},p=a.style,q=a.nodeType&&V(a),r=n._data(a,"fxshow");c.queue||(h=n._queueHooks(a,"fx"),null==h.unqueued&&(h.unqueued=0,i=h.empty.fire,h.empty.fire=function(){h.unqueued||i()}),h.unqueued++,m.always(function(){m.always(function(){h.unqueued--,n.queue(a,"fx").length||h.empty.fire()})})),1===a.nodeType&&("height"in b||"width"in b)&&(c.overflow=[p.overflow,p.overflowX,p.overflowY],j=n.css(a,"display"),k=Gb(a.nodeName),"none"===j&&(j=k),"inline"===j&&"none"===n.css(a,"float")&&(l.inlineBlockNeedsLayout&&"inline"!==k?p.zoom=1:p.display="inline-block")),c.overflow&&(p.overflow="hidden",l.shrinkWrapBlocks()||m.always(function(){p.overflow=c.overflow[0],p.overflowX=c.overflow[1],p.overflowY=c.overflow[2]}));for(d in b)if(e=b[d],bc.exec(e)){if(delete b[d],f=f||"toggle"===e,e===(q?"hide":"show")){if("show"!==e||!r||void 0===r[d])continue;q=!0}o[d]=r&&r[d]||n.style(a,d)}if(!n.isEmptyObject(o)){r?"hidden"in r&&(q=r.hidden):r=n._data(a,"fxshow",{}),f&&(r.hidden=!q),q?n(a).show():m.done(function(){n(a).hide()}),m.done(function(){var b;n._removeData(a,"fxshow");for(b in o)n.style(a,b,o[b])});for(d in o)g=ic(q?r[d]:0,d,m),d in r||(r[d]=g.start,q&&(g.end=g.start,g.start="width"===d||"height"===d?1:0))}}function kc(a,b){var c,d,e,f,g;for(c in a)if(d=n.camelCase(c),e=b[d],f=a[c],n.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=n.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function lc(a,b,c){var d,e,f=0,g=ec.length,h=n.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=_b||gc(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;i>g;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),1>f&&i?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:n.extend({},b),opts:n.extend(!0,{specialEasing:{}},c),originalProperties:b,originalOptions:c,startTime:_b||gc(),duration:c.duration,tweens:[],createTween:function(b,c){var d=n.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;d>c;c++)j.tweens[c].run(1);return b?h.resolveWith(a,[j,b]):h.rejectWith(a,[j,b]),this}}),k=j.props;for(kc(k,j.opts.specialEasing);g>f;f++)if(d=ec[f].call(j,a,k,j.opts))return d;return n.map(k,ic,j),n.isFunction(j.opts.start)&&j.opts.start.call(a,j),n.fx.timer(n.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}n.Animation=n.extend(lc,{tweener:function(a,b){n.isFunction(a)?(b=a,a=["*"]):a=a.split(" ");for(var c,d=0,e=a.length;e>d;d++)c=a[d],fc[c]=fc[c]||[],fc[c].unshift(b)},prefilter:function(a,b){b?ec.unshift(a):ec.push(a)}}),n.speed=function(a,b,c){var d=a&&"object"==typeof a?n.extend({},a):{complete:c||!c&&b||n.isFunction(a)&&a,duration:a,easing:c&&b||b&&!n.isFunction(b)&&b};return d.duration=n.fx.off?0:"number"==typeof d.duration?d.duration:d.duration in n.fx.speeds?n.fx.speeds[d.duration]:n.fx.speeds._default,(null==d.queue||d.queue===!0)&&(d.queue="fx"),d.old=d.complete,d.complete=function(){n.isFunction(d.old)&&d.old.call(this),d.queue&&n.dequeue(this,d.queue)},d},n.fn.extend({fadeTo:function(a,b,c,d){return this.filter(V).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=n.isEmptyObject(a),f=n.speed(b,c,d),g=function(){var b=lc(this,n.extend({},a),f);(e||n._data(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=n.timers,g=n._data(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&dc.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));(b||!c)&&n.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=n._data(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=n.timers,g=d?d.length:0;for(c.finish=!0,n.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;g>b;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),n.each(["toggle","show","hide"],function(a,b){var c=n.fn[b];n.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(hc(b,!0),a,d,e)}}),n.each({slideDown:hc("show"),slideUp:hc("hide"),slideToggle:hc("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){n.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),n.timers=[],n.fx.tick=function(){var a,b=n.timers,c=0;for(_b=n.now();c<b.length;c++)a=b[c],a()||b[c]!==a||b.splice(c--,1);b.length||n.fx.stop(),_b=void 0},n.fx.timer=function(a){n.timers.push(a),a()?n.fx.start():n.timers.pop()},n.fx.interval=13,n.fx.start=function(){ac||(ac=setInterval(n.fx.tick,n.fx.interval))},n.fx.stop=function(){clearInterval(ac),ac=null},n.fx.speeds={slow:600,fast:200,_default:400},n.fn.delay=function(a,b){return a=n.fx?n.fx.speeds[a]||a:a,b=b||"fx",this.queue(b,function(b,c){var d=setTimeout(b,a);c.stop=function(){clearTimeout(d)}})},function(){var a,b,c,d,e=z.createElement("div");e.setAttribute("className","t"),e.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",a=e.getElementsByTagName("a")[0],c=z.createElement("select"),d=c.appendChild(z.createElement("option")),b=e.getElementsByTagName("input")[0],a.style.cssText="top:1px",l.getSetAttribute="t"!==e.className,l.style=/top/.test(a.getAttribute("style")),l.hrefNormalized="/a"===a.getAttribute("href"),l.checkOn=!!b.value,l.optSelected=d.selected,l.enctype=!!z.createElement("form").enctype,c.disabled=!0,l.optDisabled=!d.disabled,b=z.createElement("input"),b.setAttribute("value",""),l.input=""===b.getAttribute("value"),b.value="t",b.setAttribute("type","radio"),l.radioValue="t"===b.value,a=b=c=d=e=null}();var mc=/\r/g;n.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=n.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,n(this).val()):a,null==e?e="":"number"==typeof e?e+="":n.isArray(e)&&(e=n.map(e,function(a){return null==a?"":a+""})),b=n.valHooks[this.type]||n.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=n.valHooks[e.type]||n.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(mc,""):null==c?"":c)}}}),n.extend({valHooks:{option:{get:function(a){var b=n.find.attr(a,"value");return null!=b?b:n.text(a)}},select:{get:function(a){for(var b,c,d=a.options,e=a.selectedIndex,f="select-one"===a.type||0>e,g=f?null:[],h=f?e+1:d.length,i=0>e?h:f?e:0;h>i;i++)if(c=d[i],!(!c.selected&&i!==e||(l.optDisabled?c.disabled:null!==c.getAttribute("disabled"))||c.parentNode.disabled&&n.nodeName(c.parentNode,"optgroup"))){if(b=n(c).val(),f)return b;g.push(b)}return g},set:function(a,b){var c,d,e=a.options,f=n.makeArray(b),g=e.length;while(g--)if(d=e[g],n.inArray(n.valHooks.option.get(d),f)>=0)try{d.selected=c=!0}catch(h){d.scrollHeight}else d.selected=!1;return c||(a.selectedIndex=-1),e}}}}),n.each(["radio","checkbox"],function(){n.valHooks[this]={set:function(a,b){return n.isArray(b)?a.checked=n.inArray(n(a).val(),b)>=0:void 0}},l.checkOn||(n.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var nc,oc,pc=n.expr.attrHandle,qc=/^(?:checked|selected)$/i,rc=l.getSetAttribute,sc=l.input;n.fn.extend({attr:function(a,b){return W(this,n.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){n.removeAttr(this,a)})}}),n.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(a&&3!==f&&8!==f&&2!==f)return typeof a.getAttribute===L?n.prop(a,b,c):(1===f&&n.isXMLDoc(a)||(b=b.toLowerCase(),d=n.attrHooks[b]||(n.expr.match.bool.test(b)?oc:nc)),void 0===c?d&&"get"in d&&null!==(e=d.get(a,b))?e:(e=n.find.attr(a,b),null==e?void 0:e):null!==c?d&&"set"in d&&void 0!==(e=d.set(a,c,b))?e:(a.setAttribute(b,c+""),c):void n.removeAttr(a,b))},removeAttr:function(a,b){var c,d,e=0,f=b&&b.match(F);if(f&&1===a.nodeType)while(c=f[e++])d=n.propFix[c]||c,n.expr.match.bool.test(c)?sc&&rc||!qc.test(c)?a[d]=!1:a[n.camelCase("default-"+c)]=a[d]=!1:n.attr(a,c,""),a.removeAttribute(rc?c:d)},attrHooks:{type:{set:function(a,b){if(!l.radioValue&&"radio"===b&&n.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}}}),oc={set:function(a,b,c){return b===!1?n.removeAttr(a,c):sc&&rc||!qc.test(c)?a.setAttribute(!rc&&n.propFix[c]||c,c):a[n.camelCase("default-"+c)]=a[c]=!0,c}},n.each(n.expr.match.bool.source.match(/\w+/g),function(a,b){var c=pc[b]||n.find.attr;pc[b]=sc&&rc||!qc.test(b)?function(a,b,d){var e,f;return d||(f=pc[b],pc[b]=e,e=null!=c(a,b,d)?b.toLowerCase():null,pc[b]=f),e}:function(a,b,c){return c?void 0:a[n.camelCase("default-"+b)]?b.toLowerCase():null}}),sc&&rc||(n.attrHooks.value={set:function(a,b,c){return n.nodeName(a,"input")?void(a.defaultValue=b):nc&&nc.set(a,b,c)}}),rc||(nc={set:function(a,b,c){var d=a.getAttributeNode(c);return d||a.setAttributeNode(d=a.ownerDocument.createAttribute(c)),d.value=b+="","value"===c||b===a.getAttribute(c)?b:void 0}},pc.id=pc.name=pc.coords=function(a,b,c){var d;return c?void 0:(d=a.getAttributeNode(b))&&""!==d.value?d.value:null},n.valHooks.button={get:function(a,b){var c=a.getAttributeNode(b);return c&&c.specified?c.value:void 0},set:nc.set},n.attrHooks.contenteditable={set:function(a,b,c){nc.set(a,""===b?!1:b,c)}},n.each(["width","height"],function(a,b){n.attrHooks[b]={set:function(a,c){return""===c?(a.setAttribute(b,"auto"),c):void 0}}})),l.style||(n.attrHooks.style={get:function(a){return a.style.cssText||void 0},set:function(a,b){return a.style.cssText=b+""}});var tc=/^(?:input|select|textarea|button|object)$/i,uc=/^(?:a|area)$/i;n.fn.extend({prop:function(a,b){return W(this,n.prop,a,b,arguments.length>1)},removeProp:function(a){return a=n.propFix[a]||a,this.each(function(){try{this[a]=void 0,delete this[a]}catch(b){}})}}),n.extend({propFix:{"for":"htmlFor","class":"className"},prop:function(a,b,c){var d,e,f,g=a.nodeType;if(a&&3!==g&&8!==g&&2!==g)return f=1!==g||!n.isXMLDoc(a),f&&(b=n.propFix[b]||b,e=n.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=n.find.attr(a,"tabindex");return b?parseInt(b,10):tc.test(a.nodeName)||uc.test(a.nodeName)&&a.href?0:-1}}}}),l.hrefNormalized||n.each(["href","src"],function(a,b){n.propHooks[b]={get:function(a){return a.getAttribute(b,4)}}}),l.optSelected||(n.propHooks.selected={get:function(a){var b=a.parentNode;return b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex),null}}),n.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){n.propFix[this.toLowerCase()]=this}),l.enctype||(n.propFix.enctype="encoding");var vc=/[\t\r\n\f]/g;n.fn.extend({addClass:function(a){var b,c,d,e,f,g,h=0,i=this.length,j="string"==typeof a&&a;if(n.isFunction(a))return this.each(function(b){n(this).addClass(a.call(this,b,this.className))});if(j)for(b=(a||"").match(F)||[];i>h;h++)if(c=this[h],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(vc," "):" ")){f=0;while(e=b[f++])d.indexOf(" "+e+" ")<0&&(d+=e+" ");g=n.trim(d),c.className!==g&&(c.className=g)}return this},removeClass:function(a){var b,c,d,e,f,g,h=0,i=this.length,j=0===arguments.length||"string"==typeof a&&a;if(n.isFunction(a))return this.each(function(b){n(this).removeClass(a.call(this,b,this.className))});if(j)for(b=(a||"").match(F)||[];i>h;h++)if(c=this[h],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(vc," "):"")){f=0;while(e=b[f++])while(d.indexOf(" "+e+" ")>=0)d=d.replace(" "+e+" "," ");g=a?n.trim(d):"",c.className!==g&&(c.className=g)}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):this.each(n.isFunction(a)?function(c){n(this).toggleClass(a.call(this,c,this.className,b),b)}:function(){if("string"===c){var b,d=0,e=n(this),f=a.match(F)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else(c===L||"boolean"===c)&&(this.className&&n._data(this,"__className__",this.className),this.className=this.className||a===!1?"":n._data(this,"__className__")||"")})},hasClass:function(a){for(var b=" "+a+" ",c=0,d=this.length;d>c;c++)if(1===this[c].nodeType&&(" "+this[c].className+" ").replace(vc," ").indexOf(b)>=0)return!0;return!1}}),n.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){n.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),n.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}});var wc=n.now(),xc=/\?/,yc=/(,)|(\[|{)|(}|])|"(?:[^"\\\r\n]|\\["\\\/bfnrt]|\\u[\da-fA-F]{4})*"\s*:?|true|false|null|-?(?!0\d)\d+(?:\.\d+|)(?:[eE][+-]?\d+|)/g;n.parseJSON=function(b){if(a.JSON&&a.JSON.parse)return a.JSON.parse(b+"");var c,d=null,e=n.trim(b+"");return e&&!n.trim(e.replace(yc,function(a,b,e,f){return c&&b&&(d=0),0===d?a:(c=e||b,d+=!f-!e,"")}))?Function("return "+e)():n.error("Invalid JSON: "+b)},n.parseXML=function(b){var c,d;if(!b||"string"!=typeof b)return null;try{a.DOMParser?(d=new DOMParser,c=d.parseFromString(b,"text/xml")):(c=new ActiveXObject("Microsoft.XMLDOM"),c.async="false",c.loadXML(b))}catch(e){c=void 0}return c&&c.documentElement&&!c.getElementsByTagName("parsererror").length||n.error("Invalid XML: "+b),c};var zc,Ac,Bc=/#.*$/,Cc=/([?&])_=[^&]*/,Dc=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,Ec=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Fc=/^(?:GET|HEAD)$/,Gc=/^\/\//,Hc=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,Ic={},Jc={},Kc="*/".concat("*");try{Ac=location.href}catch(Lc){Ac=z.createElement("a"),Ac.href="",Ac=Ac.href}zc=Hc.exec(Ac.toLowerCase())||[];function Mc(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(F)||[];if(n.isFunction(c))while(d=f[e++])"+"===d.charAt(0)?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Nc(a,b,c,d){var e={},f=a===Jc;function g(h){var i;return e[h]=!0,n.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Oc(a,b){var c,d,e=n.ajaxSettings.flatOptions||{};for(d in b)void 0!==b[d]&&((e[d]?a:c||(c={}))[d]=b[d]);return c&&n.extend(!0,a,c),a}function Pc(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===e&&(e=a.mimeType||b.getResponseHeader("Content-Type"));if(e)for(g in h)if(h[g]&&h[g].test(e)){i.unshift(g);break}if(i[0]in c)f=i[0];else{for(g in c){if(!i[0]||a.converters[g+" "+i[0]]){f=g;break}d||(d=g)}f=f||d}return f?(f!==i[0]&&i.unshift(f),c[f]):void 0}function Qc(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}n.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Ac,type:"GET",isLocal:Ec.test(zc[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Kc,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":n.parseJSON,"text xml":n.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Oc(Oc(a,n.ajaxSettings),b):Oc(n.ajaxSettings,a)},ajaxPrefilter:Mc(Ic),ajaxTransport:Mc(Jc),ajax:function(a,b){"object"==typeof a&&(b=a,a=void 0),b=b||{};var c,d,e,f,g,h,i,j,k=n.ajaxSetup({},b),l=k.context||k,m=k.context&&(l.nodeType||l.jquery)?n(l):n.event,o=n.Deferred(),p=n.Callbacks("once memory"),q=k.statusCode||{},r={},s={},t=0,u="canceled",v={readyState:0,getResponseHeader:function(a){var b;if(2===t){if(!j){j={};while(b=Dc.exec(f))j[b[1].toLowerCase()]=b[2]}b=j[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return 2===t?f:null},setRequestHeader:function(a,b){var c=a.toLowerCase();return t||(a=s[c]=s[c]||a,r[a]=b),this},overrideMimeType:function(a){return t||(k.mimeType=a),this},statusCode:function(a){var b;if(a)if(2>t)for(b in a)q[b]=[q[b],a[b]];else v.always(a[v.status]);return this},abort:function(a){var b=a||u;return i&&i.abort(b),x(0,b),this}};if(o.promise(v).complete=p.add,v.success=v.done,v.error=v.fail,k.url=((a||k.url||Ac)+"").replace(Bc,"").replace(Gc,zc[1]+"//"),k.type=b.method||b.type||k.method||k.type,k.dataTypes=n.trim(k.dataType||"*").toLowerCase().match(F)||[""],null==k.crossDomain&&(c=Hc.exec(k.url.toLowerCase()),k.crossDomain=!(!c||c[1]===zc[1]&&c[2]===zc[2]&&(c[3]||("http:"===c[1]?"80":"443"))===(zc[3]||("http:"===zc[1]?"80":"443")))),k.data&&k.processData&&"string"!=typeof k.data&&(k.data=n.param(k.data,k.traditional)),Nc(Ic,k,b,v),2===t)return v;h=k.global,h&&0===n.active++&&n.event.trigger("ajaxStart"),k.type=k.type.toUpperCase(),k.hasContent=!Fc.test(k.type),e=k.url,k.hasContent||(k.data&&(e=k.url+=(xc.test(e)?"&":"?")+k.data,delete k.data),k.cache===!1&&(k.url=Cc.test(e)?e.replace(Cc,"$1_="+wc++):e+(xc.test(e)?"&":"?")+"_="+wc++)),k.ifModified&&(n.lastModified[e]&&v.setRequestHeader("If-Modified-Since",n.lastModified[e]),n.etag[e]&&v.setRequestHeader("If-None-Match",n.etag[e])),(k.data&&k.hasContent&&k.contentType!==!1||b.contentType)&&v.setRequestHeader("Content-Type",k.contentType),v.setRequestHeader("Accept",k.dataTypes[0]&&k.accepts[k.dataTypes[0]]?k.accepts[k.dataTypes[0]]+("*"!==k.dataTypes[0]?", "+Kc+"; q=0.01":""):k.accepts["*"]);for(d in k.headers)v.setRequestHeader(d,k.headers[d]);if(k.beforeSend&&(k.beforeSend.call(l,v,k)===!1||2===t))return v.abort();u="abort";for(d in{success:1,error:1,complete:1})v[d](k[d]);if(i=Nc(Jc,k,b,v)){v.readyState=1,h&&m.trigger("ajaxSend",[v,k]),k.async&&k.timeout>0&&(g=setTimeout(function(){v.abort("timeout")},k.timeout));try{t=1,i.send(r,x)}catch(w){if(!(2>t))throw w;x(-1,w)}}else x(-1,"No Transport");function x(a,b,c,d){var j,r,s,u,w,x=b;2!==t&&(t=2,g&&clearTimeout(g),i=void 0,f=d||"",v.readyState=a>0?4:0,j=a>=200&&300>a||304===a,c&&(u=Pc(k,v,c)),u=Qc(k,u,v,j),j?(k.ifModified&&(w=v.getResponseHeader("Last-Modified"),w&&(n.lastModified[e]=w),w=v.getResponseHeader("etag"),w&&(n.etag[e]=w)),204===a||"HEAD"===k.type?x="nocontent":304===a?x="notmodified":(x=u.state,r=u.data,s=u.error,j=!s)):(s=x,(a||!x)&&(x="error",0>a&&(a=0))),v.status=a,v.statusText=(b||x)+"",j?o.resolveWith(l,[r,x,v]):o.rejectWith(l,[v,x,s]),v.statusCode(q),q=void 0,h&&m.trigger(j?"ajaxSuccess":"ajaxError",[v,k,j?r:s]),p.fireWith(l,[v,x]),h&&(m.trigger("ajaxComplete",[v,k]),--n.active||n.event.trigger("ajaxStop")))}return v},getJSON:function(a,b,c){return n.get(a,b,c,"json")},getScript:function(a,b){return n.get(a,void 0,b,"script")}}),n.each(["get","post"],function(a,b){n[b]=function(a,c,d,e){return n.isFunction(c)&&(e=e||d,d=c,c=void 0),n.ajax({url:a,type:b,dataType:e,data:c,success:d})}}),n.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){n.fn[b]=function(a){return this.on(b,a)}}),n._evalUrl=function(a){return n.ajax({url:a,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})},n.fn.extend({wrapAll:function(a){if(n.isFunction(a))return this.each(function(b){n(this).wrapAll(a.call(this,b))});if(this[0]){var b=n(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&1===a.firstChild.nodeType)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){return this.each(n.isFunction(a)?function(b){n(this).wrapInner(a.call(this,b))}:function(){var b=n(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=n.isFunction(a);return this.each(function(c){n(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){n.nodeName(this,"body")||n(this).replaceWith(this.childNodes)}).end()}}),n.expr.filters.hidden=function(a){return a.offsetWidth<=0&&a.offsetHeight<=0||!l.reliableHiddenOffsets()&&"none"===(a.style&&a.style.display||n.css(a,"display"))},n.expr.filters.visible=function(a){return!n.expr.filters.hidden(a)};var Rc=/%20/g,Sc=/\[\]$/,Tc=/\r?\n/g,Uc=/^(?:submit|button|image|reset|file)$/i,Vc=/^(?:input|select|textarea|keygen)/i;function Wc(a,b,c,d){var e;if(n.isArray(b))n.each(b,function(b,e){c||Sc.test(a)?d(a,e):Wc(a+"["+("object"==typeof e?b:"")+"]",e,c,d)});else if(c||"object"!==n.type(b))d(a,b);else for(e in b)Wc(a+"["+e+"]",b[e],c,d)}n.param=function(a,b){var c,d=[],e=function(a,b){b=n.isFunction(b)?b():null==b?"":b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};if(void 0===b&&(b=n.ajaxSettings&&n.ajaxSettings.traditional),n.isArray(a)||a.jquery&&!n.isPlainObject(a))n.each(a,function(){e(this.name,this.value)});else for(c in a)Wc(c,a[c],b,e);return d.join("&").replace(Rc,"+")},n.fn.extend({serialize:function(){return n.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=n.prop(this,"elements");return a?n.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!n(this).is(":disabled")&&Vc.test(this.nodeName)&&!Uc.test(a)&&(this.checked||!X.test(a))}).map(function(a,b){var c=n(this).val();return null==c?null:n.isArray(c)?n.map(c,function(a){return{name:b.name,value:a.replace(Tc,"\r\n")}}):{name:b.name,value:c.replace(Tc,"\r\n")}}).get()}}),n.ajaxSettings.xhr=void 0!==a.ActiveXObject?function(){return!this.isLocal&&/^(get|post|head|put|delete|options)$/i.test(this.type)&&$c()||_c()}:$c;var Xc=0,Yc={},Zc=n.ajaxSettings.xhr();a.ActiveXObject&&n(a).on("unload",function(){for(var a in Yc)Yc[a](void 0,!0)}),l.cors=!!Zc&&"withCredentials"in Zc,Zc=l.ajax=!!Zc,Zc&&n.ajaxTransport(function(a){if(!a.crossDomain||l.cors){var b;return{send:function(c,d){var e,f=a.xhr(),g=++Xc;if(f.open(a.type,a.url,a.async,a.username,a.password),a.xhrFields)for(e in a.xhrFields)f[e]=a.xhrFields[e];a.mimeType&&f.overrideMimeType&&f.overrideMimeType(a.mimeType),a.crossDomain||c["X-Requested-With"]||(c["X-Requested-With"]="XMLHttpRequest");for(e in c)void 0!==c[e]&&f.setRequestHeader(e,c[e]+"");f.send(a.hasContent&&a.data||null),b=function(c,e){var h,i,j;if(b&&(e||4===f.readyState))if(delete Yc[g],b=void 0,f.onreadystatechange=n.noop,e)4!==f.readyState&&f.abort();else{j={},h=f.status,"string"==typeof f.responseText&&(j.text=f.responseText);try{i=f.statusText}catch(k){i=""}h||!a.isLocal||a.crossDomain?1223===h&&(h=204):h=j.text?200:404}j&&d(h,i,j,f.getAllResponseHeaders())},a.async?4===f.readyState?setTimeout(b):f.onreadystatechange=Yc[g]=b:b()},abort:function(){b&&b(void 0,!0)}}}});function $c(){try{return new a.XMLHttpRequest}catch(b){}}function _c(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}n.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(a){return n.globalEval(a),a}}}),n.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),n.ajaxTransport("script",function(a){if(a.crossDomain){var b,c=z.head||n("head")[0]||z.documentElement;return{send:function(d,e){b=z.createElement("script"),b.async=!0,a.scriptCharset&&(b.charset=a.scriptCharset),b.src=a.url,b.onload=b.onreadystatechange=function(a,c){(c||!b.readyState||/loaded|complete/.test(b.readyState))&&(b.onload=b.onreadystatechange=null,b.parentNode&&b.parentNode.removeChild(b),b=null,c||e(200,"success"))},c.insertBefore(b,c.firstChild)},abort:function(){b&&b.onload(void 0,!0)}}}});var ad=[],bd=/(=)\?(?=&|$)|\?\?/;n.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=ad.pop()||n.expando+"_"+wc++;return this[a]=!0,a}}),n.ajaxPrefilter("json jsonp",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(bd.test(b.url)?"url":"string"==typeof b.data&&!(b.contentType||"").indexOf("application/x-www-form-urlencoded")&&bd.test(b.data)&&"data");return h||"jsonp"===b.dataTypes[0]?(e=b.jsonpCallback=n.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(bd,"$1"+e):b.jsonp!==!1&&(b.url+=(xc.test(b.url)?"&":"?")+b.jsonp+"="+e),b.converters["script json"]=function(){return g||n.error(e+" was not called"),g[0]},b.dataTypes[0]="json",f=a[e],a[e]=function(){g=arguments},d.always(function(){a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,ad.push(e)),g&&n.isFunction(f)&&f(g[0]),g=f=void 0}),"script"):void 0}),n.parseHTML=function(a,b,c){if(!a||"string"!=typeof a)return null;"boolean"==typeof b&&(c=b,b=!1),b=b||z;var d=v.exec(a),e=!c&&[];return d?[b.createElement(d[1])]:(d=n.buildFragment([a],b,e),e&&e.length&&n(e).remove(),n.merge([],d.childNodes))};var cd=n.fn.load;n.fn.load=function(a,b,c){if("string"!=typeof a&&cd)return cd.apply(this,arguments);var d,e,f,g=this,h=a.indexOf(" ");return h>=0&&(d=a.slice(h,a.length),a=a.slice(0,h)),n.isFunction(b)?(c=b,b=void 0):b&&"object"==typeof b&&(f="POST"),g.length>0&&n.ajax({url:a,type:f,dataType:"html",data:b}).done(function(a){e=arguments,g.html(d?n("<div>").append(n.parseHTML(a)).find(d):a)}).complete(c&&function(a,b){g.each(c,e||[a.responseText,b,a])}),this},n.expr.filters.animated=function(a){return n.grep(n.timers,function(b){return a===b.elem}).length};var dd=a.document.documentElement;function ed(a){return n.isWindow(a)?a:9===a.nodeType?a.defaultView||a.parentWindow:!1}n.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=n.css(a,"position"),l=n(a),m={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=n.css(a,"top"),i=n.css(a,"left"),j=("absolute"===k||"fixed"===k)&&n.inArray("auto",[f,i])>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),n.isFunction(b)&&(b=b.call(a,c,h)),null!=b.top&&(m.top=b.top-h.top+g),null!=b.left&&(m.left=b.left-h.left+e),"using"in b?b.using.call(a,m):l.css(m)}},n.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){n.offset.setOffset(this,a,b)});var b,c,d={top:0,left:0},e=this[0],f=e&&e.ownerDocument;if(f)return b=f.documentElement,n.contains(b,e)?(typeof e.getBoundingClientRect!==L&&(d=e.getBoundingClientRect()),c=ed(f),{top:d.top+(c.pageYOffset||b.scrollTop)-(b.clientTop||0),left:d.left+(c.pageXOffset||b.scrollLeft)-(b.clientLeft||0)}):d},position:function(){if(this[0]){var a,b,c={top:0,left:0},d=this[0];return"fixed"===n.css(d,"position")?b=d.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),n.nodeName(a[0],"html")||(c=a.offset()),c.top+=n.css(a[0],"borderTopWidth",!0),c.left+=n.css(a[0],"borderLeftWidth",!0)),{top:b.top-c.top-n.css(d,"marginTop",!0),left:b.left-c.left-n.css(d,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||dd;while(a&&!n.nodeName(a,"html")&&"static"===n.css(a,"position"))a=a.offsetParent;return a||dd})}}),n.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,b){var c=/Y/.test(b);n.fn[a]=function(d){return W(this,function(a,d,e){var f=ed(a);return void 0===e?f?b in f?f[b]:f.document.documentElement[d]:a[d]:void(f?f.scrollTo(c?n(f).scrollLeft():e,c?e:n(f).scrollTop()):a[d]=e)},a,d,arguments.length,null)}}),n.each(["top","left"],function(a,b){n.cssHooks[b]=Mb(l.pixelPosition,function(a,c){return c?(c=Kb(a,b),Ib.test(c)?n(a).position()[b]+"px":c):void 0})}),n.each({Height:"height",Width:"width"},function(a,b){n.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){n.fn[d]=function(d,e){var f=arguments.length&&(c||"boolean"!=typeof d),g=c||(d===!0||e===!0?"margin":"border");return W(this,function(b,c,d){var e;return n.isWindow(b)?b.document.documentElement["client"+a]:9===b.nodeType?(e=b.documentElement,Math.max(b.body["scroll"+a],e["scroll"+a],b.body["offset"+a],e["offset"+a],e["client"+a])):void 0===d?n.css(b,c,g):n.style(b,c,d,g)},b,f?d:void 0,f,null)}})}),n.fn.size=function(){return this.length},n.fn.andSelf=n.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return n});var fd=a.jQuery,gd=a.$;return n.noConflict=function(b){return a.$===n&&(a.$=gd),b&&a.jQuery===n&&(a.jQuery=fd),n},typeof b===L&&(a.jQuery=a.$=n),n}); diff --git a/snf-admin-app/synnefo_admin/admin/static/js/stats.js b/snf-admin-app/synnefo_admin/admin/static/js/stats.js new file mode 100644 index 0000000000000000000000000000000000000000..8d2e189a83a5f483bca1cf3b0cf850705cfae714 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/js/stats.js @@ -0,0 +1,55 @@ +$(document).ready(function() { + if($('#stats').length > 0) { + function syntaxHighlight(json) { + if (typeof json != 'string') { + json = JSON.stringify(json, undefined, 4); // the number of levels tah json has + } + json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); + return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) { + var cls = 'number'; + if (/^"/.test(match)) { + if (/:$/.test(match)) { + cls = 'key'; + } else { + cls = 'string'; + } + } else if (/true|false/.test(match)) { + cls = 'boolean'; + } else if (/null/.test(match)) { + cls = 'null'; + } + return '<span class="' + cls + '">' + match + '</span>'; + }); + } + + $.getJSON( "/stats/", function( data ) { + $( "<pre/>", { + "class": "stats", + html: syntaxHighlight(data) + }).appendTo("#stats"); + }); + } + + $('.stats .custom-btn').click(function(e){ + var url = $(this).attr('href'); + var download = $(this).attr('download'); + var d = new Date(); + var month = d.getMonth()+1; + var day = d.getDate(); + var output = '_' + d.getFullYear() + '_' + + ((''+month).length<2 ? '0' : '') + month + '_' + + ((''+day).length<2 ? '0' : '') + day; + var fName = download + output; + $(this).attr('download', fName); + var spinner = $(this).parents('section').find('.spinner'); + spinner.show(); + $.ajax({ + url: url, + dataType: "json", + success: function(data){ + spinner.hide(); + }, + }) + }); + +}); diff --git a/snf-admin-app/synnefo_admin/admin/static/js/tables.js b/snf-admin-app/synnefo_admin/admin/static/js/tables.js new file mode 100644 index 0000000000000000000000000000000000000000..678c11522f2d23708e3f417dd0d1ef0543466bb8 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/js/tables.js @@ -0,0 +1,865 @@ +$(document).ready(function() { + + var $actionbar = $('.actionbar'); + + if($actionbar.length > 0) { + sticker(); + } + else { + $('.filters').addClass('no-margin-left'); + } + + var $lastClicked = null; + var $prevClicked = null; + var selected = { + items: [], + actions: {} + }; + + var availableActions = {}; + var allowedActions= {}; + + /* Actionbar */ + $('.actionbar a').each(function() { + availableActions[$(this).data('action')] = true; + }); + + for(var prop in availableActions) { + allowedActions[prop] = true; + } + + /* If the sidebar link is not disabled show the corresponding modal */ + $('.actionbar a').click(function(e) { + if($(this).hasClass('disabled')) { + e.preventDefault(); + e.stopPropagation(); + } + else { + var modal = $(this).data('target'); + drawModal(modal); + } + }); + + + /* Table */ + /* For the tables we have used DataTables 1.10.0 */ + var url = $('#table-items-total').data("url"); + var serverside = Boolean($('#table-items-total').data("server-side")); + var table; + // var tableSelected; + $.fn.dataTable.ext.legacy.ajax = true; + var extraData; + // sets the classes of the btns that are used for navigation throw the pages (next, prev, 1, 2, 3...) + // $.fn.dataTableExt.oStdClasses.sPageButton = "btn btn-primary"; + var maxCellChar = 18; + var tableDomID = '#table-items-total'; + var tableSelectedDomID = '#table-items-selected' + var tableMassiveDomID = '#total-list' + table = $(tableDomID).DataTable({ + "autoWidth": false, + "paging": true, + "searching": false, + "stateSave": true, + "stateDuration": 0, + "processing": true, + "serverSide": serverside, + "ajax": { + "url": url, + "data": function(data, callback, settings) { + var prefix = 'sSearch_'; + + if(!_.isEmpty(snf.filters)) { + for (var prop in snf.filters) { + data[prefix+prop] = snf.filters[prop]; + } + } + }, + "dataSrc" : function(response) { + if(response.aaData.length != 0) { + var rowsArray = response.aaData; + var rowL = rowsArray.length; + var extraCol = rowsArray[0].length; //last column + for (var i=0; i<rowL; i++) { + rowsArray[i][extraCol] = response.extra[i] + } + } + return response.aaData; + } + }, + "columnDefs": [ + { + "targets": 0, + "render": function(data, type, rowData) { + return checkboxTemplate(data, 'unchecked'); + } + }, + { + "targets": -1, // the first column counting from the right is "Summary" + "orderable": false, + "render": function(data, type, rowData) { + return extraTemplate(data); + } + }, + // "targets": '_all' this must be the last item of the array + { + "targets": '_all', + "render": function( data, type, row, meta ) { + if(data.length > maxCellChar) { + return _.template(snf.tables.html.trimedCell, {data: data, trimmedData: data.substring(0, maxCellChar)}); + } + else { + return data; + } + } + }, + ], + "order": [0, "asc"], + "createdRow": function(row, data, dataIndex) { + var extraIndex = data.length - 1; + row.id = data[extraIndex].id.value; //sets the dom id + clickSummary(row); + clickDetails(row); + }, + + "dom": '<"custom-buttons">frtilp', + "language" : { + "sLengthMenu": 'Pagination _MENU_' + }, + "drawCallback": function(settings) { + isSelected(); + updateToggleAllSelect(); + $("[data-toggle=popover]").popover(); + } + }); + + if($actionbar.length > 0) { + var btns = snf.tables.html.reloadTable + snf.tables.html.selectPageBtn + snf.tables.html.selectAllBtn + snf.tables.html.clearSelected + snf.tables.html.toggleSelected + $("div.custom-buttons:not(.bottom)").html(btns); + } + else { + $("div.custom-buttons:not(.bottom)").html(snf.tables.html.reloadTable); + } + + $('.container').on('click', '.reload-table', function(e) { + e.preventDefault(); + $(tableDomID).dataTable().api().ajax.reload(); + }); + $('.notify').on('click', '.clear-reload', function(e) { + e.preventDefault(); + resetAll(tableDomID); + $(tableDomID).dataTable().api().ajax.reload(); + + }) + + + function isSelected() { + var tableLength = table.rows()[0].length; + var selectedL = selected.items.length; + if(selectedL !== 0 && tableLength !== 0) { + var dataLength = table.row(0).data().length + var extraIndex = dataLength - 1; + for(var j = 0; j<tableLength; j++) { // index of rows start from zero + for(var i = 0; i<selectedL; i++){ + if (selected.items[i].id === table.row(j).data()[extraIndex].id.value) { + $(table.row(j).nodes()).addClass('selected'); + break; + } + } + } + } + } + + var newTable = true; + $('.select-all-confirm').click(function(e) { + $(this).closest('.modal').addClass('in-progress'); + if(newTable) { + newTable = false; + countme = true; + $(tableMassiveDomID).DataTable({ + "paging": false, + "processing": false, + "serverSide": true, + "ajax": { + "url": url, + "data": function(data, callback, settings) { + + var prefix = 'sSearch_'; + + if(!$.isEmptyObject(snf.filters)) { + for (var prop in snf.filters) { + data[prefix+prop] = snf.filters[prop]; + } + } + }, + + "dataSrc" : function(response) { + if(response.aaData.length != 0) { + var rowsArray = response.aaData; + var rowL = rowsArray.length; + var extraCol = rowsArray[0].length; //last column + for (var i=0; i<rowL; i++) { + rowsArray[i][extraCol] = response.extra[i]; + } + } + return response.aaData; + } + }, + createdRow: function(row, data, dataIndex) { + if(countme) { + countme = false; + } + var info = data[data.length - 1]; + var newItem = addItem(info); + if(newItem !== null) { + enableActions(newItem.actions); + keepSelected(data); + if(dataIndex>=500 && dataIndex%500 === 0) { + setTimeout(function() { + return true; + }, 50); + } + } + }, + "drawCallback": function(settings) { + isSelected(); + updateCounter('.selected-num') + $('#massive-actions-warning').modal('hide') + $('#massive-actions-warning').removeClass('in-progress') + tableSelected.rows().draw(); + updateToggleAllSelect(); + updateClearAll(); + } + }); + } + else { + $(tableMassiveDomID).dataTable().api().ajax.reload(); + } + }); + + tableSelected = $(tableSelectedDomID).DataTable({ + // "stateSave": true, + "columnDefs": [ + { + "targets": 0, + "render": function(data, type, rowData) { + return checkboxTemplate(data, 'checked'); + } + }, + { + "targets": -1, // the first column counting from the right is "Summary" + "orderable": false, + "render": function(data, type, rowData) { + return extraTemplate(data); + }, + }, + // "targets": '_all' this must be the last item of the array + { + "targets": '_all', + "render": function( data, type, row, meta ) { + if(data.length > maxCellChar) { + return _.template(snf.tables.html.trimedCell, {data: data, trimmedData: data.substring(0, maxCellChar)}); + } + else { + return data; + } + } + }, + ], + "order": [0, "asc"], + "lengthMenu": [[5, 10, 25, 50, -1], [5, 10, 25, 50, "All"]], + "dom": 'frtilp', + "language" : { + "sLengthMenu": 'Pagination _MENU_' + }, + "createdRow": function(row, data, dataIndex) { + var extraIndex = data.length - 1; + row.id = 'selected-'+data[extraIndex].id.value; //sets the dom id + clickDetails(row); + clickSummary(row); + }, + }); + + function keepSelected(data, drawNow) { + //return; + if(drawNow) { + tableSelected.row.add(data).draw(); + } + else + tableSelected.row.add(data).node(); + }; + + + /* Removes a row from the table of selected items */ + function removeSelected(rowID) { + if(rowID === true) { + tableSelected.clear().draw() + } + else { + tableSelected.row('#selected-'+rowID).remove().draw(); + } + }; + + /* Applies style that indicates that a row from the main table is not selected */ + function deselectRow(itemID) { + table.row('#'+itemID).nodes().to$().removeClass('selected'); + } + + function updateDisplaySelected() { + if(selected.items.length > 0) { + $('a.toggle-selected').removeClass('disabled'); + } + else { + $('a.toggle-selected').addClass('disabled'); + } + } + + $(tableSelectedDomID).on('click', 'tbody tr td:first-child .select', function() { + var $tr = $(this).closest('tr'); + var column = $tr.find('td').length - 1; + var $trID = $tr.attr('id'); + var selectedRow = tableSelected.row('#'+$trID); + var itemID = tableSelected.cell('#'+$trID, column).data().id.value; + $tr.fadeOut('slow', function() { + selectedRow.remove().draw(); + table.row('#'+itemID).nodes().to$().removeClass('selected'); + deselectRow(itemID) + + }); + removeItem(itemID); + enableActions(undefined, true); + updateCounter('.selected-num'); + updateToggleAllSelect(); + }); + + + $(tableDomID).on('click', 'tbody tr .select', function(e) { + $prevClicked = $lastClicked; + $lastClicked = $(this).closest('tr'); + if(!e.shiftKey) { + selectRow($lastClicked, e.type); + } + else { + var select; + if($lastClicked.hasClass('selected')) { + select = false; + } + else { + select = true; + } + if(e.shiftKey && $prevClicked !== null && $lastClicked !== null) { + var startRow; + var start = $prevClicked.index(); + var end = $lastClicked.index(); + if(start < end) { + startRow = $prevClicked; + for (var i = start; i<=end; i++) { + if((select && !($(startRow).hasClass('selected'))) || (!select && $(startRow).hasClass('selected'))) { + selectRow(startRow); + } + startRow = startRow.next(); + } + } + else if(end < start) { + startRow = $prevClicked; + for (var i = start; i>=end; i--) { + if((select && !($(startRow).hasClass('selected'))) || (!select && $(startRow).hasClass('selected'))) { + selectRow(startRow); + } + startRow = startRow.prev(); + } + } + } + } + updateClearAll(); + }); + + $(document).bind('keydown', function(e){ + if(e.shiftKey && !$(e.target).is('input') && !$(e.target).is('textarea')) { + $(tableDomID).addClass('with-shift') + } + }); + + $(document).bind('keyup', function(e){ + if(e.which === 16 && !$(e.target).is('input') && !$(e.target).is('textarea')) { + deselectText(); + $(tableDomID).removeClass('with-shift') + } + }); + + function deselectText() { + if (window.getSelection) { + if (window.getSelection().empty) { // Chrome + window.getSelection().empty(); + } else if (window.getSelection().removeAllRanges) { // Firefox + window.getSelection().removeAllRanges(); + } + } else if (document.selection) { // IE? + document.selection.empty(); + } + } + + function selectRow(row) { + var $row = $(row); + var infoRow = table.row($row).data(); + var info = infoRow[infoRow.length - 1]; + if($row.hasClass('selected')) { + $row.removeClass('selected'); + removeItem(info.id.value); + enableActions(undefined, true); + removeSelected($row.attr('id')); + } + else { + $row.addClass('selected'); + var newItem = addItem(info); + enableActions(newItem.actions) + selData = table.row($row).data(); + + keepSelected(selData, true); + } + updateCounter('.selected-num'); + updateToggleAllSelect(); + }; + + function updateCounter(counterDOM, num) { + var $counter = $(counterDOM); + if(num) { + $counter.text(num); + } + else { + $counter.text(selected.items.length); + } + }; + + function checkboxTemplate(data, initState) { + if(data.length > maxCellChar) { + data = _.template(snf.tables.html.trimedCell, {data: data, trimmedData: data.substring(0, maxCellChar)}); + } + if($actionbar.length > 0) + return _.template(snf.tables.html.checkboxCell, {content: data}); + else + return data; + } + + function extraTemplate(data) { + var list = ''; + var html; + var hasDetails = false; + for(var prop in data) { + if(prop !== "details_url") { + if(data[prop].visible) { + list += _.template(snf.tables.html.summaryLine, {key: data[prop].display_name, value: data[prop].value}); + } + } + else { + hasDetails = true; + } + } + if(hasDetails) { + html = _.template(snf.tables.html.detailsBtn, {url: data["details_url"].value}) + _.template(snf.tables.html.summary, {list: list}); + } + else { + html = _.template(snf.tables.html.summary, {list: list}); + } + return html; + }; + + function clickDetails(row) { + $(row).find('td:last-child a.details-link').click(function(e) { + e.stopPropagation(); + }); + }; + + function clickSummary(row) { + $(row).find('td:last-child a.expand-area').click(function(e) { + e.preventDefault(); + + var $summaryTd = $(this).closest('td'); + var $btn = $summaryTd.find('.expand-area'); + var $btnIcon = $btn.find('span'); + var $summaryContent = $summaryTd.find('.info-summary'); + + var summaryContentWidth = $summaryTd.closest('tr').width(); + var summaryContentHeight = $summaryTd.closest('tr').height() - parseInt($summaryTd.css('padding-top')) - $btn.height()- parseInt($summaryTd.css('padding-bottom')) ; + var summaryContPos = summaryContentWidth - $summaryTd.width()+ parseInt($summaryTd.css('padding-left')); + + if ( $btnIcon.hasClass('snf-angle-down')) { + $summaryContent.css({ + width: summaryContentWidth, + right: summaryContPos, + paddingTop: summaryContentHeight, + }); + } + + $btnIcon.toggleClass('snf-angle-up snf-angle-down'); + $summaryContent.stop().slideToggle(600, function() { + if ($summaryContent.is(':visible')) { + $btnIcon.removeClass('snf-angle-down').addClass('snf-angle-up'); + } + else { + $btnIcon.removeClass('snf-angle-up').addClass('snf-angle-down'); + } + }); + }) + }; + + + function addItem(infoObj) { + var $selectedNum = $('.actionbar a').find('.selected-num'); + var itemsL; + var newItem = {} + var isNew = true; + var actionsArray = infoObj.allowed_actions.value; + var actionsL = actionsArray.length; + var newItem = { + "id": infoObj.id.value, + "item_name": infoObj.item_name.value, + "contact_id": infoObj.contact_id.value, + "contact_name": infoObj.contact_name.value, + "contact_email": infoObj.contact_email.value, + "actions": {} + } + + itemsL = selected.items.length; + for(var i=0; i<itemsL; i++) { + if(selected.items[i].id === newItem.id) { + isNew = false; + break; + } + } + if(isNew) { + for (var i = 0; i<actionsL; i++) { + newItem.actions[actionsArray[i]] = true; + } + for(var prop in availableActions) { + if(!(prop in newItem.actions)) { + newItem.actions[prop] = false; + } + } + selected.items.push(newItem); + return newItem + } + else + return null; + }; + + function removeItem(itemID) { + var items = selected.items; + var itemsL = items.length; + for (var i = 0; i<itemsL; i++) { + if(String(items[i].id) === String(itemID)) { + selected.items.splice(i, 1); + break; + } + } + }; + + + /* It enables the btn (link) of the corresponding allowed action */ + function enableActions(actionsObj, removeItemFlag) { + var itemActionsL =selected.items.length; + var $actionBar = $('.actionbar'); + var itemActions = {}; + if (removeItemFlag) { + if(!selected.items.length) { + for(var prop in allowedActions) { + allowedActions[prop] = false; + } + } + else { + for(var prop in allowedActions) { + allowedActions[prop] =true; + for(var i=0; i<itemActionsL; i++) { + allowedActions[prop] = allowedActions[prop] && selected.items[i].actions[prop]; + } + } + } + } + else { + if(selected.items.length === 1) { + for(var prop in allowedActions) { + allowedActions[prop] = availableActions[prop] && actionsObj[prop]; + } + } + else { + for(var prop in allowedActions) { + allowedActions[prop] = allowedActions[prop] && actionsObj[prop]; + } + } + } + for(var prop in allowedActions) { + if(allowedActions[prop]) { + $actionBar.find('a[data-action='+prop+']').removeClass('disabled'); + } + else { + $actionBar.find('a[data-action='+prop+']').addClass('disabled'); + } + } + }; + + function resetAll(tableDomID) { + selected.items = []; + removeSelected(true); //removes all selected items from the table of selected items + updateCounter('.selected-num'); + enableActions(undefined, true); + $(table.rows('.selected').nodes()).find('td:first-child .select').toggleClass('snf-checkbox-checked snf-checkbox-unchecked'); + $(tableDomID).dataTable().api().rows('.selected').nodes().to$().removeClass('selected'); + + updateToggleAllSelect(); + updateClearAll(); + }; + + + /* select-page button */ + + $('#select-page').click(function(e) { + e.preventDefault(); + toggleVisSelected(tableDomID, $(this).hasClass('select')); + updateClearAll(); + }); + + + /* select-page / deselect-page */ + function toggleVisSelected(tableDomID, selectFlag) { + $lastClicked = null; + $prevClicked = null; + if(selectFlag) { + $(tableDomID).find('tbody tr:not(.selected)').each(function() { // temp : shouldn't have a func that calls a named func + selectRow(this); + }); + } + else { + $(tableDomID).find('tbody tr.selected').each(function() { // temp : shouldn't have a func that calls a named func + selectRow(this); + }); + } + }; + + /* Checks how many rows are selected and adjusts the classes and + the text of the select-qll btn */ + function updateToggleAllSelect() { + var $togglePageItems = $('#select-page'); + var $label = $togglePageItems.find('span') + var $tr = $(tableDomID).find('tbody tr'); + if($tr.length >= 1) { + var allSelected = true + $tr.each(function() { + allSelected = allSelected && $(this).hasClass('selected'); + return allSelected; + }); + if($togglePageItems.hasClass('select') && allSelected) { + $togglePageItems.addClass('deselect').removeClass('select'); + $label.text('Deselect Page') + } + else if($togglePageItems.hasClass('deselect') && !allSelected) { + $togglePageItems.addClass('select').removeClass('deselect'); + $label.text('Select Page') + } + } + else { + $togglePageItems.addClass('select').removeClass('deselect') + $label.text('Select Page') + } + }; + + function updateClearAll() { + var $clearAllBtn = $('#clear-all') + if(selected.items.length === 0) { + $clearAllBtn.addClass('disabled'); + } + else { + $clearAllBtn.removeClass('disabled'); + } + }; + + + /* Modals */ + + function removeWarningDupl(modal) { + var $modal = $(modal); + $modal.find('.warning-duplicate').remove(); + }; + + function resetToggleAllBtn(modal) { + var $modal = $(modal); + $modal.find('.toggle-more').removeClass('open').addClass('closed'); + $modal.find('.toggle-more').find('span').text('Show all'); + }; + + $('.modal .cancel').click(function(e) { + $('[data-toggle="popover"]').popover('hide'); + var $modal = $(this).closest('.modal'); + snf.modals.resetErrors($modal); + snf.modals.resetInputs($modal); + removeWarningDupl($modal); + resetToggleAllBtn($modal); + // resetAll(tableDomID); + updateToggleAllSelect(); + updateClearAll(); + enableActions(undefined, true); + }); + + $('.modal .clear-all-confirm').click(function() { + resetAll(tableDomID); + }); + + var $notificationArea = $('.notify'); + var countAction = 0; + $('.modal .apply-action').click(function(e) { + var $modal = $(this).closest('.modal'); + var noError = true; + var itemsNum = $modal.find('tbody tr').length; + if(selected.items.length === 0) { + snf.modals.showError($modal, 'no-selected'); + noError = false; + } + if($modal.attr('data-type') === 'contact') { + var validForm = snf.modals.validateContactForm($modal); + noError = noError && validForm; + } + if(!noError) { + e.preventDefault(); + e.stopPropagation(); + } + else { + $('[data-toggle="popover"]').popover('hide'); + snf.modals.performAction($modal, $notificationArea, snf.modals.html.notifyReloadTable, itemsNum, countAction); + snf.modals.resetErrors($modal); + snf.modals.resetInputs($modal); + removeWarningDupl($modal); + resetAll(tableDomID); + resetToggleAllBtn($modal); + countAction++; + } + }); + + /* remove an item after the modal is visible */ + $('.modal').on('click', '.remove', function(e) { + e.preventDefault(); + var $modal = $(this).closest('.modal') + var $actionBtn = $modal.find('.modal-footer .apply-action'); + var $num = $modal.find('.num'); + var $tr = $(this).closest('tr'); + var itemID = $tr.attr('data-itemid'); + var idsArray = []; + deselectRow(itemID); + removeSelected(itemID); + removeItem(itemID); + idsArray = $actionBtn.attr('data-ids').replace('[', '').replace(']', '').split(','); + var index = idsArray.indexOf(itemID); + idsArray.splice(index, 1); + + $actionBtn.attr('data-ids','[' + idsArray + ']'); + $tr.slideUp('slow', function() { + $(this).siblings('.hidden-row').first().css('display', 'table-row'); + $(this).siblings('.hidden-row').first().removeClass('hidden-row'); + if($(this).siblings('.hidden-row').length === 0) { + $modal.find('.toggle-more').hide(); + } + $(this).remove(); + }); + $num.html(idsArray.length); // should this use updateCounter? + updateCounter('.selected-num'); + }); + + + function drawModal(modalID) { + var $tableBody = $(modalID).find('.table-selected tbody'); + var modalType = $(modalID).attr('data-type'); + var itemType = $(modalID).attr('data-item'); + var $counter = $(modalID).find('.num'); + var rowsNum = selected.items.length; + var $actionBtn = $(modalID).find('.apply-action'); + var maxVisible = 5; + var currentRow; + var htmlRows = ''; + var unique = true; + var uniqueProp = ''; + var count = 0; + var idsArray = []; + var warningMsg = snf.modals.html.warningDuplicates; + var warningInserted = false; + var associations = {}; + var $btn = $(modalID).find('.toggle-more'); + $tableBody.empty(); + if(modalType === "contact") { + uniqueProp = 'contact_id'; + for(var i=0; i<rowsNum; i++) { + var currContactID = selected.items[i][uniqueProp]; + if(associations[currContactID] === undefined) { + associations[currContactID] = [selected.items[i]['item_name']]; + } + else { + selected.items[i]['notFirst'] = true; // not the first item with the current contact_id + associations[currContactID].push(selected.items[i]['item_name']); + } + if(!warningInserted && selected.items[i]['notFirst']) { + $tableBody.closest('table').before(warningMsg); + warningInserted = true; + } + } + for(var i=0; i<rowsNum; i++) { + if (!selected.items[i]['notFirst']) { + idsArray.push(selected.items[i][uniqueProp]); + currentRow = _.template(snf.modals.html.contactRow, {itemID: selected.items[i].contact_id, showAssociations: (itemType !== 'user'), associations: associations[selected.items[i][uniqueProp]].toString().replace(/\,/gi, ', '), fullName: selected.items[i].contact_name, email: selected.items[i].contact_email, hidden: (i >maxVisible)}) + htmlRows += currentRow; + } + } + } + + else { + uniqueProp = 'id'; + for(var i=0; i<rowsNum; i++) { + idsArray.push(selected.items[i][uniqueProp]); + currentRow = _.template(snf.modals.html.commonRow, {itemID: selected.items[i].id, itemName: selected.items[i].item_name, ownerEmail: selected.items[i].contact_email, ownerName: selected.items[i].contact_name, hidden: (i >=maxVisible)}) + htmlRows += currentRow; + } + } + $tableBody.append(htmlRows); // should change + $actionBtn.attr('data-ids','['+idsArray+']'); + updateCounter($counter, idsArray.length); + + if(idsArray.length >= maxVisible) { + $btn.css('display', 'block'); + } + else { + $btn.css('display', 'none'); + } + delete associations; + }; + + $('.modal .toggle-more').click( function() { + var $tableBody = $(this).closest('.modal').find('table'); + if($(this).hasClass('closed')) { + $(this).find('span').text('Show less'); + $tableBody.find('.hidden-row').slideDown('slow'); + } + else { + var that = this; + $tableBody.find('tr.hidden-row').slideUp('slow', function() { + $(that).find('span').text('Show all'); + }); + } + $(this).toggleClass('closed open'); + }); + + + + + $('.toggle-selected').click(function (e) { + e.preventDefault(); + var $label = $(this).find('.text'); + var label1 = 'Show selected'; + var label2 = 'Hide selected'; + $(this).toggleClass('open'); + if($(this).hasClass('open')) { + $('#table-items-selected_wrapper').slideDown('slow', function() { + $label.text(label2); + }); + } + else { + $('#table-items-selected_wrapper').slideUp('slow', function() { + $label.text(label1); + }); + } + }); +}); diff --git a/snf-admin-app/synnefo_admin/admin/static/js/underscore.js b/snf-admin-app/synnefo_admin/admin/static/js/underscore.js new file mode 100644 index 0000000000000000000000000000000000000000..9a4cabecf7f8a686b1d4ec6b3b1ade467dbe1f3b --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/js/underscore.js @@ -0,0 +1,1343 @@ +// Underscore.js 1.6.0 +// http://underscorejs.org +// (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +// Underscore may be freely distributed under the MIT license. + +(function() { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `exports` on the server. + var root = this; + + // Save the previous value of the `_` variable. + var previousUnderscore = root._; + + // Establish the object that gets returned to break out of a loop iteration. + var breaker = {}; + + // Save bytes in the minified (but not gzipped) version: + var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; + + // Create quick reference variables for speed access to core prototypes. + var + push = ArrayProto.push, + slice = ArrayProto.slice, + concat = ArrayProto.concat, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty; + + // All **ECMAScript 5** native function implementations that we hope to use + // are declared here. + var + nativeForEach = ArrayProto.forEach, + nativeMap = ArrayProto.map, + nativeReduce = ArrayProto.reduce, + nativeReduceRight = ArrayProto.reduceRight, + nativeFilter = ArrayProto.filter, + nativeEvery = ArrayProto.every, + nativeSome = ArrayProto.some, + nativeIndexOf = ArrayProto.indexOf, + nativeLastIndexOf = ArrayProto.lastIndexOf, + nativeIsArray = Array.isArray, + nativeKeys = Object.keys, + nativeBind = FuncProto.bind; + + // Create a safe reference to the Underscore object for use below. + var _ = function(obj) { + if (obj instanceof _) return obj; + if (!(this instanceof _)) return new _(obj); + this._wrapped = obj; + }; + + // Export the Underscore object for **Node.js**, with + // backwards-compatibility for the old `require()` API. If we're in + // the browser, add `_` as a global object via a string identifier, + // for Closure Compiler "advanced" mode. + if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = _; + } + exports._ = _; + } else { + root._ = _; + } + + // Current version. + _.VERSION = '1.6.0'; + + // Collection Functions + // -------------------- + + // The cornerstone, an `each` implementation, aka `forEach`. + // Handles objects with the built-in `forEach`, arrays, and raw objects. + // Delegates to **ECMAScript 5**'s native `forEach` if available. + var each = _.each = _.forEach = function(obj, iterator, context) { + if (obj == null) return obj; + if (nativeForEach && obj.forEach === nativeForEach) { + obj.forEach(iterator, context); + } else if (obj.length === +obj.length) { + for (var i = 0, length = obj.length; i < length; i++) { + if (iterator.call(context, obj[i], i, obj) === breaker) return; + } + } else { + var keys = _.keys(obj); + for (var i = 0, length = keys.length; i < length; i++) { + if (iterator.call(context, obj[keys[i]], keys[i], obj) === breaker) return; + } + } + return obj; + }; + + // Return the results of applying the iterator to each element. + // Delegates to **ECMAScript 5**'s native `map` if available. + _.map = _.collect = function(obj, iterator, context) { + var results = []; + if (obj == null) return results; + if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); + each(obj, function(value, index, list) { + results.push(iterator.call(context, value, index, list)); + }); + return results; + }; + + var reduceError = 'Reduce of empty array with no initial value'; + + // **Reduce** builds up a single result from a list of values, aka `inject`, + // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. + _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { + var initial = arguments.length > 2; + if (obj == null) obj = []; + if (nativeReduce && obj.reduce === nativeReduce) { + if (context) iterator = _.bind(iterator, context); + return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); + } + each(obj, function(value, index, list) { + if (!initial) { + memo = value; + initial = true; + } else { + memo = iterator.call(context, memo, value, index, list); + } + }); + if (!initial) throw new TypeError(reduceError); + return memo; + }; + + // The right-associative version of reduce, also known as `foldr`. + // Delegates to **ECMAScript 5**'s native `reduceRight` if available. + _.reduceRight = _.foldr = function(obj, iterator, memo, context) { + var initial = arguments.length > 2; + if (obj == null) obj = []; + if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { + if (context) iterator = _.bind(iterator, context); + return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); + } + var length = obj.length; + if (length !== +length) { + var keys = _.keys(obj); + length = keys.length; + } + each(obj, function(value, index, list) { + index = keys ? keys[--length] : --length; + if (!initial) { + memo = obj[index]; + initial = true; + } else { + memo = iterator.call(context, memo, obj[index], index, list); + } + }); + if (!initial) throw new TypeError(reduceError); + return memo; + }; + + // Return the first value which passes a truth test. Aliased as `detect`. + _.find = _.detect = function(obj, predicate, context) { + var result; + any(obj, function(value, index, list) { + if (predicate.call(context, value, index, list)) { + result = value; + return true; + } + }); + return result; + }; + + // Return all the elements that pass a truth test. + // Delegates to **ECMAScript 5**'s native `filter` if available. + // Aliased as `select`. + _.filter = _.select = function(obj, predicate, context) { + var results = []; + if (obj == null) return results; + if (nativeFilter && obj.filter === nativeFilter) return obj.filter(predicate, context); + each(obj, function(value, index, list) { + if (predicate.call(context, value, index, list)) results.push(value); + }); + return results; + }; + + // Return all the elements for which a truth test fails. + _.reject = function(obj, predicate, context) { + return _.filter(obj, function(value, index, list) { + return !predicate.call(context, value, index, list); + }, context); + }; + + // Determine whether all of the elements match a truth test. + // Delegates to **ECMAScript 5**'s native `every` if available. + // Aliased as `all`. + _.every = _.all = function(obj, predicate, context) { + predicate || (predicate = _.identity); + var result = true; + if (obj == null) return result; + if (nativeEvery && obj.every === nativeEvery) return obj.every(predicate, context); + each(obj, function(value, index, list) { + if (!(result = result && predicate.call(context, value, index, list))) return breaker; + }); + return !!result; + }; + + // Determine if at least one element in the object matches a truth test. + // Delegates to **ECMAScript 5**'s native `some` if available. + // Aliased as `any`. + var any = _.some = _.any = function(obj, predicate, context) { + predicate || (predicate = _.identity); + var result = false; + if (obj == null) return result; + if (nativeSome && obj.some === nativeSome) return obj.some(predicate, context); + each(obj, function(value, index, list) { + if (result || (result = predicate.call(context, value, index, list))) return breaker; + }); + return !!result; + }; + + // Determine if the array or object contains a given value (using `===`). + // Aliased as `include`. + _.contains = _.include = function(obj, target) { + if (obj == null) return false; + if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; + return any(obj, function(value) { + return value === target; + }); + }; + + // Invoke a method (with arguments) on every item in a collection. + _.invoke = function(obj, method) { + var args = slice.call(arguments, 2); + var isFunc = _.isFunction(method); + return _.map(obj, function(value) { + return (isFunc ? method : value[method]).apply(value, args); + }); + }; + + // Convenience version of a common use case of `map`: fetching a property. + _.pluck = function(obj, key) { + return _.map(obj, _.property(key)); + }; + + // Convenience version of a common use case of `filter`: selecting only objects + // containing specific `key:value` pairs. + _.where = function(obj, attrs) { + return _.filter(obj, _.matches(attrs)); + }; + + // Convenience version of a common use case of `find`: getting the first object + // containing specific `key:value` pairs. + _.findWhere = function(obj, attrs) { + return _.find(obj, _.matches(attrs)); + }; + + // Return the maximum element or (element-based computation). + // Can't optimize arrays of integers longer than 65,535 elements. + // See [WebKit Bug 80797](https://bugs.webkit.org/show_bug.cgi?id=80797) + _.max = function(obj, iterator, context) { + if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { + return Math.max.apply(Math, obj); + } + var result = -Infinity, lastComputed = -Infinity; + each(obj, function(value, index, list) { + var computed = iterator ? iterator.call(context, value, index, list) : value; + if (computed > lastComputed) { + result = value; + lastComputed = computed; + } + }); + return result; + }; + + // Return the minimum element (or element-based computation). + _.min = function(obj, iterator, context) { + if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { + return Math.min.apply(Math, obj); + } + var result = Infinity, lastComputed = Infinity; + each(obj, function(value, index, list) { + var computed = iterator ? iterator.call(context, value, index, list) : value; + if (computed < lastComputed) { + result = value; + lastComputed = computed; + } + }); + return result; + }; + + // Shuffle an array, using the modern version of the + // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle). + _.shuffle = function(obj) { + var rand; + var index = 0; + var shuffled = []; + each(obj, function(value) { + rand = _.random(index++); + shuffled[index - 1] = shuffled[rand]; + shuffled[rand] = value; + }); + return shuffled; + }; + + // Sample **n** random values from a collection. + // If **n** is not specified, returns a single random element. + // The internal `guard` argument allows it to work with `map`. + _.sample = function(obj, n, guard) { + if (n == null || guard) { + if (obj.length !== +obj.length) obj = _.values(obj); + return obj[_.random(obj.length - 1)]; + } + return _.shuffle(obj).slice(0, Math.max(0, n)); + }; + + // An internal function to generate lookup iterators. + var lookupIterator = function(value) { + if (value == null) return _.identity; + if (_.isFunction(value)) return value; + return _.property(value); + }; + + // Sort the object's values by a criterion produced by an iterator. + _.sortBy = function(obj, iterator, context) { + iterator = lookupIterator(iterator); + return _.pluck(_.map(obj, function(value, index, list) { + return { + value: value, + index: index, + criteria: iterator.call(context, value, index, list) + }; + }).sort(function(left, right) { + var a = left.criteria; + var b = right.criteria; + if (a !== b) { + if (a > b || a === void 0) return 1; + if (a < b || b === void 0) return -1; + } + return left.index - right.index; + }), 'value'); + }; + + // An internal function used for aggregate "group by" operations. + var group = function(behavior) { + return function(obj, iterator, context) { + var result = {}; + iterator = lookupIterator(iterator); + each(obj, function(value, index) { + var key = iterator.call(context, value, index, obj); + behavior(result, key, value); + }); + return result; + }; + }; + + // Groups the object's values by a criterion. Pass either a string attribute + // to group by, or a function that returns the criterion. + _.groupBy = group(function(result, key, value) { + _.has(result, key) ? result[key].push(value) : result[key] = [value]; + }); + + // Indexes the object's values by a criterion, similar to `groupBy`, but for + // when you know that your index values will be unique. + _.indexBy = group(function(result, key, value) { + result[key] = value; + }); + + // Counts instances of an object that group by a certain criterion. Pass + // either a string attribute to count by, or a function that returns the + // criterion. + _.countBy = group(function(result, key) { + _.has(result, key) ? result[key]++ : result[key] = 1; + }); + + // Use a comparator function to figure out the smallest index at which + // an object should be inserted so as to maintain order. Uses binary search. + _.sortedIndex = function(array, obj, iterator, context) { + iterator = lookupIterator(iterator); + var value = iterator.call(context, obj); + var low = 0, high = array.length; + while (low < high) { + var mid = (low + high) >>> 1; + iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid; + } + return low; + }; + + // Safely create a real, live array from anything iterable. + _.toArray = function(obj) { + if (!obj) return []; + if (_.isArray(obj)) return slice.call(obj); + if (obj.length === +obj.length) return _.map(obj, _.identity); + return _.values(obj); + }; + + // Return the number of elements in an object. + _.size = function(obj) { + if (obj == null) return 0; + return (obj.length === +obj.length) ? obj.length : _.keys(obj).length; + }; + + // Array Functions + // --------------- + + // Get the first element of an array. Passing **n** will return the first N + // values in the array. Aliased as `head` and `take`. The **guard** check + // allows it to work with `_.map`. + _.first = _.head = _.take = function(array, n, guard) { + if (array == null) return void 0; + if ((n == null) || guard) return array[0]; + if (n < 0) return []; + return slice.call(array, 0, n); + }; + + // Returns everything but the last entry of the array. Especially useful on + // the arguments object. Passing **n** will return all the values in + // the array, excluding the last N. The **guard** check allows it to work with + // `_.map`. + _.initial = function(array, n, guard) { + return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); + }; + + // Get the last element of an array. Passing **n** will return the last N + // values in the array. The **guard** check allows it to work with `_.map`. + _.last = function(array, n, guard) { + if (array == null) return void 0; + if ((n == null) || guard) return array[array.length - 1]; + return slice.call(array, Math.max(array.length - n, 0)); + }; + + // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. + // Especially useful on the arguments object. Passing an **n** will return + // the rest N values in the array. The **guard** + // check allows it to work with `_.map`. + _.rest = _.tail = _.drop = function(array, n, guard) { + return slice.call(array, (n == null) || guard ? 1 : n); + }; + + // Trim out all falsy values from an array. + _.compact = function(array) { + return _.filter(array, _.identity); + }; + + // Internal implementation of a recursive `flatten` function. + var flatten = function(input, shallow, output) { + if (shallow && _.every(input, _.isArray)) { + return concat.apply(output, input); + } + each(input, function(value) { + if (_.isArray(value) || _.isArguments(value)) { + shallow ? push.apply(output, value) : flatten(value, shallow, output); + } else { + output.push(value); + } + }); + return output; + }; + + // Flatten out an array, either recursively (by default), or just one level. + _.flatten = function(array, shallow) { + return flatten(array, shallow, []); + }; + + // Return a version of the array that does not contain the specified value(s). + _.without = function(array) { + return _.difference(array, slice.call(arguments, 1)); + }; + + // Split an array into two arrays: one whose elements all satisfy the given + // predicate, and one whose elements all do not satisfy the predicate. + _.partition = function(array, predicate) { + var pass = [], fail = []; + each(array, function(elem) { + (predicate(elem) ? pass : fail).push(elem); + }); + return [pass, fail]; + }; + + // Produce a duplicate-free version of the array. If the array has already + // been sorted, you have the option of using a faster algorithm. + // Aliased as `unique`. + _.uniq = _.unique = function(array, isSorted, iterator, context) { + if (_.isFunction(isSorted)) { + context = iterator; + iterator = isSorted; + isSorted = false; + } + var initial = iterator ? _.map(array, iterator, context) : array; + var results = []; + var seen = []; + each(initial, function(value, index) { + if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) { + seen.push(value); + results.push(array[index]); + } + }); + return results; + }; + + // Produce an array that contains the union: each distinct element from all of + // the passed-in arrays. + _.union = function() { + return _.uniq(_.flatten(arguments, true)); + }; + + // Produce an array that contains every item shared between all the + // passed-in arrays. + _.intersection = function(array) { + var rest = slice.call(arguments, 1); + return _.filter(_.uniq(array), function(item) { + return _.every(rest, function(other) { + return _.contains(other, item); + }); + }); + }; + + // Take the difference between one array and a number of other arrays. + // Only the elements present in just the first array will remain. + _.difference = function(array) { + var rest = concat.apply(ArrayProto, slice.call(arguments, 1)); + return _.filter(array, function(value){ return !_.contains(rest, value); }); + }; + + // Zip together multiple lists into a single array -- elements that share + // an index go together. + _.zip = function() { + var length = _.max(_.pluck(arguments, 'length').concat(0)); + var results = new Array(length); + for (var i = 0; i < length; i++) { + results[i] = _.pluck(arguments, '' + i); + } + return results; + }; + + // Converts lists into objects. Pass either a single array of `[key, value]` + // pairs, or two parallel arrays of the same length -- one of keys, and one of + // the corresponding values. + _.object = function(list, values) { + if (list == null) return {}; + var result = {}; + for (var i = 0, length = list.length; i < length; i++) { + if (values) { + result[list[i]] = values[i]; + } else { + result[list[i][0]] = list[i][1]; + } + } + return result; + }; + + // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), + // we need this function. Return the position of the first occurrence of an + // item in an array, or -1 if the item is not included in the array. + // Delegates to **ECMAScript 5**'s native `indexOf` if available. + // If the array is large and already in sort order, pass `true` + // for **isSorted** to use binary search. + _.indexOf = function(array, item, isSorted) { + if (array == null) return -1; + var i = 0, length = array.length; + if (isSorted) { + if (typeof isSorted == 'number') { + i = (isSorted < 0 ? Math.max(0, length + isSorted) : isSorted); + } else { + i = _.sortedIndex(array, item); + return array[i] === item ? i : -1; + } + } + if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted); + for (; i < length; i++) if (array[i] === item) return i; + return -1; + }; + + // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. + _.lastIndexOf = function(array, item, from) { + if (array == null) return -1; + var hasIndex = from != null; + if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) { + return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item); + } + var i = (hasIndex ? from : array.length); + while (i--) if (array[i] === item) return i; + return -1; + }; + + // Generate an integer Array containing an arithmetic progression. A port of + // the native Python `range()` function. See + // [the Python documentation](http://docs.python.org/library/functions.html#range). + _.range = function(start, stop, step) { + if (arguments.length <= 1) { + stop = start || 0; + start = 0; + } + step = arguments[2] || 1; + + var length = Math.max(Math.ceil((stop - start) / step), 0); + var idx = 0; + var range = new Array(length); + + while(idx < length) { + range[idx++] = start; + start += step; + } + + return range; + }; + + // Function (ahem) Functions + // ------------------ + + // Reusable constructor function for prototype setting. + var ctor = function(){}; + + // Create a function bound to a given object (assigning `this`, and arguments, + // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if + // available. + _.bind = function(func, context) { + var args, bound; + if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); + if (!_.isFunction(func)) throw new TypeError; + args = slice.call(arguments, 2); + return bound = function() { + if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); + ctor.prototype = func.prototype; + var self = new ctor; + ctor.prototype = null; + var result = func.apply(self, args.concat(slice.call(arguments))); + if (Object(result) === result) return result; + return self; + }; + }; + + // Partially apply a function by creating a version that has had some of its + // arguments pre-filled, without changing its dynamic `this` context. _ acts + // as a placeholder, allowing any combination of arguments to be pre-filled. + _.partial = function(func) { + var boundArgs = slice.call(arguments, 1); + return function() { + var position = 0; + var args = boundArgs.slice(); + for (var i = 0, length = args.length; i < length; i++) { + if (args[i] === _) args[i] = arguments[position++]; + } + while (position < arguments.length) args.push(arguments[position++]); + return func.apply(this, args); + }; + }; + + // Bind a number of an object's methods to that object. Remaining arguments + // are the method names to be bound. Useful for ensuring that all callbacks + // defined on an object belong to it. + _.bindAll = function(obj) { + var funcs = slice.call(arguments, 1); + if (funcs.length === 0) throw new Error('bindAll must be passed function names'); + each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); + return obj; + }; + + // Memoize an expensive function by storing its results. + _.memoize = function(func, hasher) { + var memo = {}; + hasher || (hasher = _.identity); + return function() { + var key = hasher.apply(this, arguments); + return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); + }; + }; + + // Delays a function for the given number of milliseconds, and then calls + // it with the arguments supplied. + _.delay = function(func, wait) { + var args = slice.call(arguments, 2); + return setTimeout(function(){ return func.apply(null, args); }, wait); + }; + + // Defers a function, scheduling it to run after the current call stack has + // cleared. + _.defer = function(func) { + return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); + }; + + // Returns a function, that, when invoked, will only be triggered at most once + // during a given window of time. Normally, the throttled function will run + // as much as it can, without ever going more than once per `wait` duration; + // but if you'd like to disable the execution on the leading edge, pass + // `{leading: false}`. To disable execution on the trailing edge, ditto. + _.throttle = function(func, wait, options) { + var context, args, result; + var timeout = null; + var previous = 0; + options || (options = {}); + var later = function() { + previous = options.leading === false ? 0 : _.now(); + timeout = null; + result = func.apply(context, args); + context = args = null; + }; + return function() { + var now = _.now(); + if (!previous && options.leading === false) previous = now; + var remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0) { + clearTimeout(timeout); + timeout = null; + previous = now; + result = func.apply(context, args); + context = args = null; + } else if (!timeout && options.trailing !== false) { + timeout = setTimeout(later, remaining); + } + return result; + }; + }; + + // Returns a function, that, as long as it continues to be invoked, will not + // be triggered. The function will be called after it stops being called for + // N milliseconds. If `immediate` is passed, trigger the function on the + // leading edge, instead of the trailing. + _.debounce = function(func, wait, immediate) { + var timeout, args, context, timestamp, result; + + var later = function() { + var last = _.now() - timestamp; + if (last < wait) { + timeout = setTimeout(later, wait - last); + } else { + timeout = null; + if (!immediate) { + result = func.apply(context, args); + context = args = null; + } + } + }; + + return function() { + context = this; + args = arguments; + timestamp = _.now(); + var callNow = immediate && !timeout; + if (!timeout) { + timeout = setTimeout(later, wait); + } + if (callNow) { + result = func.apply(context, args); + context = args = null; + } + + return result; + }; + }; + + // Returns a function that will be executed at most one time, no matter how + // often you call it. Useful for lazy initialization. + _.once = function(func) { + var ran = false, memo; + return function() { + if (ran) return memo; + ran = true; + memo = func.apply(this, arguments); + func = null; + return memo; + }; + }; + + // Returns the first function passed as an argument to the second, + // allowing you to adjust arguments, run code before and after, and + // conditionally execute the original function. + _.wrap = function(func, wrapper) { + return _.partial(wrapper, func); + }; + + // Returns a function that is the composition of a list of functions, each + // consuming the return value of the function that follows. + _.compose = function() { + var funcs = arguments; + return function() { + var args = arguments; + for (var i = funcs.length - 1; i >= 0; i--) { + args = [funcs[i].apply(this, args)]; + } + return args[0]; + }; + }; + + // Returns a function that will only be executed after being called N times. + _.after = function(times, func) { + return function() { + if (--times < 1) { + return func.apply(this, arguments); + } + }; + }; + + // Object Functions + // ---------------- + + // Retrieve the names of an object's properties. + // Delegates to **ECMAScript 5**'s native `Object.keys` + _.keys = function(obj) { + if (!_.isObject(obj)) return []; + if (nativeKeys) return nativeKeys(obj); + var keys = []; + for (var key in obj) if (_.has(obj, key)) keys.push(key); + return keys; + }; + + // Retrieve the values of an object's properties. + _.values = function(obj) { + var keys = _.keys(obj); + var length = keys.length; + var values = new Array(length); + for (var i = 0; i < length; i++) { + values[i] = obj[keys[i]]; + } + return values; + }; + + // Convert an object into a list of `[key, value]` pairs. + _.pairs = function(obj) { + var keys = _.keys(obj); + var length = keys.length; + var pairs = new Array(length); + for (var i = 0; i < length; i++) { + pairs[i] = [keys[i], obj[keys[i]]]; + } + return pairs; + }; + + // Invert the keys and values of an object. The values must be serializable. + _.invert = function(obj) { + var result = {}; + var keys = _.keys(obj); + for (var i = 0, length = keys.length; i < length; i++) { + result[obj[keys[i]]] = keys[i]; + } + return result; + }; + + // Return a sorted list of the function names available on the object. + // Aliased as `methods` + _.functions = _.methods = function(obj) { + var names = []; + for (var key in obj) { + if (_.isFunction(obj[key])) names.push(key); + } + return names.sort(); + }; + + // Extend a given object with all the properties in passed-in object(s). + _.extend = function(obj) { + each(slice.call(arguments, 1), function(source) { + if (source) { + for (var prop in source) { + obj[prop] = source[prop]; + } + } + }); + return obj; + }; + + // Return a copy of the object only containing the whitelisted properties. + _.pick = function(obj) { + var copy = {}; + var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); + each(keys, function(key) { + if (key in obj) copy[key] = obj[key]; + }); + return copy; + }; + + // Return a copy of the object without the blacklisted properties. + _.omit = function(obj) { + var copy = {}; + var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); + for (var key in obj) { + if (!_.contains(keys, key)) copy[key] = obj[key]; + } + return copy; + }; + + // Fill in a given object with default properties. + _.defaults = function(obj) { + each(slice.call(arguments, 1), function(source) { + if (source) { + for (var prop in source) { + if (obj[prop] === void 0) obj[prop] = source[prop]; + } + } + }); + return obj; + }; + + // Create a (shallow-cloned) duplicate of an object. + _.clone = function(obj) { + if (!_.isObject(obj)) return obj; + return _.isArray(obj) ? obj.slice() : _.extend({}, obj); + }; + + // Invokes interceptor with the obj, and then returns obj. + // The primary purpose of this method is to "tap into" a method chain, in + // order to perform operations on intermediate results within the chain. + _.tap = function(obj, interceptor) { + interceptor(obj); + return obj; + }; + + // Internal recursive comparison function for `isEqual`. + var eq = function(a, b, aStack, bStack) { + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). + if (a === b) return a !== 0 || 1 / a == 1 / b; + // A strict comparison is necessary because `null == undefined`. + if (a == null || b == null) return a === b; + // Unwrap any wrapped objects. + if (a instanceof _) a = a._wrapped; + if (b instanceof _) b = b._wrapped; + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className != toString.call(b)) return false; + switch (className) { + // Strings, numbers, dates, and booleans are compared by value. + case '[object String]': + // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is + // equivalent to `new String("5")`. + return a == String(b); + case '[object Number]': + // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for + // other numeric values. + return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); + case '[object Date]': + case '[object Boolean]': + // Coerce dates and booleans to numeric primitive values. Dates are compared by their + // millisecond representations. Note that invalid dates with millisecond representations + // of `NaN` are not equivalent. + return +a == +b; + // RegExps are compared by their source patterns and flags. + case '[object RegExp]': + return a.source == b.source && + a.global == b.global && + a.multiline == b.multiline && + a.ignoreCase == b.ignoreCase; + } + if (typeof a != 'object' || typeof b != 'object') return false; + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + var length = aStack.length; + while (length--) { + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (aStack[length] == a) return bStack[length] == b; + } + // Objects with different constructors are not equivalent, but `Object`s + // from different frames are. + var aCtor = a.constructor, bCtor = b.constructor; + if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) && + _.isFunction(bCtor) && (bCtor instanceof bCtor)) + && ('constructor' in a && 'constructor' in b)) { + return false; + } + // Add the first object to the stack of traversed objects. + aStack.push(a); + bStack.push(b); + var size = 0, result = true; + // Recursively compare objects and arrays. + if (className == '[object Array]') { + // Compare array lengths to determine if a deep comparison is necessary. + size = a.length; + result = size == b.length; + if (result) { + // Deep compare the contents, ignoring non-numeric properties. + while (size--) { + if (!(result = eq(a[size], b[size], aStack, bStack))) break; + } + } + } else { + // Deep compare objects. + for (var key in a) { + if (_.has(a, key)) { + // Count the expected number of properties. + size++; + // Deep compare each member. + if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break; + } + } + // Ensure that both objects contain the same number of properties. + if (result) { + for (key in b) { + if (_.has(b, key) && !(size--)) break; + } + result = !size; + } + } + // Remove the first object from the stack of traversed objects. + aStack.pop(); + bStack.pop(); + return result; + }; + + // Perform a deep comparison to check if two objects are equal. + _.isEqual = function(a, b) { + return eq(a, b, [], []); + }; + + // Is a given array, string, or object empty? + // An "empty" object has no enumerable own-properties. + _.isEmpty = function(obj) { + if (obj == null) return true; + if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; + for (var key in obj) if (_.has(obj, key)) return false; + return true; + }; + + // Is a given value a DOM element? + _.isElement = function(obj) { + return !!(obj && obj.nodeType === 1); + }; + + // Is a given value an array? + // Delegates to ECMA5's native Array.isArray + _.isArray = nativeIsArray || function(obj) { + return toString.call(obj) == '[object Array]'; + }; + + // Is a given variable an object? + _.isObject = function(obj) { + return obj === Object(obj); + }; + + // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. + each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { + _['is' + name] = function(obj) { + return toString.call(obj) == '[object ' + name + ']'; + }; + }); + + // Define a fallback version of the method in browsers (ahem, IE), where + // there isn't any inspectable "Arguments" type. + if (!_.isArguments(arguments)) { + _.isArguments = function(obj) { + return !!(obj && _.has(obj, 'callee')); + }; + } + + // Optimize `isFunction` if appropriate. + if (typeof (/./) !== 'function') { + _.isFunction = function(obj) { + return typeof obj === 'function'; + }; + } + + // Is a given object a finite number? + _.isFinite = function(obj) { + return isFinite(obj) && !isNaN(parseFloat(obj)); + }; + + // Is the given value `NaN`? (NaN is the only number which does not equal itself). + _.isNaN = function(obj) { + return _.isNumber(obj) && obj != +obj; + }; + + // Is a given value a boolean? + _.isBoolean = function(obj) { + return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; + }; + + // Is a given value equal to null? + _.isNull = function(obj) { + return obj === null; + }; + + // Is a given variable undefined? + _.isUndefined = function(obj) { + return obj === void 0; + }; + + // Shortcut function for checking if an object has a given property directly + // on itself (in other words, not on a prototype). + _.has = function(obj, key) { + return hasOwnProperty.call(obj, key); + }; + + // Utility Functions + // ----------------- + + // Run Underscore.js in *noConflict* mode, returning the `_` variable to its + // previous owner. Returns a reference to the Underscore object. + _.noConflict = function() { + root._ = previousUnderscore; + return this; + }; + + // Keep the identity function around for default iterators. + _.identity = function(value) { + return value; + }; + + _.constant = function(value) { + return function () { + return value; + }; + }; + + _.property = function(key) { + return function(obj) { + return obj[key]; + }; + }; + + // Returns a predicate for checking whether an object has a given set of `key:value` pairs. + _.matches = function(attrs) { + return function(obj) { + if (obj === attrs) return true; //avoid comparing an object to itself. + for (var key in attrs) { + if (attrs[key] !== obj[key]) + return false; + } + return true; + } + }; + + // Run a function **n** times. + _.times = function(n, iterator, context) { + var accum = Array(Math.max(0, n)); + for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i); + return accum; + }; + + // Return a random integer between min and max (inclusive). + _.random = function(min, max) { + if (max == null) { + max = min; + min = 0; + } + return min + Math.floor(Math.random() * (max - min + 1)); + }; + + // A (possibly faster) way to get the current timestamp as an integer. + _.now = Date.now || function() { return new Date().getTime(); }; + + // List of HTML entities for escaping. + var entityMap = { + escape: { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + } + }; + entityMap.unescape = _.invert(entityMap.escape); + + // Regexes containing the keys and values listed immediately above. + var entityRegexes = { + escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'), + unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g') + }; + + // Functions for escaping and unescaping strings to/from HTML interpolation. + _.each(['escape', 'unescape'], function(method) { + _[method] = function(string) { + if (string == null) return ''; + return ('' + string).replace(entityRegexes[method], function(match) { + return entityMap[method][match]; + }); + }; + }); + + // If the value of the named `property` is a function then invoke it with the + // `object` as context; otherwise, return it. + _.result = function(object, property) { + if (object == null) return void 0; + var value = object[property]; + return _.isFunction(value) ? value.call(object) : value; + }; + + // Add your own custom functions to the Underscore object. + _.mixin = function(obj) { + each(_.functions(obj), function(name) { + var func = _[name] = obj[name]; + _.prototype[name] = function() { + var args = [this._wrapped]; + push.apply(args, arguments); + return result.call(this, func.apply(_, args)); + }; + }); + }; + + // Generate a unique integer id (unique within the entire client session). + // Useful for temporary DOM ids. + var idCounter = 0; + _.uniqueId = function(prefix) { + var id = ++idCounter + ''; + return prefix ? prefix + id : id; + }; + + // By default, Underscore uses ERB-style template delimiters, change the + // following template settings to use alternative delimiters. + _.templateSettings = { + evaluate : /<%([\s\S]+?)%>/g, + interpolate : /<%=([\s\S]+?)%>/g, + escape : /<%-([\s\S]+?)%>/g + }; + + // When customizing `templateSettings`, if you don't want to define an + // interpolation, evaluation or escaping regex, we need one that is + // guaranteed not to match. + var noMatch = /(.)^/; + + // Certain characters need to be escaped so that they can be put into a + // string literal. + var escapes = { + "'": "'", + '\\': '\\', + '\r': 'r', + '\n': 'n', + '\t': 't', + '\u2028': 'u2028', + '\u2029': 'u2029' + }; + + var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; + + // JavaScript micro-templating, similar to John Resig's implementation. + // Underscore templating handles arbitrary delimiters, preserves whitespace, + // and correctly escapes quotes within interpolated code. + _.template = function(text, data, settings) { + var render; + settings = _.defaults({}, settings, _.templateSettings); + + // Combine delimiters into one regular expression via alternation. + var matcher = new RegExp([ + (settings.escape || noMatch).source, + (settings.interpolate || noMatch).source, + (settings.evaluate || noMatch).source + ].join('|') + '|$', 'g'); + + // Compile the template source, escaping string literals appropriately. + var index = 0; + var source = "__p+='"; + text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { + source += text.slice(index, offset) + .replace(escaper, function(match) { return '\\' + escapes[match]; }); + + if (escape) { + source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; + } + if (interpolate) { + source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; + } + if (evaluate) { + source += "';\n" + evaluate + "\n__p+='"; + } + index = offset + match.length; + return match; + }); + source += "';\n"; + + // If a variable is not specified, place data values in local scope. + if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; + + source = "var __t,__p='',__j=Array.prototype.join," + + "print=function(){__p+=__j.call(arguments,'');};\n" + + source + "return __p;\n"; + + try { + render = new Function(settings.variable || 'obj', '_', source); + } catch (e) { + e.source = source; + throw e; + } + + if (data) return render(data, _); + var template = function(data) { + return render.call(this, data, _); + }; + + // Provide the compiled function source as a convenience for precompilation. + template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; + + return template; + }; + + // Add a "chain" function, which will delegate to the wrapper. + _.chain = function(obj) { + return _(obj).chain(); + }; + + // OOP + // --------------- + // If Underscore is called as a function, it returns a wrapped object that + // can be used OO-style. This wrapper holds altered versions of all the + // underscore functions. Wrapped objects may be chained. + + // Helper function to continue chaining intermediate results. + var result = function(obj) { + return this._chain ? _(obj).chain() : obj; + }; + + // Add all of the Underscore functions to the wrapper object. + _.mixin(_); + + // Add all mutator Array functions to the wrapper. + each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + var obj = this._wrapped; + method.apply(obj, arguments); + if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0]; + return result.call(this, obj); + }; + }); + + // Add all accessor Array functions to the wrapper. + each(['concat', 'join', 'slice'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + return result.call(this, method.apply(this._wrapped, arguments)); + }; + }); + + _.extend(_.prototype, { + + // Start chaining a wrapped Underscore object. + chain: function() { + this._chain = true; + return this; + }, + + // Extracts the result from a wrapped and chained object. + value: function() { + return this._wrapped; + } + + }); + + // AMD registration happens at the end for compatibility with AMD loaders + // that may not enforce next-turn semantics on modules. Even though general + // practice for AMD registration is to be anonymous, underscore registers + // as a named module because, like jQuery, it is a base library that is + // popular enough to be bundled in a third party lib, but not be part of + // an AMD load request. Those cases could generate an error when an + // anonymous define() is called outside of a loader request. + if (typeof define === 'function' && define.amd) { + define('underscore', [], function() { + return _; + }); + } +}).call(this); diff --git a/snf-admin-app/synnefo_admin/admin/static/min-css/icon-fonts.css b/snf-admin-app/synnefo_admin/admin/static/min-css/icon-fonts.css new file mode 100644 index 0000000000000000000000000000000000000000..34539634a889f595d66a09da8dfecf75961369cd --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/min-css/icon-fonts.css @@ -0,0 +1 @@ +@font-face{font-family:'font-icons';src:url("../fonts/font-icons.eot?hm0cup");src:url("../fonts/font-icons.eot?#iefixhm0cup") format("embedded-opentype"),url("../fonts/font-icons.woff?hm0cup") format("woff"),url("../fonts/font-icons.ttf?hm0cup") format("truetype"),url("../fonts/font-icons.svg?hm0cup#font-icons") format("svg");font-weight:normal;font-style:normal}@font-face{font-family:"snf-font";src:url("../fonts/snf-font.eot");src:url("../fonts/snf-font.eot?#iefix") format("embedded-opentype"),url("../fonts/snf-font.woff") format("woff"),url("../fonts/snf-font.ttf") format("truetype"),url("../fonts/snf-font.svg#snf-font") format("svg");font-weight:normal;font-style:normal}.snf-ok{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-ok:before{content:"\61"}.snf-remove{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-remove:before{content:"\62"}.snf-envelope{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-envelope:before{content:"\63"}.snf-envelope-alt{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-envelope-alt:before{content:"\64"}.snf-angle-up{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-angle-up:before{content:"\65"}.snf-angle-down{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-angle-down:before{content:"\66"}.snf-exclamation-sign{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-exclamation-sign:before{content:"\67"}.snf-clipboard-h{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-clipboard-h:before{content:"\68"}.snf-clipboard-i{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-clipboard-i:before{content:"\69"}.snf-copy{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-copy:before{content:"\6c"}.snf-search{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-search:before{content:"\6d"}.snf-sign-out{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-sign-out:before{content:"\6e"}.snf-archive{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-archive:before{content:"\6b"}.snf-checkbox-checked{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-checkbox-checked:before{content:"\6f"}.snf-checkbox-unchecked{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-checkbox-unchecked:before{content:"\70"}.snf-radio-checked{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-radio-checked:before{content:"\71"}.snf-radio-unchecked{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-radio-unchecked:before{content:"\72"}.snf-info{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-info:before{content:"\73"}.snf-user-outline{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-user-outline:before{content:"\75"}.snf-user-full{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-user-full:before{content:"\74"}.snf-wallet-full{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-wallet-full:before{content:"\78"}.snf-wallet-outline{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-wallet-outline:before{content:"\79"}.snf-keyboard{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-keyboard:before{content:"\7a"}.snf-book-2{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-book-2:before{content:"\42"}.snf-bell-1{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-bell-1:before{content:"\43"}.snf-bulb{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-bulb:before{content:"\46"}.snf-sun-1{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-sun-1:before{content:"\47"}.snf-moon-1{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-moon-1:before{content:"\76"}.snf-sun-2-full{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-sun-2-full:before{content:"\77"}.snf-sun-2-outline{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-sun-2-outline:before{content:"\6a"}.snf-moon-2-full:before{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-moon-2-full:before:before{content:"\44"}.snf-moon-2-outline{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-moon-2-outline:before{content:"\45"}.snf-sun-3{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-sun-3:before{content:"\41"}.snf-filter{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-filter:before{content:"\7b"}.snf-eye{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-eye:before{content:"\41"}.snf-radio-checked{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-radio-checked:before{content:"\42"}.snf-radio-unchecked{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-radio-unchecked:before{content:"\43"}.snf-close{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-close:before{content:"\44"}.snf-www{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-www:before{content:"\49"}.snf-arrow-up{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-arrow-up:before{content:"\4c"}.snf-arrow-down{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-arrow-down:before{content:"\4d"}.snf-checkbox-unchecked{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-checkbox-unchecked:before{content:"\61"}.snf-checkbox-checked{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-checkbox-checked:before{content:"\62"}.snf-cancel-circled{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-cancel-circled:before{content:"\63"}.snf-search{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-search:before{content:"\64"}.snf-twitter-logo{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-twitter-logo:before{content:"\67"}.snf-ok{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-ok:before{content:"\68"}.snf-switch{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-switch:before{content:"\69"}.snf-ban-circle{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-ban-circle:before{content:"\6a"}.snf-ok-sign{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-ok-sign:before{content:"\6c"}.snf-minus-sign{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-minus-sign:before{content:"\6e"}.snf-edit{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-edit:before{content:"\71"}.snf-listview{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-listview:before{content:"\73"}.snf-gridview{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-gridview:before{content:"\74"}.snf-dashboard-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-dashboard-outline:before{content:"\7a"}.snf-pithos-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-pithos-outline:before{content:"\79"}.snf-info-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-info-full:before{content:"\70"}.snf-volume-create-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-volume-create-full:before{content:"\36"}.snf-image-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-image-full:before{content:"\51"}.snf-pc-create-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-pc-create-full:before{content:"\53"}.snf-network-create-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-network-create-outline:before{content:"\54"}.snf-network-create-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-network-create-full:before{content:"\55"}.snf-ram-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-ram-outline:before{content:"\4a"}.snf-nic-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-nic-outline:before{content:"\50"}.snf-ram-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-ram-full:before{content:"\52"}.snf-nic-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-nic-full:before{content:"\72"}.snf-network-broken-1-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-network-broken-1-full:before{content:"\56"}.snf-network-broken-2-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-network-broken-2-full:before{content:"\57"}.snf-pc-broken-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-pc-broken-full:before{content:"\58"}.snf-pc-reboot-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-pc-reboot-full:before{content:"\59"}.snf-pc-switch-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-pc-switch-full:before{content:"\5a"}.snf-key-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-key-full:before{content:"\31"}.snf-router-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-router-full:before{content:"\32"}.snf-chip-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-chip-full:before{content:"\33"}.snf-plus-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-plus-full:before{content:"\34"}.snf-snapshot-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-snapshot-full:before{content:"\4e"}.snf-pithos-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-pithos-full:before{content:"\35"}.snf-volume-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-volume-full:before{content:"\4f"}.snf-network-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-network-full:before{content:"\4b"}.snf-pc-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-pc-full:before{content:"\78"}.snf-network-broken-1-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-network-broken-1-outline:before{content:"\37"}.snf-network-broken-2-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-network-broken-2-outline:before{content:"\38"}.snf-pc-broken-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-pc-broken-outline:before{content:"\39"}.snf-volume-broken-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-volume-broken-outline:before{content:"\30"}.snf-pc-reboot-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-pc-reboot-outline:before{content:"\21"}.snf-pc-switch-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-pc-switch-outline:before{content:"\40"}.snf-key-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-key-outline:before{content:"\23"}.snf-router-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-router-outline:before{content:"\48"}.snf-chip-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-chip-outline:before{content:"\45"}.snf-image-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-image-outline:before{content:"\66"}.snf-plus-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-plus-outline:before{content:"\6d"}.snf-snapshot-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-snapshot-outline:before{content:"\65"}.snf-volume-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-volume-outline:before{content:"\75"}.snf-network-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-network-outline:before{content:"\76"}.snf-pc-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-pc-outline:before{content:"\77"}.snf-info-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-info-outline:before{content:"\6f"}.snf-thunder-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-thunder-full:before{content:"\6b"}.snf-lock-closed-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-lock-closed-full:before{content:"\46"}.snf-lock-open-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-lock-open-full:before{content:"\47"}.snf-link-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-link-outline:before{content:"\26"}.snf-refresh-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-refresh-outline:before{content:"\29"}.snf-download-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-download-full:before{content:"\25"}.snf-person-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-person-outline:before{content:"\2a"}.snf-upload-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-upload-full:before{content:"\28"}.snf-arrow-right-small-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-arrow-right-small-full:before{content:"\2d"}.snf-copy-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-copy-outline:before{content:"\3f"}.snf-copy-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-copy-full:before{content:"\22"}.snf-arrow-left-small-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-arrow-left-small-full:before{content:"\5f"}.snf-trash-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-trash-full:before{content:"\3d"}.snf-trash-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-trash-outline:before{content:"\24"} diff --git a/snf-admin-app/synnefo_admin/admin/static/min-css/main-light.css b/snf-admin-app/synnefo_admin/admin/static/min-css/main-light.css new file mode 100644 index 0000000000000000000000000000000000000000..120316febf7c5f95c6a17e825fabdb034f6f5ded --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/min-css/main-light.css @@ -0,0 +1 @@ +/*! normalize.css v3.0.0 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:0.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace, monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}@media print{*{text-shadow:none !important;color:#000 !important;background:transparent !important;box-shadow:none !important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff !important}.navbar{display:none}.table td,.table th{background-color:#fff !important}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000 !important}.label{border:1px solid #000}.table{border-collapse:collapse !important}.table-bordered th,.table-bordered td{border:1px solid #ddd !important}}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:62.5%;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Open Sans",sans-serif;font-size:14px;line-height:1.42857;color:#222;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit;color:#222}a{color:#005b9a;text-decoration:none}a:hover,a:focus{color:#ee5161}a:focus{outline:0 none}figure{margin:0}img{vertical-align:middle}.img-responsive{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857;background-color:#303030;border:1px solid #ddd;border-radius:0;-webkit-transition:all 0.2s ease-in-out;transition:all 0.2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #d9d9d9}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1 small,h1 .small,h2 small,h2 .small,h3 small,h3 .small,h4 small,h4 .small,h5 small,h5 .small,h6 small,h6 .small,.h1 small,.h1 .small,.h2 small,.h2 .small,.h3 small,.h3 .small,.h4 small,.h4 .small,.h5 small,.h5 .small,.h6 small,.h6 .small{font-weight:normal;line-height:1;color:#4e4e4e}h1,.h1,h2,.h2,h3,.h3{margin-top:20px;margin-bottom:10px}h1 small,h1 .small,.h1 small,.h1 .small,h2 small,h2 .small,.h2 small,.h2 .small,h3 small,h3 .small,.h3 small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10px;margin-bottom:10px}h4 small,h4 .small,.h4 small,.h4 .small,h5 small,h5 .small,.h5 small,.h5 .small,h6 small,h6 .small,.h6 small,.h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:200;line-height:1.4}@media (min-width: 768px){.lead{font-size:21px}}small,.small{font-size:85%}cite{font-style:normal}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-muted{color:#4e4e4e}.text-primary{color:#fff}a.text-primary:hover{color:#e6e6e6}.text-success{color:#3c763d}a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:hover{color:#843534}.bg-primary{color:#fff}.bg-primary{background-color:#fff}a.bg-primary:hover{background-color:#e6e6e6}.bg-success{background-color:#dff0d8}a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #d9d9d9}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ul ol,ol ul,ol ol{margin-bottom:0}.list-unstyled,.list-inline{padding-left:0;list-style:none}.list-inline{margin-left:-5px}.list-inline>li{display:inline-block;padding-left:5px;padding-right:5px}dl{margin-top:0;margin-bottom:0}dt,dd{line-height:1.42857}dt{font-weight:bold}dd{margin-left:0}@media (min-width: 768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}.dl-horizontal dd:before,.dl-horizontal dd:after{content:" ";display:table}.dl-horizontal dd:after{clear:both}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #4e4e4e}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #d9d9d9}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.42857;color:#4e4e4e}blockquote footer:before,blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #d9d9d9;border-left:0;text-align:right}.blockquote-reverse footer:before,.blockquote-reverse small:before,.blockquote-reverse .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before,blockquote.pull-right .small:before{content:''}.blockquote-reverse footer:after,.blockquote-reverse small:after,.blockquote-reverse .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}blockquote:before,blockquote:after{content:""}address{margin-bottom:20px;font-style:normal;line-height:1.42857}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;white-space:nowrap;border-radius:0}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;box-shadow:inset 0 -1px 0 rgba(0,0,0,0.25)}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857;word-break:break-all;word-wrap:break-word;color:#303030;background-color:#f5f5f5;border:1px solid #ccc;border-radius:0}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.container:before,.container:after{content:" ";display:table}.container:after{clear:both}@media (min-width: 768px){.container{width:810px}}@media (min-width: 992px){.container{width:1010px}}@media (min-width: 1200px){.container{width:1170px}}.container-fluid{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.container-fluid:before,.container-fluid:after{content:" ";display:table}.container-fluid:after{clear:both}.row{margin-left:-15px;margin-right:-15px}.row:before,.row:after{content:" ";display:table}.row:after{clear:both}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-1{width:8.33333%}.col-xs-2{width:16.66667%}.col-xs-3{width:25%}.col-xs-4{width:33.33333%}.col-xs-5{width:41.66667%}.col-xs-6{width:50%}.col-xs-7{width:58.33333%}.col-xs-8{width:66.66667%}.col-xs-9{width:75%}.col-xs-10{width:83.33333%}.col-xs-11{width:91.66667%}.col-xs-12{width:100%}.col-xs-pull-0{right:0%}.col-xs-pull-1{right:8.33333%}.col-xs-pull-2{right:16.66667%}.col-xs-pull-3{right:25%}.col-xs-pull-4{right:33.33333%}.col-xs-pull-5{right:41.66667%}.col-xs-pull-6{right:50%}.col-xs-pull-7{right:58.33333%}.col-xs-pull-8{right:66.66667%}.col-xs-pull-9{right:75%}.col-xs-pull-10{right:83.33333%}.col-xs-pull-11{right:91.66667%}.col-xs-pull-12{right:100%}.col-xs-push-0{left:0%}.col-xs-push-1{left:8.33333%}.col-xs-push-2{left:16.66667%}.col-xs-push-3{left:25%}.col-xs-push-4{left:33.33333%}.col-xs-push-5{left:41.66667%}.col-xs-push-6{left:50%}.col-xs-push-7{left:58.33333%}.col-xs-push-8{left:66.66667%}.col-xs-push-9{left:75%}.col-xs-push-10{left:83.33333%}.col-xs-push-11{left:91.66667%}.col-xs-push-12{left:100%}.col-xs-offset-0{margin-left:0%}.col-xs-offset-1{margin-left:8.33333%}.col-xs-offset-2{margin-left:16.66667%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-4{margin-left:33.33333%}.col-xs-offset-5{margin-left:41.66667%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-7{margin-left:58.33333%}.col-xs-offset-8{margin-left:66.66667%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-10{margin-left:83.33333%}.col-xs-offset-11{margin-left:91.66667%}.col-xs-offset-12{margin-left:100%}@media (min-width: 768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-1{width:8.33333%}.col-sm-2{width:16.66667%}.col-sm-3{width:25%}.col-sm-4{width:33.33333%}.col-sm-5{width:41.66667%}.col-sm-6{width:50%}.col-sm-7{width:58.33333%}.col-sm-8{width:66.66667%}.col-sm-9{width:75%}.col-sm-10{width:83.33333%}.col-sm-11{width:91.66667%}.col-sm-12{width:100%}.col-sm-pull-0{right:0%}.col-sm-pull-1{right:8.33333%}.col-sm-pull-2{right:16.66667%}.col-sm-pull-3{right:25%}.col-sm-pull-4{right:33.33333%}.col-sm-pull-5{right:41.66667%}.col-sm-pull-6{right:50%}.col-sm-pull-7{right:58.33333%}.col-sm-pull-8{right:66.66667%}.col-sm-pull-9{right:75%}.col-sm-pull-10{right:83.33333%}.col-sm-pull-11{right:91.66667%}.col-sm-pull-12{right:100%}.col-sm-push-0{left:0%}.col-sm-push-1{left:8.33333%}.col-sm-push-2{left:16.66667%}.col-sm-push-3{left:25%}.col-sm-push-4{left:33.33333%}.col-sm-push-5{left:41.66667%}.col-sm-push-6{left:50%}.col-sm-push-7{left:58.33333%}.col-sm-push-8{left:66.66667%}.col-sm-push-9{left:75%}.col-sm-push-10{left:83.33333%}.col-sm-push-11{left:91.66667%}.col-sm-push-12{left:100%}.col-sm-offset-0{margin-left:0%}.col-sm-offset-1{margin-left:8.33333%}.col-sm-offset-2{margin-left:16.66667%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-4{margin-left:33.33333%}.col-sm-offset-5{margin-left:41.66667%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-7{margin-left:58.33333%}.col-sm-offset-8{margin-left:66.66667%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-10{margin-left:83.33333%}.col-sm-offset-11{margin-left:91.66667%}.col-sm-offset-12{margin-left:100%}}@media (min-width: 992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-1{width:8.33333%}.col-md-2{width:16.66667%}.col-md-3{width:25%}.col-md-4{width:33.33333%}.col-md-5{width:41.66667%}.col-md-6{width:50%}.col-md-7{width:58.33333%}.col-md-8{width:66.66667%}.col-md-9{width:75%}.col-md-10{width:83.33333%}.col-md-11{width:91.66667%}.col-md-12{width:100%}.col-md-pull-0{right:0%}.col-md-pull-1{right:8.33333%}.col-md-pull-2{right:16.66667%}.col-md-pull-3{right:25%}.col-md-pull-4{right:33.33333%}.col-md-pull-5{right:41.66667%}.col-md-pull-6{right:50%}.col-md-pull-7{right:58.33333%}.col-md-pull-8{right:66.66667%}.col-md-pull-9{right:75%}.col-md-pull-10{right:83.33333%}.col-md-pull-11{right:91.66667%}.col-md-pull-12{right:100%}.col-md-push-0{left:0%}.col-md-push-1{left:8.33333%}.col-md-push-2{left:16.66667%}.col-md-push-3{left:25%}.col-md-push-4{left:33.33333%}.col-md-push-5{left:41.66667%}.col-md-push-6{left:50%}.col-md-push-7{left:58.33333%}.col-md-push-8{left:66.66667%}.col-md-push-9{left:75%}.col-md-push-10{left:83.33333%}.col-md-push-11{left:91.66667%}.col-md-push-12{left:100%}.col-md-offset-0{margin-left:0%}.col-md-offset-1{margin-left:8.33333%}.col-md-offset-2{margin-left:16.66667%}.col-md-offset-3{margin-left:25%}.col-md-offset-4{margin-left:33.33333%}.col-md-offset-5{margin-left:41.66667%}.col-md-offset-6{margin-left:50%}.col-md-offset-7{margin-left:58.33333%}.col-md-offset-8{margin-left:66.66667%}.col-md-offset-9{margin-left:75%}.col-md-offset-10{margin-left:83.33333%}.col-md-offset-11{margin-left:91.66667%}.col-md-offset-12{margin-left:100%}}@media (min-width: 1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-1{width:8.33333%}.col-lg-2{width:16.66667%}.col-lg-3{width:25%}.col-lg-4{width:33.33333%}.col-lg-5{width:41.66667%}.col-lg-6{width:50%}.col-lg-7{width:58.33333%}.col-lg-8{width:66.66667%}.col-lg-9{width:75%}.col-lg-10{width:83.33333%}.col-lg-11{width:91.66667%}.col-lg-12{width:100%}.col-lg-pull-0{right:0%}.col-lg-pull-1{right:8.33333%}.col-lg-pull-2{right:16.66667%}.col-lg-pull-3{right:25%}.col-lg-pull-4{right:33.33333%}.col-lg-pull-5{right:41.66667%}.col-lg-pull-6{right:50%}.col-lg-pull-7{right:58.33333%}.col-lg-pull-8{right:66.66667%}.col-lg-pull-9{right:75%}.col-lg-pull-10{right:83.33333%}.col-lg-pull-11{right:91.66667%}.col-lg-pull-12{right:100%}.col-lg-push-0{left:0%}.col-lg-push-1{left:8.33333%}.col-lg-push-2{left:16.66667%}.col-lg-push-3{left:25%}.col-lg-push-4{left:33.33333%}.col-lg-push-5{left:41.66667%}.col-lg-push-6{left:50%}.col-lg-push-7{left:58.33333%}.col-lg-push-8{left:66.66667%}.col-lg-push-9{left:75%}.col-lg-push-10{left:83.33333%}.col-lg-push-11{left:91.66667%}.col-lg-push-12{left:100%}.col-lg-offset-0{margin-left:0%}.col-lg-offset-1{margin-left:8.33333%}.col-lg-offset-2{margin-left:16.66667%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-4{margin-left:33.33333%}.col-lg-offset-5{margin-left:41.66667%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-7{margin-left:58.33333%}.col-lg-offset-8{margin-left:66.66667%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-10{margin-left:83.33333%}.col-lg-offset-11{margin-left:91.66667%}.col-lg-offset-12{margin-left:100%}}table{max-width:100%;background-color:transparent}th{text-align:left}.table{width:100%;margin-bottom:20px}.table>thead>tr>th,.table>thead>tr>td,.table>tbody>tr>th,.table>tbody>tr>td,.table>tfoot>tr>th,.table>tfoot>tr>td{padding:10px;line-height:1.42857;vertical-align:top;border-top:1px solid #ccc}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ccc}.table>caption+thead>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>th,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ccc}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ccc}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>tfoot>tr>td{border:1px solid #ccc}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-child(odd)>td,.table-striped>tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover>tbody>tr:hover>td,.table-hover>tbody>tr:hover>th{background-color:#f5f5f5}table col[class*="col-"]{position:static;float:none;display:table-column}table td[class*="col-"],table th[class*="col-"]{position:static;float:none;display:table-cell}.table>thead>tr>td.active,.table>thead>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th{background-color:#e8e8e8}.table>thead>tr>td.success,.table>thead>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th{background-color:#dff0d8}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th{background-color:#d0e9c6}.table>thead>tr>td.info,.table>thead>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th{background-color:#d9edf7}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th{background-color:#c4e3f3}.table>thead>tr>td.warning,.table>thead>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th{background-color:#faf2cc}.table>thead>tr>td.danger,.table>thead>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th{background-color:#f2dede}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th{background-color:#ebcccc}@media (max-width: 767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;overflow-x:scroll;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ccc;-webkit-overflow-scrolling:touch}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{padding:0;margin:0;border:0;min-width:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#303030;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;margin-bottom:5px;font-weight:bold}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type="file"]{display:block}input[type="range"]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:0 none}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out 0.15s,box-shadow ease-in-out 0.15s;transition:border-color ease-in-out 0.15s,box-shadow ease-in-out 0.15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6)}.form-control::-moz-placeholder{color:#4e4e4e;opacity:1}.form-control:-ms-input-placeholder{color:#4e4e4e}.form-control::-webkit-input-placeholder{color:#4e4e4e}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#d9d9d9;opacity:1}textarea.form-control{height:auto}input[type="search"]{-webkit-appearance:none}input[type="date"]{line-height:34px}.form-group{margin-bottom:15px}.radio,.checkbox{display:block;min-height:20px;margin-top:10px;margin-bottom:10px;padding-left:20px}.radio label,.checkbox label{display:inline;font-weight:normal;cursor:pointer}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{float:left;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:normal;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type="radio"][disabled],fieldset[disabled] input[type="radio"],input[type="checkbox"][disabled],fieldset[disabled] input[type="checkbox"],.radio[disabled],fieldset[disabled] .radio,.radio-inline[disabled],fieldset[disabled] .radio-inline,.checkbox[disabled],fieldset[disabled] .checkbox,.checkbox-inline[disabled],fieldset[disabled] .checkbox-inline{cursor:not-allowed}.input-sm,.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm,.input-group-sm>select.form-control,.input-group-sm>select.input-group-addon,.input-group-sm>.input-group-btn>select.btn{height:30px;line-height:30px}textarea.input-sm,.input-group-sm>textarea.form-control,.input-group-sm>textarea.input-group-addon,.input-group-sm>.input-group-btn>textarea.btn,select[multiple].input-sm,.input-group-sm>select[multiple].form-control,.input-group-sm>select[multiple].input-group-addon,.input-group-sm>.input-group-btn>select[multiple].btn{height:auto}.input-lg,.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-lg,.input-group-lg>select.form-control,.input-group-lg>select.input-group-addon,.input-group-lg>.input-group-btn>select.btn{height:46px;line-height:46px}textarea.input-lg,.input-group-lg>textarea.form-control,.input-group-lg>textarea.input-group-addon,.input-group-lg>.input-group-btn>textarea.btn,select[multiple].input-lg,.input-group-lg>select[multiple].form-control,.input-group-lg>select[multiple].input-group-addon,.input-group-lg>.input-group-btn>select[multiple].btn{height:auto}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.has-feedback .form-control-feedback{position:absolute;top:25px;right:0;display:block;width:34px;height:34px;line-height:34px;text-align:center}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;border-color:#3c763d;background-color:#dff0d8}.has-success .form-control-feedback{color:#3c763d}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;border-color:#8a6d3b;background-color:#fcf8e3}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;border-color:#a94442;background-color:#f2dede}.has-error .form-control-feedback{color:#a94442}.form-control-static{margin-bottom:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#626262}@media (min-width: 768px){.form-inline .form-group,.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control,.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .input-group>.form-control,.navbar-form .input-group>.form-control{width:100%}.form-inline .control-label,.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.navbar-form .radio,.form-inline .checkbox,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;padding-left:0;vertical-align:middle}.form-inline .radio input[type="radio"],.navbar-form .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"],.navbar-form .checkbox input[type="checkbox"]{float:none;margin-left:0}.form-inline .has-feedback .form-control-feedback,.navbar-form .has-feedback .form-control-feedback{top:0}}.form-horizontal .control-label,.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{margin-top:0;margin-bottom:0;padding-top:7px}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-left:-15px;margin-right:-15px}.form-horizontal .form-group:before,.form-horizontal .form-group:after{content:" ";display:table}.form-horizontal .form-group:after{clear:both}.form-horizontal .form-control-static{padding-top:7px}@media (min-width: 768px){.form-horizontal .control-label{text-align:right}}.form-horizontal .has-feedback .form-control-feedback{top:0;right:15px}.btn{display:inline-block;margin-bottom:0;font-weight:normal;text-align:center;vertical-align:middle;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;padding:6px 12px;font-size:14px;line-height:1.42857;border-radius:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus{outline:0 none}.btn:hover,.btn:focus{color:#333;text-decoration:none}.btn:active,.btn.active{outline:0;background-image:none;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;pointer-events:none;opacity:0.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active{color:#333;background-color:#ebebeb;border-color:#adadad}.open .btn-default.dropdown-toggle{color:#333;background-color:#ebebeb;border-color:#adadad}.btn-default:active,.btn-default.active{background-image:none}.open .btn-default.dropdown-toggle{background-image:none}.btn-default.disabled,.btn-default.disabled:hover,.btn-default.disabled:focus,.btn-default.disabled:active,.btn-default.disabled.active,.btn-default[disabled],.btn-default[disabled]:hover,.btn-default[disabled]:focus,.btn-default[disabled]:active,.btn-default[disabled].active,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default:hover,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#fff;border-color:#f2f2f2}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active{color:#fff;background-color:#ebebeb;border-color:#d4d4d4}.open .btn-primary.dropdown-toggle{color:#fff;background-color:#ebebeb;border-color:#d4d4d4}.btn-primary:active,.btn-primary.active{background-image:none}.open .btn-primary.dropdown-toggle{background-image:none}.btn-primary.disabled,.btn-primary.disabled:hover,.btn-primary.disabled:focus,.btn-primary.disabled:active,.btn-primary.disabled.active,.btn-primary[disabled],.btn-primary[disabled]:hover,.btn-primary[disabled]:focus,.btn-primary[disabled]:active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary:hover,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary.active{background-color:#fff;border-color:#f2f2f2}.btn-primary .badge{color:#fff;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active{color:#fff;background-color:#47a447;border-color:#398439}.open .btn-success.dropdown-toggle{color:#fff;background-color:#47a447;border-color:#398439}.btn-success:active,.btn-success.active{background-image:none}.open .btn-success.dropdown-toggle{background-image:none}.btn-success.disabled,.btn-success.disabled:hover,.btn-success.disabled:focus,.btn-success.disabled:active,.btn-success.disabled.active,.btn-success[disabled],.btn-success[disabled]:hover,.btn-success[disabled]:focus,.btn-success[disabled]:active,.btn-success[disabled].active,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success:hover,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active{color:#fff;background-color:#39b3d7;border-color:#269abc}.open .btn-info.dropdown-toggle{color:#fff;background-color:#39b3d7;border-color:#269abc}.btn-info:active,.btn-info.active{background-image:none}.open .btn-info.dropdown-toggle{background-image:none}.btn-info.disabled,.btn-info.disabled:hover,.btn-info.disabled:focus,.btn-info.disabled:active,.btn-info.disabled.active,.btn-info[disabled],.btn-info[disabled]:hover,.btn-info[disabled]:focus,.btn-info[disabled]:active,.btn-info[disabled].active,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info:hover,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active{color:#fff;background-color:#ed9c28;border-color:#d58512}.open .btn-warning.dropdown-toggle{color:#fff;background-color:#ed9c28;border-color:#d58512}.btn-warning:active,.btn-warning.active{background-image:none}.open .btn-warning.dropdown-toggle{background-image:none}.btn-warning.disabled,.btn-warning.disabled:hover,.btn-warning.disabled:focus,.btn-warning.disabled:active,.btn-warning.disabled.active,.btn-warning[disabled],.btn-warning[disabled]:hover,.btn-warning[disabled]:focus,.btn-warning[disabled]:active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning:hover,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active{color:#fff;background-color:#d2322d;border-color:#ac2925}.open .btn-danger.dropdown-toggle{color:#fff;background-color:#d2322d;border-color:#ac2925}.btn-danger:active,.btn-danger.active{background-image:none}.open .btn-danger.dropdown-toggle{background-image:none}.btn-danger.disabled,.btn-danger.disabled:hover,.btn-danger.disabled:focus,.btn-danger.disabled:active,.btn-danger.disabled.active,.btn-danger[disabled],.btn-danger[disabled]:hover,.btn-danger[disabled]:focus,.btn-danger[disabled]:active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger:hover,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{color:#005b9a;font-weight:normal;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#ee5161;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:hover,fieldset[disabled] .btn-link:focus{color:#818181;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-sm,.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%;padding-left:0;padding-right:0}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity 0.15s linear;transition:opacity 0.15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height 0.35s ease;transition:height 0.35s ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;font-size:14px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);border-radius:0;-webkit-box-shadow:0 6px 12px rgba(0,0,0,0.175);box-shadow:0 6px 12px rgba(0,0,0,0.175);background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:1.42857;color:#303030;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{text-decoration:none;color:#303030;background-color:#d9d9d9}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;outline:0;background-color:#ee5161}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#4e4e4e}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);cursor:not-allowed}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{left:auto;right:0}.dropdown-menu-left{left:0;right:auto}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857;color:#4e4e4e}.dropdown-backdrop{position:fixed;left:0;right:0;bottom:0;top:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}@media (min-width: 768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{left:0;right:auto}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn:hover,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn.active{z-index:2}.btn-group>.btn:focus,.btn-group-vertical>.btn:focus{outline:none}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar:before,.btn-toolbar:after{content:" ";display:table}.btn-toolbar:after{clear:both}.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px}.btn-group>.btn-lg+.dropdown-toggle,.btn-group-lg.btn-group>.btn+.dropdown-toggle{padding-left:12px;padding-right:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret,.btn-group-lg>.btn .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret,.dropup .btn-group-lg>.btn .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after{content:" ";display:table}.btn-group-vertical>.btn-group:after{clear:both}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-bottom-left-radius:0;border-top-right-radius:0;border-top-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{float:none;display:table-cell;width:1%}.btn-group-justified>.btn-group .btn{width:100%}[data-toggle="buttons"]>.btn>input[type="radio"],[data-toggle="buttons"]>.btn>input[type="checkbox"]{display:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*="col-"]{float:none;padding-left:0;padding-right:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:normal;line-height:1;color:#555;text-align:center;background-color:#d9d9d9;border:1px solid #ccc;border-radius:0}.input-group-addon.input-sm,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.input-group-addon.btn{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.input-group-addon.btn{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type="radio"],.input-group-addon input[type="checkbox"]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{margin-left:-1px}.nav{margin-bottom:0;padding-left:0;list-style:none}.nav:before,.nav:after{content:" ";display:table}.nav:after{clear:both}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#d9d9d9}.nav>li.disabled>a{color:#4e4e4e}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#4e4e4e;text-decoration:none;background-color:transparent;cursor:not-allowed}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#d9d9d9;border-color:#005b9a}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #e0e0e0}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857;border:1px solid transparent;border-radius:0 0 0 0;color:#222}.nav-tabs>li>a:hover,.nav-tabs>li>a:focus{background:inherit;border-color:inherit inherit #e0e0e0}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#222;background-color:#d9d9d9;border:1px solid inherit;border-bottom-color:transparent;cursor:default}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:0}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#ee5161}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified,.nav-tabs.nav-justified{width:100%}.nav-justified>li,.nav-tabs.nav-justified>li{float:none}.nav-justified>li>a,.nav-tabs.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width: 768px){.nav-justified>li,.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a,.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified,.nav-tabs.nav-justified{border-bottom:0}.nav-tabs-justified>li>a,.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:0}.nav-tabs-justified>.active>a,.nav-tabs.nav-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media (min-width: 768px){.nav-tabs-justified>li>a,.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:0 0 0 0}.nav-tabs-justified>.active>a,.nav-tabs.nav-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#303030}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}.navbar:before,.navbar:after{content:" ";display:table}.navbar:after{clear:both}@media (min-width: 768px){.navbar{border-radius:0}}.navbar-header:before,.navbar-header:after{content:" ";display:table}.navbar-header:after{clear:both}@media (min-width: 768px){.navbar-header{float:left}}.navbar-collapse{max-height:340px;overflow-x:visible;padding-right:0;padding-left:0;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);-webkit-overflow-scrolling:touch}.navbar-collapse:before,.navbar-collapse:after{content:" ";display:table}.navbar-collapse:after{clear:both}.navbar-collapse.in{overflow-y:auto}@media (min-width: 768px){.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block !important;height:auto !important;padding-bottom:0;overflow:visible !important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-left:0;padding-right:0}}.container>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-header,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}@media (min-width: 768px){.container>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-header,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width: 768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}@media (min-width: 768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:15px 0;font-size:18px;line-height:20px;height:50px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}@media (min-width: 768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:0}}.navbar-toggle{position:relative;float:right;margin-right:0;padding:9px 10px;margin-top:8px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:0}.navbar-toggle:focus{outline:none}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width: 768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px 0}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width: 767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media (min-width: 768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}.navbar-nav.navbar-right:last-child{margin-right:0}}@media (min-width: 768px){.navbar-left{float:left !important}.navbar-right{float:right !important}}.navbar-form{margin-left:0;margin-right:0;padding:10px 0;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);margin-top:8px;margin-bottom:8px}@media (max-width: 767px){.navbar-form .form-group{margin-bottom:5px}}@media (min-width: 768px){.navbar-form{width:auto;border:0;margin-left:0;margin-right:0;padding-top:0;padding-bottom:0;-webkit-box-shadow:none;box-shadow:none}.navbar-form.navbar-right:last-child{margin-right:0}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm,.btn-group-sm>.navbar-btn.btn{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs,.btn-group-xs>.navbar-btn.btn{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width: 768px){.navbar-text{float:left;margin-left:0;margin-right:0}.navbar-text.navbar-right:last-child{margin-right:0}}.navbar-default{background-color:#ececec;border-color:#e0e0e0}.navbar-default .navbar-brand{color:#fff}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#e6e6e6;background-color:#008b44}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#222}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#222;background-color:#e4e4e4}.navbar-default .navbar-nav>.has-dropdown:not(.active):hover>a:first-child{color:#222;background-color:#e4e4e4}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#fff;background-color:#ee5161}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e0e0e0}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{background-color:#ee5161;color:#fff}@media (max-width: 767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#222}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#222;background-color:#e4e4e4}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#ee5161}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#222}.navbar-default .navbar-link:hover{color:#222}.navbar-inverse{background-color:#ccc;border-color:transparent}.navbar-inverse .navbar-brand{color:#fff}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#222}.navbar-inverse .navbar-nav>li>a{color:#222}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#222;background-color:#d9d9d9}.navbar-inverse .navbar-nav>li.has-dropdown:hover>a:first-child{color:#222;background-color:#d9d9d9}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#353535}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#bababa}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{background-color:#353535;color:#fff}@media (max-width: 767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#222}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#222;background-color:#d9d9d9}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#353535}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#222}.navbar-inverse .navbar-link:hover{color:#222}.pager{padding-left:0;margin:20px 0;list-style:none;text-align:center}.pager:before,.pager:after{content:" ";display:table}.pager:after{clear:both}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#d9d9d9}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#4e4e4e;background-color:#fff;cursor:not-allowed}.label{display:inline;padding:.2em .6em .3em;font-size:13px;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em;background:#444}.label[href]:hover,.label[href]:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#444;color:white}.label-default[href]:hover,.label-default[href]:focus{background-color:#2b2b2b}.label-primary{background-color:#fff;color:white}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#e6e6e6}.label-success{background-color:#5cb85c;color:white}.label-success[href]:hover,.label-success[href]:focus{background-color:#449d44}.label-info{background-color:#5bc0de;color:white}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f0ad4e;color:white}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#d9534f;color:white}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:bold;color:inherit;line-height:1;vertical-align:baseline;white-space:nowrap;text-align:center;background-color:#4e4e4e;border-radius:0}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge,.btn-group-xs>.btn .badge{top:0;padding:1px 5px}a.badge:hover,a.badge:focus{color:inherit;text-decoration:none;cursor:pointer}a.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#4d99d8;background-color:#fff}.nav-pills>li>a>.badge{margin-left:3px}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:0}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:bold}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable{padding-right:35px}.alert-dismissable .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{background-color:#dff0d8;border-color:#d6e9c6;color:#3c763d}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{background-color:#d9edf7;border-color:#bce8f1;color:#31708f}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{background-color:#fcf8e3;border-color:#faebcc;color:#8a6d3b}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{background-color:#f2dede;border-color:#ebccd1;color:#a94442}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:0;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.05);box-shadow:0 1px 1px rgba(0,0,0,0.05)}.panel-body{padding:15px}.panel-body:before,.panel-body:after{content:" ";display:table}.panel-body:after{clear:both}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:-1;border-top-left-radius:-1}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:-1;border-bottom-left-radius:-1}.panel>.list-group{margin-bottom:0}.panel>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-right-radius:-1;border-top-left-radius:-1}.panel>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:-1;border-bottom-left-radius:-1}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-right-radius:-1;border-top-left-radius:-1}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:-1}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:-1}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:-1;border-bottom-left-radius:-1}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:-1}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:-1}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive{border-top:1px solid #ccc}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{border:0;margin-bottom:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:0;overflow:hidden}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse .panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#303030;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse .panel-body{border-top-color:#ddd}.panel-default>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#fff}.panel-primary>.panel-heading{color:#fff;background-color:#fff;border-color:#fff}.panel-primary>.panel-heading+.panel-collapse .panel-body{border-top-color:#fff}.panel-primary>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#fff}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse .panel-body{border-top-color:#d6e9c6}.panel-success>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse .panel-body{border-top-color:#bce8f1}.panel-info>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse .panel-body{border-top-color:#faebcc}.panel-warning>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse .panel-body{border-top-color:#ebccd1}.panel-danger>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ebccd1}.well{min-height:20px;padding:0;margin-bottom:20px;background-color:inherit;border:1px solid inherit;border-radius:0}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:bold;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:0.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:0.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{display:none;overflow:auto;overflow-y:scroll;position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0, -25%);-ms-transform:translate(0, -25%);transform:translate(0, -25%);-webkit-transition:-webkit-transform 0.3s ease-out;-moz-transition:-moz-transform 0.3s ease-out;-o-transition:-o-transform 0.3s ease-out;transition:transform 0.3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);transform:translate(0, 0)}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,0.2);border-radius:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,0.5);box-shadow:0 3px 9px rgba(0,0,0,0.5);background-clip:padding-box;outline:none}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:0.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid transparent;min-height:16.42857px}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857}.modal-body{position:relative;padding:20px}.modal-footer{margin-top:15px;padding:19px 20px 20px;text-align:right;border-top:1px solid transparent}.modal-footer:before,.modal-footer:after{content:" ";display:table}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}@media (min-width: 768px){.modal-dialog{width:760px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,0.5);box-shadow:0 5px 15px rgba(0,0,0,0.5)}.modal-sm{width:300px}}@media (min-width: 992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1030;display:block;visibility:visible;font-size:12px;line-height:1.4;opacity:0;filter:alpha(opacity=0)}.tooltip.in{opacity:0.9;filter:alpha(opacity=90)}.tooltip.top{margin-top:-3px;padding:5px 0}.tooltip.right{margin-left:3px;padding:0 5px}.tooltip.bottom{margin-top:3px;padding:5px 0}.tooltip.left{margin-left:-3px;padding:0 5px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;border-radius:0}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{bottom:0;left:5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;right:5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;left:5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;right:5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;max-width:276px;padding:1px;text-align:left;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);border-radius:3px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);white-space:normal}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{margin:0;padding:8px 14px;font-size:14px;font-weight:normal;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:3px 3px 0 0}.popover-content{padding:5px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{border-width:10px;content:""}.popover.top>.arrow{left:50%;margin-left:-11px;border-bottom-width:0;border-top-color:#999;border-top-color:fadein(rgba(0,0,0,0.2), 5%);bottom:-11px}.popover.top>.arrow:after{content:" ";bottom:1px;margin-left:-10px;border-bottom-width:0;border-top-color:#fff}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-left-width:0;border-right-color:#999;border-right-color:fadein(rgba(0,0,0,0.2), 5%)}.popover.right>.arrow:after{content:" ";left:1px;bottom:-10px;border-left-width:0;border-right-color:#fff}.popover.bottom>.arrow{left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:fadein(rgba(0,0,0,0.2), 5%);top:-11px}.popover.bottom>.arrow:after{content:" ";top:1px;margin-left:-10px;border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:fadein(rgba(0,0,0,0.2), 5%)}.popover.left>.arrow:after{content:" ";right:1px;border-right-width:0;border-left-color:#fff;bottom:-10px}.clearfix:before,.clearfix:after{content:" ";display:table}.clearfix:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right !important}.pull-left{float:left !important}.hide{display:none !important}.show{display:block !important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none !important;visibility:hidden !important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none !important}@media (max-width: 767px){.visible-xs{display:block !important}table.visible-xs{display:table}tr.visible-xs{display:table-row !important}th.visible-xs,td.visible-xs{display:table-cell !important}}@media (min-width: 768px) and (max-width: 991px){.visible-sm{display:block !important}table.visible-sm{display:table}tr.visible-sm{display:table-row !important}th.visible-sm,td.visible-sm{display:table-cell !important}}@media (min-width: 992px) and (max-width: 1199px){.visible-md{display:block !important}table.visible-md{display:table}tr.visible-md{display:table-row !important}th.visible-md,td.visible-md{display:table-cell !important}}@media (min-width: 1200px){.visible-lg{display:block !important}table.visible-lg{display:table}tr.visible-lg{display:table-row !important}th.visible-lg,td.visible-lg{display:table-cell !important}}@media (max-width: 767px){.hidden-xs{display:none !important}}@media (min-width: 768px) and (max-width: 991px){.hidden-sm{display:none !important}}@media (min-width: 992px) and (max-width: 1199px){.hidden-md{display:none !important}}@media (min-width: 1200px){.hidden-lg{display:none !important}}.visible-print{display:none !important}@media print{.visible-print{display:block !important}table.visible-print{display:table}tr.visible-print{display:table-row !important}th.visible-print,td.visible-print{display:table-cell !important}}@media print{.hidden-print{display:none !important}}.spinner{text-align:center}.spinner>div{width:8px;height:8px;background-color:#222;border-radius:100%;display:inline-block;-webkit-animation:bouncedelay 1.4s infinite ease-in-out;animation:bouncedelay 1.4s infinite ease-in-out;-webkit-animation-fill-mode:both;animation-fill-mode:both}.spinner .bounce1{-webkit-animation-delay:-0.32s;animation-delay:-0.32s}.spinner .bounce2{-webkit-animation-delay:-0.16s;animation-delay:-0.16s}@-webkit-keyframes bouncedelay{0%,80%,100%{-webkit-transform:scale(0)}40%{-webkit-transform:scale(1)}}@keyframes bouncedelay{0%,80%,100%{transform:scale(0);-webkit-transform:scale(0)}40%{transform:scale(1);-webkit-transform:scale(1)}}.line-btn{display:inline-block;text-align:center;opacity:1;background-color:#e0e0e0;border-bottom:2px solid #e0e0e0;color:#222}.line-btn span{display:inline-block;height:100%;line-height:100%;padding:8px}.line-btn:hover,.line-btn:focus{text-decoration:none;opacity:0.85}.line-btn .snf-font-remove{display:inline}.line-btn.disabled{background:transparent !important;border-color:#818181 !important;color:#818181 !important}.line-btn.disabled:hover,.line-btn.disabled:focus{cursor:default;opacity:1}.line-btn.disabled:hover span,.line-btn.disabled:focus span{color:#818181 !important}.line-btn:hover,.line-btn:focus{opacity:1;border-bottom-color:#222;color:#222}.outline-btn{display:inline-block;text-align:center;opacity:1;border:1px solid #222;color:#222}.outline-btn span{display:inline-block;height:100%;line-height:100%;padding:8px}.outline-btn:hover,.outline-btn:focus{text-decoration:none;opacity:0.85}.outline-btn .snf-font-remove{display:inline}.outline-btn.disabled{background:transparent !important;border-color:#818181 !important;color:#818181 !important}.outline-btn.disabled:hover,.outline-btn.disabled:focus{cursor:default;opacity:1}.outline-btn.disabled:hover span,.outline-btn.disabled:focus span{color:#818181 !important}.outline-btn span{border:1px solid transparent;width:100%}.outline-btn:hover span,.outline-btn:focus span{border-color:#222}.outline-btn.disabled{@inlcude disabled;;color:#818181}.outline-btn.disabled:hover span,.outline-btn.disabled:focus span{border-color:transparent}.custom-btn{display:inline-block;text-align:center;opacity:1;border:1px solid #3c96e0;color:#fff;background-color:#3c96e0}.custom-btn span{display:inline-block;height:100%;line-height:100%;padding:8px}.custom-btn:hover,.custom-btn:focus{text-decoration:none;opacity:0.85}.custom-btn .snf-font-remove{display:inline}.custom-btn.disabled{background:transparent !important;border-color:#818181 !important;color:#818181 !important}.custom-btn.disabled:hover,.custom-btn.disabled:focus{cursor:default;opacity:1}.custom-btn.disabled:hover span,.custom-btn.disabled:focus span{color:#818181 !important}.custom-btn span{border:1px solid transparent;background:transparent}.custom-btn:hover span,.custom-btn:focus span{color:#fff}.custom-btn.disabled{background:transparent !important;border-color:#818181 !important;color:#818181 !important}.custom-btn.disabled:hover,.custom-btn.disabled:focus{cursor:default;opacity:1}.custom-btn.disabled:hover span,.custom-btn.disabled:focus span{color:#818181 !important}.custom-btn[data-karma="neutral"]{background-color:#3c96e0;border-color:#3c96e0}.custom-btn[data-karma="good"]{background-color:#00a551;border-color:#00a551}.custom-btn[data-karma="bad"]{background-color:#d2881f;border-color:#d2881f}.custom-btn[data-caution="warning"][data-karma="good"],.custom-btn[data-caution="warning"][data-karma="neutral"]{background-color:#d2881f;border-color:#d2881f}.custom-btn[data-caution="dangerous"][data-karma="bad"],.custom-btn[data-caution="dangerous"][data-karma="neutral"]{background-color:#e42a48;border-color:#e42a48}.search-btn{display:inline-block;text-align:center;opacity:1;background-color:#e0e0e0;border-bottom:2px solid #e0e0e0;color:#222;position:relative;top:-2px;margin-left:20px;cursor:pointer}.search-btn span{display:inline-block;height:100%;line-height:100%;padding:8px}.search-btn:hover,.search-btn:focus{text-decoration:none;opacity:0.85}.search-btn .snf-font-remove{display:inline}.search-btn.disabled{background:transparent !important;border-color:#818181 !important;color:#818181 !important}.search-btn.disabled:hover,.search-btn.disabled:focus{cursor:default;opacity:1}.search-btn.disabled:hover span,.search-btn.disabled:focus span{color:#818181 !important}.search-btn:hover,.search-btn:focus{opacity:1;border-bottom-color:#222;color:#222}.search-btn span{padding:7px}.search-mode-btn{float:right;line-height:30px}.search-mode-btn:hover{cursor:pointer}.instructions .line-btn{padding:8px 10px}.instructions .line-btn span{padding:0 4px}.instructions .line-btn:hover .arrow{font-weight:bold}.instructions .line-btn.open:hover{border-bottom-color:transparent}.instructions .line-btn .arrow{vertical-align:middle}.sidebar{margin:0 30px 0 0;width:110px;height:auto;float:left}.sidebar .btn-group-vertical{width:100%}@media (max-width: 1200px){.sidebar{width:auto;margin:20px auto;float:none}.sidebar .btn-group-vertical a{margin-right:10px;display:inline-block}}.sidebar .custom-btn{display:block;margin:0 0 1em}.sidebar .custom-btn span{padding:8px}body .custom-buttons{float:left;margin-right:10px}body .custom-buttons .line-btn{margin-right:1em}body .custom-buttons .disabled{display:none}body .custom-buttons .extra-btn{float:right;margin-right:0}body .custom-buttons .extra-btn span{display:inline-block}body .custom-buttons .extra-btn .badge{background:transparent;line-height:0.8;display:inline;padding:0 5px 0 0;font-weight:normal;font-size:1em}body .custom-buttons .extra-btn .badge::before{content:"("}body .custom-buttons .extra-btn .badge::after{content:")"}.show-hide-all{float:right}.show-hide-all em{font-style:normal}.show-hide-all.line-btn{padding:8px}.show-hide-all.line-btn span{display:inline}.actions-per-item .custom-btn{margin:10px 10px 10px 0}.charts .chart{display:none}.charts .sidebar a{display:inline-block;text-align:center;opacity:1;border:1px solid #222;color:#222;display:block;margin:20px auto}.charts .sidebar a span{display:inline-block;height:100%;line-height:100%;padding:8px}.charts .sidebar a:hover,.charts .sidebar a:focus{text-decoration:none;opacity:0.85}.charts .sidebar a .snf-font-remove{display:inline}.charts .sidebar a.disabled{background:transparent !important;border-color:#818181 !important;color:#818181 !important}.charts .sidebar a.disabled:hover,.charts .sidebar a.disabled:focus{cursor:default;opacity:1}.charts .sidebar a.disabled:hover span,.charts .sidebar a.disabled:focus span{color:#818181 !important}.charts .sidebar a span{border:1px solid transparent;width:100%}.charts .sidebar a:hover span,.charts .sidebar a:focus span{border-color:#222}.charts .sidebar a.disabled{@inlcude disabled;;color:#818181}.charts .sidebar a.disabled:hover span,.charts .sidebar a.disabled:focus span{border-color:transparent}.charts .sidebar a.active{display:inline-block;text-align:center;opacity:1;border:1px solid #3c96e0;color:#fff;background-color:#3c96e0;display:block}.charts .sidebar a.active span{display:inline-block;height:100%;line-height:100%;padding:8px}.charts .sidebar a.active:hover,.charts .sidebar a.active:focus{text-decoration:none;opacity:0.85}.charts .sidebar a.active .snf-font-remove{display:inline}.charts .sidebar a.active.disabled{background:transparent !important;border-color:#818181 !important;color:#818181 !important}.charts .sidebar a.active.disabled:hover,.charts .sidebar a.active.disabled:focus{cursor:default;opacity:1}.charts .sidebar a.active.disabled:hover span,.charts .sidebar a.active.disabled:focus span{color:#818181 !important}.charts .sidebar a.active span{border:1px solid transparent;background:transparent}.charts .sidebar a.active:hover span,.charts .sidebar a.active:focus span{color:#fff}.charts .sidebar a.active.disabled{background:transparent !important;border-color:#818181 !important;color:#818181 !important}.charts .sidebar a.active.disabled:hover,.charts .sidebar a.active.disabled:focus{cursor:default;opacity:1}.charts .sidebar a.active.disabled:hover span,.charts .sidebar a.active.disabled:focus span{color:#818181 !important}@media (max-width: 1200px){.charts .sidebar a,.charts .sidebar a.active{margin-right:10px;display:inline-block}}.notify .reload-btn{padding:0 4px;font-size:18px;vertical-align:middle;cursor:pointer}.onoffswitch{display:inline-block;float:right;position:relative;width:134px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}.onoffswitch-checkbox{display:none}.onoffswitch-label{display:block;overflow:hidden;cursor:pointer;border-radius:20px}.onoffswitch-inner{display:block;width:200%;margin-left:-100%;-moz-transition:margin 0.3s ease-in 0s;-webkit-transition:margin 0.3s ease-in 0s;-o-transition:margin 0.3s ease-in 0s;transition:margin 0.3s ease-in 0s}.onoffswitch-inner:before,.onoffswitch-inner:after{display:block;float:left;width:50%;height:30px;padding:0;line-height:30px;font-size:12px;color:white;font-family:Trebuchet, Arial, sans-serif;font-weight:normal;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.onoffswitch-inner:before{content:"Standard View";padding-left:10px;background-color:#e0e0e0;color:#222}.onoffswitch-inner:after{content:"Compact View";padding-right:10px;background-color:#e0e0e0;color:#222;text-align:right}.onoffswitch-switch{display:block;width:19px;margin:6px;background:#222;border:2px solid #F7EFEF;border-radius:20px;position:absolute;top:0;bottom:4px;right:103px;-moz-transition:all 0.3s ease-in 0s;-webkit-transition:all 0.3s ease-in 0s;-o-transition:all 0.3s ease-in 0s;transition:all 0.3s ease-in 0s}.onoffswitch-checkbox:checked+.onoffswitch-label .onoffswitch-inner{margin-left:0}.onoffswitch-checkbox:checked+.onoffswitch-label .onoffswitch-switch{right:0px}li.active .snf-checkbox-unchecked,li.active .snf-radio-unchecked{display:none}li:not(.active) .snf-checkbox-checked,li:not(.active) .snf-radio-checked{display:none}table.dataTable tbody tr.selected .snf-checkbox-unchecked{display:none}table.dataTable tbody tr:not(.selected) .snf-checkbox-checked{display:none}.show-hide-all.open .snf-font-arrow-down{display:none}.show-hide-all:not(.open) .snf-font-arrow-up{display:none}.instructions .line-btn.open .snf-angle-down{display:none}.instructions .line-btn:not(.open) .snf-angle-up{display:none}@font-face{font-family:'font-icons';src:url("../fonts/font-icons.eot?hm0cup");src:url("../fonts/font-icons.eot?#iefixhm0cup") format("embedded-opentype"),url("../fonts/font-icons.woff?hm0cup") format("woff"),url("../fonts/font-icons.ttf?hm0cup") format("truetype"),url("../fonts/font-icons.svg?hm0cup#font-icons") format("svg");font-weight:normal;font-style:normal}@font-face{font-family:"snf-font";src:url("../fonts/snf-font.eot");src:url("../fonts/snf-font.eot?#iefix") format("embedded-opentype"),url("../fonts/snf-font.woff") format("woff"),url("../fonts/snf-font.ttf") format("truetype"),url("../fonts/snf-font.svg#snf-font") format("svg");font-weight:normal;font-style:normal}.snf-ok{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-ok:before{content:"\61"}.snf-remove,body .custom-buttons .snf-font-remove{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-remove:before,body .custom-buttons .snf-font-remove:before{content:"\62"}.snf-envelope{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-envelope:before{content:"\63"}.snf-envelope-alt{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-envelope-alt:before{content:"\64"}.snf-angle-up{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-angle-up:before{content:"\65"}.snf-angle-down{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-angle-down:before{content:"\66"}.snf-exclamation-sign{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-exclamation-sign:before{content:"\67"}.snf-clipboard-h,.snf-details-project{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-clipboard-h:before,.snf-details-project:before{content:"\68"}.snf-clipboard-i{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-clipboard-i:before{content:"\69"}.snf-copy{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-copy:before{content:"\6c"}.snf-search{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-search:before{content:"\6d"}.snf-sign-out{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-sign-out:before{content:"\6e"}.snf-archive{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-archive:before{content:"\6b"}.snf-checkbox-checked{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-checkbox-checked:before{content:"\6f"}.snf-checkbox-unchecked{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-checkbox-unchecked:before{content:"\70"}.snf-radio-checked{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-radio-checked:before{content:"\71"}.snf-radio-unchecked{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-radio-unchecked:before{content:"\72"}.snf-info{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-info:before{content:"\73"}.snf-user-outline{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-user-outline:before{content:"\75"}.snf-user-full,.snf-details-user{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-user-full:before,.snf-details-user:before{content:"\74"}.snf-wallet-full,.snf-details-quota{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-wallet-full:before,.snf-details-quota:before{content:"\78"}.snf-wallet-outline{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-wallet-outline:before{content:"\79"}.snf-keyboard{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-keyboard:before{content:"\7a"}.snf-book-2{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-book-2:before{content:"\42"}.snf-bell-1{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-bell-1:before{content:"\43"}.snf-bulb{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-bulb:before{content:"\46"}.snf-sun-1{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-sun-1:before{content:"\47"}.snf-moon-1{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-moon-1:before{content:"\76"}.snf-sun-2-full{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-sun-2-full:before{content:"\77"}.snf-sun-2-outline{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-sun-2-outline:before{content:"\6a"}.snf-moon-2-full:before{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-moon-2-full:before:before{content:"\44"}.snf-moon-2-outline{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-moon-2-outline:before{content:"\45"}.snf-sun-3{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-sun-3:before{content:"\41"}.snf-filter{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-filter:before{content:"\7b"}.snf-eye{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-eye:before{content:"\41"}.snf-radio-checked{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-radio-checked:before{content:"\42"}.snf-radio-unchecked{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-radio-unchecked:before{content:"\43"}.snf-close{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-close:before{content:"\44"}.snf-www{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-www:before{content:"\49"}.snf-arrow-up,.show-hide-all span.snf-font-arrow-up{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-arrow-up:before,.show-hide-all span.snf-font-arrow-up:before{content:"\4c"}.snf-arrow-down,.show-hide-all span.snf-font-arrow-down{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-arrow-down:before,.show-hide-all span.snf-font-arrow-down:before{content:"\4d"}.snf-checkbox-unchecked{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-checkbox-unchecked:before{content:"\61"}.snf-checkbox-checked{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-checkbox-checked:before{content:"\62"}.snf-cancel-circled{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-cancel-circled:before{content:"\63"}.snf-search{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-search:before{content:"\64"}.snf-twitter-logo{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-twitter-logo:before{content:"\67"}.snf-ok{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-ok:before{content:"\68"}.snf-switch{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-switch:before{content:"\69"}.snf-ban-circle{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-ban-circle:before{content:"\6a"}.snf-ok-sign{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-ok-sign:before{content:"\6c"}.snf-minus-sign{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-minus-sign:before{content:"\6e"}.snf-edit{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-edit:before{content:"\71"}.snf-listview{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-listview:before{content:"\73"}.snf-gridview{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-gridview:before{content:"\74"}.snf-dashboard-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-dashboard-outline:before{content:"\7a"}.snf-pithos-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-pithos-outline:before{content:"\79"}.snf-info-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-info-full:before{content:"\70"}.snf-volume-create-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-volume-create-full:before{content:"\36"}.snf-image-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-image-full:before{content:"\51"}.snf-pc-create-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-pc-create-full:before{content:"\53"}.snf-network-create-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-network-create-outline:before{content:"\54"}.snf-network-create-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-network-create-full:before{content:"\55"}.snf-ram-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-ram-outline:before{content:"\4a"}.snf-nic-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-nic-outline:before{content:"\50"}.snf-ram-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-ram-full:before{content:"\52"}.snf-nic-full,.snf-details-nic{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-nic-full:before,.snf-details-nic:before{content:"\72"}.snf-network-broken-1-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-network-broken-1-full:before{content:"\56"}.snf-network-broken-2-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-network-broken-2-full:before{content:"\57"}.snf-pc-broken-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-pc-broken-full:before{content:"\58"}.snf-pc-reboot-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-pc-reboot-full:before{content:"\59"}.snf-pc-switch-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-pc-switch-full:before{content:"\5a"}.snf-key-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-key-full:before{content:"\31"}.snf-router-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-router-full:before{content:"\32"}.snf-chip-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-chip-full:before{content:"\33"}.snf-plus-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-plus-full:before{content:"\34"}.snf-snapshot-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-snapshot-full:before{content:"\4e"}.snf-pithos-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-pithos-full:before{content:"\35"}.snf-volume-full,.snf-details-volume{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-volume-full:before,.snf-details-volume:before{content:"\4f"}.snf-network-full,.snf-details-network{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-network-full:before,.snf-details-network:before{content:"\4b"}.snf-pc-full,.snf-details-vm{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-pc-full:before,.snf-details-vm:before{content:"\78"}.snf-network-broken-1-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-network-broken-1-outline:before{content:"\37"}.snf-network-broken-2-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-network-broken-2-outline:before{content:"\38"}.snf-pc-broken-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-pc-broken-outline:before{content:"\39"}.snf-volume-broken-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-volume-broken-outline:before{content:"\30"}.snf-pc-reboot-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-pc-reboot-outline:before{content:"\21"}.snf-pc-switch-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-pc-switch-outline:before{content:"\40"}.snf-key-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-key-outline:before{content:"\23"}.snf-router-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-router-outline:before{content:"\48"}.snf-chip-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-chip-outline:before{content:"\45"}.snf-image-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-image-outline:before{content:"\66"}.snf-plus-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-plus-outline:before{content:"\6d"}.snf-snapshot-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-snapshot-outline:before{content:"\65"}.snf-volume-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-volume-outline:before{content:"\75"}.snf-network-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-network-outline:before{content:"\76"}.snf-pc-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-pc-outline:before{content:"\77"}.snf-info-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-info-outline:before{content:"\6f"}.snf-thunder-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-thunder-full:before{content:"\6b"}.snf-lock-closed-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-lock-closed-full:before{content:"\46"}.snf-lock-open-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-lock-open-full:before{content:"\47"}.snf-link-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-link-outline:before{content:"\26"}.snf-refresh-outline,body .custom-buttons .snf-font-reload{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-refresh-outline:before,body .custom-buttons .snf-font-reload:before{content:"\29"}.snf-download-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-download-full:before{content:"\25"}.snf-person-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-person-outline:before{content:"\2a"}.snf-upload-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-upload-full:before{content:"\28"}.snf-arrow-right-small-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-arrow-right-small-full:before{content:"\2d"}.snf-copy-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-copy-outline:before{content:"\3f"}.snf-copy-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-copy-full:before{content:"\22"}.snf-arrow-left-small-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-arrow-left-small-full:before{content:"\5f"}.snf-trash-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-trash-full:before{content:"\3d"}.snf-trash-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-trash-outline:before{content:"\24"}.main{margin:2em 0 5em}.main h4 .title{font-size:24px}.main span[class^="snf-details"]{float:left;margin-right:8px;font-size:35px}.main .lt{line-height:35px}.main .rt{padding-top:5px}.main .actions-per-item{padding:0}.object-anchor{height:2px}.object-details h4{font-size:14px;letter-spacing:1px}.object-details h4 .lt{display:block;float:left;max-width:60%;word-wrap:break-word}.object-details h4 .rt{padding-top:5px;display:block;overflow:hidden}.object-details h4 .arrow{position:relative;padding:0 8px}.object-details h4 .arrow:hover,.object-details h4 .arrow:focus{top:2px;cursor:pointer;outline:0 none}.object-details h4 .label{float:right;margin-left:15px;margin-bottom:10px}.object-details h4 .label.important{font-weight:bold}.object-details h4 em{float:none}.object-details h4 em.os-info{float:right;position:relative;bottom:3px}.object-details h4 em.os-info img{height:26px;margin-right:5px}.object-details h3{font-size:18px;margin:0 0 1em;font-weight:400;line-height:35px}.object-details h3 em{margin-left:10px;font-size:14px;display:inline-block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:50%;vertical-align:top}.object-details h3 span[class^="snf-details"]{float:left;margin-right:8px;font-size:25px;height:35px;line-height:35px}.object-details h3 .popover-dismiss{display:inline-block;width:18px;height:18px;background:#ccc;-webkit-border-radius:9px;-moz-border-radius:9px;border-radius:9px;text-align:center;font-weight:bold;vertical-align:middle;line-height:18px;font-size:16px;vertical-align:super;cursor:pointer;margin-left:10px;color:#fff}.object-details h3 .popover-dismiss:hover,.object-details h3 .popover-dismiss:focus{background:#b3b3b3;color:#eee}.object-details h3 .popover .popover-content{font-size:12px;line-height:130%}.object-details .icon-link{margin-right:10px}.object-details p{margin:10px 20px;font-style:italic}.object-details .length{margin-left:6px;border:0 none;font-style:italic}.object-details .length::before{content:'( '}.object-details .length::after{content:' )'}.object-details>.object-details{margin-left:-20px;margin-right:-20px;padding:12px 20px}.object-details-content .nav-tabs>li a{opacity:0.7}.object-details-content .nav-tabs>li.active>a{opacity:1}.object-details-content .nav-tabs>li:not(.active)>a:hover,.object-details-content .nav-tabs>li:not(.active)>a:focus{opacity:1}.tab-pane{overflow:auto}.parts-separator{border-top:2px solid #e0e0e0;padding-top:1em}.parts-separator h2{font-size:24px;margin-bottom:2em;padding-top:1em}.parts-separator h2 em{max-width:50%;display:inline;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:top}.part-two>.object-details{border-bottom:2px solid #e0e0e0;background:#ececec;padding:14px 20px;overflow-x:auto}.part-two>.object-details .object-details{padding:5px 20px}.part-two>.object-details .object-details:hover,.part-two>.object-details .object-details:focus{background:#e2e2e2}.part-two>.object-details .custom-btn span{padding:5px}.part-two .object-details-content{display:none;padding:0 35px}.show-hide-all span.snf-font-arrow-up{padding:0}.show-hide-all span.snf-font-arrow-down{padding:0}.filters-area{margin-bottom:40px;margin-left:140px}@media (max-width: 1200px){.filters-area{margin:0 10px 10px 0}}.filters-area.no-margin-left{margin-left:0}.filters-area a:focus,.filters-area input:focus{outline:none}.filters-area .badge{margin-left:6px;opacity:0.9;padding:2px 9px}.filters-area ul.nav a{padding-bottom:10px}.filter{height:30px;margin:0 10px 10px 0;display:inline-block;background:#ececec;border:1px solid #ccc}.filter .form-group{margin:0;height:30px}.filter label,.filter .dropdown{height:30px;line-height:30px;border:0 none;padding:0 10px;color:#222;background:transparent;font-weight:normal;margin:0}.filter label>a .selected-value,.filter .dropdown>a .selected-value{margin-left:4px}.filter label>a .arrow,.filter .dropdown>a .arrow{font-weight:bold}.filter label.open a,.filter .dropdown.open a{text-decoration:none;color:#222}.filter label a,.filter .dropdown a{color:#222}.filter .dropdown-menu,.filter .dropdown-list{background:#ececec;margin:0;width:auto}.filter .dropdown-menu>.active>a,.filter .dropdown-list>.active>a{background:#d3d3d3}.filter .dropdown-menu>li:hover>a,.filter .dropdown-list>li:hover>a{background:#dfdfdf;color:inherit}.filter .dropdown-menu a,.filter .dropdown-list a{padding-left:12px;padding-right:12px}.filter .dropdown-menu a span,.filter .dropdown-list a span{margin-right:6px}.filter input{border:0 none;background:transparent;height:30px;line-height:30px;padding:0 5px;font-weight:normal;color:#222}.filter .dropdown-list>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:1.42857;color:#303030;white-space:nowrap}.input-with-btn{border-width:0px;background-color:transparent;display:inline}@media screen and (min-width: 400px){.input-with-btn input{width:200px}}@media screen and (min-width: 600px){.input-with-btn input{width:300px}}@media screen and (min-width: 800px){.input-with-btn input{width:500px}}@media screen and (min-width: 1000px){.input-with-btn input{width:700px}}.input-with-btn .form-group{display:inline-block;background:#ececec;border:1px solid #ccc;margin-bottom:0.6em}.input-with-btn .filter-error{word-wrap:break-word}.input-with-btn .error-sign{display:block;opacity:0;position:static;display:inline-block;margin-right:6px;margin-left:10px;vertical-align:bottom}.input-with-btn .instructions{margin-top:0.6em}.input-with-btn .instructions *{color:#222}.input-with-btn .instructions .content-area{display:none;background:#e0e0e0;padding:12px 13px 18px}.input-with-btn .instructions .content-area dt{width:200px}.input-with-btn .instructions .content-area dd{margin-left:220px}.input-with-btn .instructions .clarifications{font-style:italic}.filter:not(.visible-filter):not(.visible-filter-fade){display:none;opacity:0}.visible-filter-fade{opacity:1;transition:opacity 0.5s}.filters .filters-list{border-radius:15px;background:#e0e0e0;border:1px solid #ccc;height:28px}.filters .filters-list>a{color:#222;line-height:28px;font-weight:bold;padding:8px 7px;background:transparent}.filters .filters-list .popover{padding:0}.filters .filters-list .popover-content{padding:0}.filters .filters-list .popover ul{list-style:none;padding:5px 0px;min-width:160px}.filters .filters-list .popover ul li{white-space:nowrap}.filters .filters-list .popover ul li a{color:#222}.filters .filters-list .popover ul li span{margin-right:10px}.filters .filters-list .popover ul .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.filters .filters-list .popover.bottom>.arrow:after{border-bottom-color:#ececec}p.progress-area{visibility:hidden}.in-progress .modal-body{background-color:#818181}.in-progress .modal-body p.progress-area{visibility:visible}.modal[data-item="user"]:not([data-type="contact"]) .table-selected td:nth-child(3){display:none}.modal#user-contact p{margin-top:18px;position:relative}.modal p{position:relative}.modal p>.error-sign{top:0}.modal h3{margin-top:0;font-weight:bold}.modal textarea{resize:vertical}.modal textarea,.modal input{width:87%;vertical-align:text-top;padding:4px 8px;border:1px solid #d9d9d9;color:#222}.modal textarea.body,.modal input.body{min-height:160px}.modal label{margin-right:6px;width:70px;vertical-align:sub}.modal .modal-body{background-color:white}.modal .modal-footer{margin-top:0}.modal .modal-footer form{display:inline}.modal .modal-footer .custom-btn:first-child{float:left;background-color:#303030;border-color:#303030}.modal .custom-btn{color:white;opacity:0.9}.modal .custom-btn:hover,.modal .custom-btn:focus{opacity:1}.modal[data-karma="dark"] .elem{color:#4e4e4e}.modal[data-karma="neutral"] .elem{color:#207dc9}.modal[data-karma="good"] .elem{color:#007238}.modal[data-karma="bad"] .elem{color:#a66b18}.modal[data-caution="warning"][data-karma="good"] .elem,.modal[data-caution="warning"][data-karma="neutral"] .elem{color:#a66b18}.modal[data-caution="dangerous"][data-karma="bad"] .elem,.modal[data-caution="dangerous"][data-karma="neutral"] .elem{color:#c21934}.custom-btn[data-karma="dark"]{background-color:#222222;border-color:transparent}.modal em{font-weight:bold;font-style:normal}.modal .popover{z-index:2000}.modal .popover dl{color:black;font-weight:normal}.modal .popover dl dt{width:90px}.modal .popover dl dd{margin-left:110px}.modal .popover h2{font-size:16px;color:#303030;font-weight:bold;text-align:center}.modal .popover-content{min-width:150px}.modal-content{padding:20px;color:#303030}.modal-content .badge{background-color:transparent}.instructions-icon{color:#3c96e0;font-size:22px;margin-left:78px}.instructions-icon:hover{text-decoration:none}.extra-info{margin-top:10px}.error-sign{color:red;font-size:20px;margin-left:10px;position:absolute;top:6px;display:none}.error-sign:hover,.error-sign:focus{color:red;text-decoration:none}.form-area{position:relative}.form-subject{margin-bottom:15px}.toggle-more{margin-top:-16px;display:none}.modal .table-selected th,.modal .table-selected td{word-break:break-word}.modal .table-selected td:last-child .wrap{padding-right:36px}.modal .table-selected tr:nth-child(2n){background:#f2f2f2}.modal .table-selected tr a{font-weight:bold}.modal .table-selected tr:hover,.modal .table-selected tr:focus{background:#d9d9d9}.modal .table-selected tr:hover a,.modal .table-selected tr:focus a{color:red}.modal .table-selected .remove{position:absolute;right:14px;color:transparent}.modal .table-selected .remove:hover{cursor:pointer;text-decoration:none}table thead th{white-space:nowrap}table td,table th{vertical-align:top}table .wrap{position:relative}.table-items .snf-search{opacity:0.7;font-size:15px}.table-items .snf-search:hover,.table-items .snf-search:focus{opacity:1}.table-items .login-method{padding:2px 16px 2px 0px;text-align:center}.table-items th .badge{margin:0 2px 0 4px;display:inline;padding-top:2px}.table-items td{padding:8px 6px 0 6px}.table-selected-main:not(.table-selected) td:last-child,.table-items:not(.table-selected) td:last-child{max-width:60px;min-width:60px;padding:8px 5px}.table-selected-main:not(.table-selected) td:last-child .details-link:hover,.table-items:not(.table-selected) td:last-child .details-link:hover{text-decoration:none}.table-selected-main:not(.table-selected) td:last-child .summary-expand,.table-items:not(.table-selected) td:last-child .summary-expand{position:relative;z-index:10;float:right;padding-left:8px;padding-right:8px;background-color:#005b9a;color:#fff}.table-selected-main:not(.table-selected) td:last-child .summary-expand:hover,.table-selected-main:not(.table-selected) td:last-child .summary-expand:focus,.table-items:not(.table-selected) td:last-child .summary-expand:hover,.table-items:not(.table-selected) td:last-child .summary-expand:focus{text-decoration:none;background-color:#ee5161}.table-selected-main:not(.table-selected) td:last-child dl,.table-items:not(.table-selected) td:last-child dl{z-index:0;position:relative;padding:8px;display:none;margin:0}.table-items .headerSortUp span.caret{border-top:0;border-bottom:4px solid}#table-items-selected_filter label,#table-items-total_filter label{color:#222}#table-items-selected_filter input,#table-items-total_filter input{color:#222;background:#ececec;border:1px solid #ccc;padding:3px 5px}#table-items-selected_filter input:focus,#table-items-total_filter input:focus{outline:0 none}#table-items-selected_wrapper{padding:10px;border:1px solid #e0e0e0;margin-bottom:20px;display:none}div.dataTables_length{padding-left:2em;padding-top:0.55em}div.dataTables_length select{width:55px;display:inline-block;margin-left:4px;vertical-align:baseline;color:#222}table.dataTable tbody tr{background-color:inherit}table.dataTable tbody tr.even{background-color:#ececec}table.dataTable thead th,table.dataTable thead td{border-bottom:1px solid white;border-top:1px solid #e0e0e0}table.dataTable tbody tr:hover{background-color:#e0e0e0}table.dataTable tbody tr.selected{color:#222;background-color:#ccc}html body .dataTables_wrapper label{font-weight:normal}html body .dataTables_wrapper table th.sorting,html body .dataTables_wrapper table th.sorting_asc,html body .dataTables_wrapper table th.sorting_desc{background-position:center left;padding-left:22px}.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_paginate{padding-top:0;margin-bottom:0.5em;color:#222;line-height:35px}table.dataTable.no-footer{border-bottom:1px solid #eee;margin:2em 0}.dataTables_wrapper .dataTables_paginate .paginate_button{color:#222 !important;padding:0 1em}.container .dataTables_wrapper .dataTables_paginate .paginate_button:hover,.container .dataTables_wrapper .dataTables_paginate .paginate_button:focus{background:transparent;border-color:#222;color:#222 !important}.container .dataTables_wrapper .dataTables_paginate .paginate_button.disabled{border-color:transparent;color:#818181 !important}.container .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover,.container .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:focus,.container .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active{color:#818181 !important}.dataTables_wrapper .dataTables_paginate .paginate_button.current,.dataTables_wrapper .dataTables_paginate .paginate_button.current:focus,.dataTables_wrapper .dataTables_paginate .paginate_button.current:hover{background:#ccc;color:#222 !important;border:transparent}.dataTables_wrapper>.custom-buttons{margin-bottom:1em;width:100%}.dataTables_wrapper .dataTables_processing{background:#ffa914;color:#fff;padding:5px 10px;-webkit-box-shadow:inset 0 0 5px #888;box-shadow:inset 0 0 5px #888;z-index:1}.fixed{position:fixed}.ip_log tr td:nth-child(2),.ip_log tr th:nth-child(2){word-break:break-word;max-width:250px}.ip_log tr td:nth-child(3),.ip_log tr th:nth-child(3){word-break:break-word;max-width:150px}.ip_log tr td:nth-child(4),.ip_log tr th:nth-child(4){word-break:break-word;max-width:150px}html,body{height:100%}body{padding-top:100px}.wrapper{padding-bottom:50px}.container:not(.container-solid){max-width:960px}h1,h2,h3,h4{word-wrap:break-word}.info{overflow:auto}.dl-horizontal dd,dt,.tooltip-inner{word-wrap:break-word}.disabled{cursor:default !important}.app-list{position:relative;text-align:center;padding-top:100px}.app-list a{width:210px;font-size:24px;margin:0 20px;display:inline-block;text-align:center;opacity:1;border:1px solid #222;color:#222;opacity:1}.app-list a span{display:inline-block;height:100%;line-height:100%;padding:12px 10px}.app-list a:hover,.app-list a:focus{text-decoration:none;opacity:0.85}.app-list a .snf-font-remove{display:inline}.app-list a.disabled{background:transparent !important;border-color:#818181 !important;color:#818181 !important}.app-list a.disabled:hover,.app-list a.disabled:focus{cursor:default;opacity:1}.app-list a.disabled:hover span,.app-list a.disabled:focus span{color:#818181 !important}.app-list a span{border:1px solid transparent;width:100%}.app-list a:hover span,.app-list a:focus span{border-color:#222}.app-list a.disabled{@inlcude disabled;;color:#818181}.app-list a.disabled:hover span,.app-list a.disabled:focus span{border-color:transparent}.app-list a.disabled{border-color:#a7a7a7;color:gray}.app-list a.disabled:hover span,.app-list a.disabled:focus span{border-color:transparent}.nav-simple{padding:20px;border-bottom:1px solid #222}.nav-simple .header{float:left;line-height:40px;font-size:26px}.nav-simple .header img{max-height:50px}.nav-simple .login-info{float:right;position:relative;line-height:40px;font-size:16px}.nav-simple .login-info .has-dropdown{display:inline;position:relative}.nav-simple .login-info .has-dropdown:hover>a,.nav-simple .login-info .has-dropdown:focus>a{background:#fefefe}.nav-simple .login-info .has-dropdown>a{color:#222;display:inline-block;padding:0 10px}.nav-simple .login-info .dropdown-menu{left:auto;right:0;top:27px}.navbar-default{border:0 none;border-bottom:1px solid #e0e0e0;z-index:1040;margin:0 auto}.navbar-default .container-fluid{padding:0}.navbar-default .home-icon{padding:0;height:50px;width:50px;text-align:center;line-height:50px;font-size:2px;background:#00a551}.navbar-default .home-icon img{max-height:50px}.sub-nav{top:50px;min-height:inherit}.sub-nav .nav>li>a{padding-top:8px;padding-bottom:8px}@media (max-width: 768px){.sub-nav{display:none}}.dropdown-menu{overflow-y:auto}.nav .has-dropdown:hover>ul.dropdown-menu,.nav-simple .has-dropdown:hover>ul.dropdown-menu{display:block}svg>text:last-child{display:none}.has-dropdown .arrow{margin-left:6px;vertical-align:middle}.hidden-row{display:none}.with-shift *::selection{background-color:transparent}.with-shift *::-moz-selection{background:transparent}.tab-content{background:#d9d9d9;color:#222;padding:20px;border:0 none}.tab-content .well{margin-bottom:0}.selection-indicator{cursor:pointer;padding:6px 12px 6px 6px}.notify{padding:30px 10px 15px;width:100%;position:fixed;bottom:0;background:#444;color:#fff}.notify .container>*:not(:last-child){margin-bottom:16px}.notify .remove-icon{color:transparent;margin-left:20px;font-weight:bold}.notify .container>*:hover .remove-icon{color:#d9534f}.notify .state-icon{margin-right:10px}.notify .success{color:#449d44}.notify .error{color:#d9534f}.notify .pending{color:#f0ad4e}.notify .warning,.notify .no-notifications{font-style:italic;font-weight:bold;display:inline-block;text-align:right}.notify .warning>.wrap,.notify .no-notifications>.wrap{display:block;padding-right:4px}.notify .warning a:hover,.notify .no-notifications a:hover{cursor:pointer}.notify .close-notify{position:absolute;right:20px;top:20px;color:#fff}.notify .close-notify:hover,.notify .close-notify:focus{color:inherit}.notify .dl-horizontal{margin-left:21px}.notify .dl-horizontal dt{width:80px;vertical-align:top;text-align:left}.notify .dl-horizontal dt span{font-size:20px;vertical-align:text-bottom;margin-right:10px}.notify .dl-horizontal dd{margin-left:80px}.lowercase{text-transform:lowercase}.shortcuts-btn .book-icon{padding-right:2px;vertical-align:sub;font-size:17px}body .shortcuts dt{width:119px;margin-bottom:12px}body .shortcuts dd{margin-left:139px}body .shortcuts .key{padding:2px 9px;font-style:normal;font-weight:bold;border:1px solid #ddd;background:#f5f5f5;border-radius:6px}.filters-examples dt{font-weight:normal;margin-bottom:0}.filters-examples dd{margin-bottom:12px}.filters-examples dd .highlight{background:#f5f5f5;padding:2px 6px;border-bottom:1px solid #ddd}.filters-examples dd.divider{margin-bottom:8px;border-bottom:1px solid #ddd}.notes dt{width:50px}.notes dd{margin-left:60px}.popover{z-index:1999;max-width:none;color:#222;margin-bottom:20px}.popover h2{text-align:center;font-size:1.3em;font-weight:bold;margin-top:0}.popover h3{font-size:1.2em;font-weight:bold}.popover h4{font-size:1.1em;font-weight:bold}.popover dt{margin-bottom:8px;overflow:visible}.popover .panel-default{border-color:transparent;box-shadow:none}.sign-out{text-align:right}.sign-out span{margin-right:10px;vertical-align:middle;font-size:18px}.stats section{margin-bottom:3em}.stats section h3{margin-bottom:1em}.stats section h3 span{margin-right:0.5em}.stats section .custom-btn{float:left;margin-right:32px}.stats section .custom-btn span{padding-left:0}.stats section .custom-btn .snf-download-full{padding-right:0;padding-left:8px}.stats section .spinner{display:none;float:left;padding:8px}.navbar-right .dropdown-menu,.login-info .dropdown-menu{min-width:0}@media (min-width: 1200px){.stick{position:fixed;top:100px;width:inherit}}.themes{position:fixed;left:10px;bottom:10px}.charts .info{overflow:hidden}.charts h3{text-align:center;margin-bottom:1em}.charts .c3-axis{fill:#222}.charts .c3 path,.charts .c3 line{stroke:#222}.charts .c3-legend-item text{fill:#222}.charts .c3-tooltip{color:#222}.popover-content{max-width:800px} diff --git a/snf-admin-app/synnefo_admin/admin/static/min-css/main.css b/snf-admin-app/synnefo_admin/admin/static/min-css/main.css new file mode 100644 index 0000000000000000000000000000000000000000..db97a1c929118ad1f9c193d9384449410d5b2e51 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/min-css/main.css @@ -0,0 +1 @@ +/*! normalize.css v3.0.0 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:0.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace, monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}@media print{*{text-shadow:none !important;color:#000 !important;background:transparent !important;box-shadow:none !important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff !important}.navbar{display:none}.table td,.table th{background-color:#fff !important}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000 !important}.label{border:1px solid #000}.table{border-collapse:collapse !important}.table-bordered th,.table-bordered td{border:1px solid #ddd !important}}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:62.5%;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Open Sans",sans-serif;font-size:14px;line-height:1.42857;color:#fff;background-color:#303030}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit;color:#fff}a{color:#4d99d8;text-decoration:none}a:hover,a:focus{color:#83b8e4}a:focus{outline:0 none}figure{margin:0}img{vertical-align:middle}.img-responsive{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857;background-color:#303030;border:1px solid #ddd;border-radius:0;-webkit-transition:all 0.2s ease-in-out;transition:all 0.2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #d9d9d9}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1 small,h1 .small,h2 small,h2 .small,h3 small,h3 .small,h4 small,h4 .small,h5 small,h5 .small,h6 small,h6 .small,.h1 small,.h1 .small,.h2 small,.h2 .small,.h3 small,.h3 .small,.h4 small,.h4 .small,.h5 small,.h5 .small,.h6 small,.h6 .small{font-weight:normal;line-height:1;color:#4e4e4e}h1,.h1,h2,.h2,h3,.h3{margin-top:20px;margin-bottom:10px}h1 small,h1 .small,.h1 small,.h1 .small,h2 small,h2 .small,.h2 small,.h2 .small,h3 small,h3 .small,.h3 small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10px;margin-bottom:10px}h4 small,h4 .small,.h4 small,.h4 .small,h5 small,h5 .small,.h5 small,.h5 .small,h6 small,h6 .small,.h6 small,.h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:200;line-height:1.4}@media (min-width: 768px){.lead{font-size:21px}}small,.small{font-size:85%}cite{font-style:normal}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-muted{color:#4e4e4e}.text-primary{color:#fff}a.text-primary:hover{color:#e6e6e6}.text-success{color:#3c763d}a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:hover{color:#843534}.bg-primary{color:#fff}.bg-primary{background-color:#fff}a.bg-primary:hover{background-color:#e6e6e6}.bg-success{background-color:#dff0d8}a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #d9d9d9}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ul ol,ol ul,ol ol{margin-bottom:0}.list-unstyled,.list-inline{padding-left:0;list-style:none}.list-inline{margin-left:-5px}.list-inline>li{display:inline-block;padding-left:5px;padding-right:5px}dl{margin-top:0;margin-bottom:0}dt,dd{line-height:1.42857}dt{font-weight:bold}dd{margin-left:0}@media (min-width: 768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}.dl-horizontal dd:before,.dl-horizontal dd:after{content:" ";display:table}.dl-horizontal dd:after{clear:both}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #4e4e4e}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #d9d9d9}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.42857;color:#4e4e4e}blockquote footer:before,blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #d9d9d9;border-left:0;text-align:right}.blockquote-reverse footer:before,.blockquote-reverse small:before,.blockquote-reverse .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before,blockquote.pull-right .small:before{content:''}.blockquote-reverse footer:after,.blockquote-reverse small:after,.blockquote-reverse .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}blockquote:before,blockquote:after{content:""}address{margin-bottom:20px;font-style:normal;line-height:1.42857}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;white-space:nowrap;border-radius:0}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;box-shadow:inset 0 -1px 0 rgba(0,0,0,0.25)}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857;word-break:break-all;word-wrap:break-word;color:#303030;background-color:#f5f5f5;border:1px solid #ccc;border-radius:0}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.container:before,.container:after{content:" ";display:table}.container:after{clear:both}@media (min-width: 768px){.container{width:810px}}@media (min-width: 992px){.container{width:1010px}}@media (min-width: 1200px){.container{width:1170px}}.container-fluid{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.container-fluid:before,.container-fluid:after{content:" ";display:table}.container-fluid:after{clear:both}.row{margin-left:-15px;margin-right:-15px}.row:before,.row:after{content:" ";display:table}.row:after{clear:both}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-1{width:8.33333%}.col-xs-2{width:16.66667%}.col-xs-3{width:25%}.col-xs-4{width:33.33333%}.col-xs-5{width:41.66667%}.col-xs-6{width:50%}.col-xs-7{width:58.33333%}.col-xs-8{width:66.66667%}.col-xs-9{width:75%}.col-xs-10{width:83.33333%}.col-xs-11{width:91.66667%}.col-xs-12{width:100%}.col-xs-pull-0{right:0%}.col-xs-pull-1{right:8.33333%}.col-xs-pull-2{right:16.66667%}.col-xs-pull-3{right:25%}.col-xs-pull-4{right:33.33333%}.col-xs-pull-5{right:41.66667%}.col-xs-pull-6{right:50%}.col-xs-pull-7{right:58.33333%}.col-xs-pull-8{right:66.66667%}.col-xs-pull-9{right:75%}.col-xs-pull-10{right:83.33333%}.col-xs-pull-11{right:91.66667%}.col-xs-pull-12{right:100%}.col-xs-push-0{left:0%}.col-xs-push-1{left:8.33333%}.col-xs-push-2{left:16.66667%}.col-xs-push-3{left:25%}.col-xs-push-4{left:33.33333%}.col-xs-push-5{left:41.66667%}.col-xs-push-6{left:50%}.col-xs-push-7{left:58.33333%}.col-xs-push-8{left:66.66667%}.col-xs-push-9{left:75%}.col-xs-push-10{left:83.33333%}.col-xs-push-11{left:91.66667%}.col-xs-push-12{left:100%}.col-xs-offset-0{margin-left:0%}.col-xs-offset-1{margin-left:8.33333%}.col-xs-offset-2{margin-left:16.66667%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-4{margin-left:33.33333%}.col-xs-offset-5{margin-left:41.66667%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-7{margin-left:58.33333%}.col-xs-offset-8{margin-left:66.66667%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-10{margin-left:83.33333%}.col-xs-offset-11{margin-left:91.66667%}.col-xs-offset-12{margin-left:100%}@media (min-width: 768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-1{width:8.33333%}.col-sm-2{width:16.66667%}.col-sm-3{width:25%}.col-sm-4{width:33.33333%}.col-sm-5{width:41.66667%}.col-sm-6{width:50%}.col-sm-7{width:58.33333%}.col-sm-8{width:66.66667%}.col-sm-9{width:75%}.col-sm-10{width:83.33333%}.col-sm-11{width:91.66667%}.col-sm-12{width:100%}.col-sm-pull-0{right:0%}.col-sm-pull-1{right:8.33333%}.col-sm-pull-2{right:16.66667%}.col-sm-pull-3{right:25%}.col-sm-pull-4{right:33.33333%}.col-sm-pull-5{right:41.66667%}.col-sm-pull-6{right:50%}.col-sm-pull-7{right:58.33333%}.col-sm-pull-8{right:66.66667%}.col-sm-pull-9{right:75%}.col-sm-pull-10{right:83.33333%}.col-sm-pull-11{right:91.66667%}.col-sm-pull-12{right:100%}.col-sm-push-0{left:0%}.col-sm-push-1{left:8.33333%}.col-sm-push-2{left:16.66667%}.col-sm-push-3{left:25%}.col-sm-push-4{left:33.33333%}.col-sm-push-5{left:41.66667%}.col-sm-push-6{left:50%}.col-sm-push-7{left:58.33333%}.col-sm-push-8{left:66.66667%}.col-sm-push-9{left:75%}.col-sm-push-10{left:83.33333%}.col-sm-push-11{left:91.66667%}.col-sm-push-12{left:100%}.col-sm-offset-0{margin-left:0%}.col-sm-offset-1{margin-left:8.33333%}.col-sm-offset-2{margin-left:16.66667%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-4{margin-left:33.33333%}.col-sm-offset-5{margin-left:41.66667%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-7{margin-left:58.33333%}.col-sm-offset-8{margin-left:66.66667%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-10{margin-left:83.33333%}.col-sm-offset-11{margin-left:91.66667%}.col-sm-offset-12{margin-left:100%}}@media (min-width: 992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-1{width:8.33333%}.col-md-2{width:16.66667%}.col-md-3{width:25%}.col-md-4{width:33.33333%}.col-md-5{width:41.66667%}.col-md-6{width:50%}.col-md-7{width:58.33333%}.col-md-8{width:66.66667%}.col-md-9{width:75%}.col-md-10{width:83.33333%}.col-md-11{width:91.66667%}.col-md-12{width:100%}.col-md-pull-0{right:0%}.col-md-pull-1{right:8.33333%}.col-md-pull-2{right:16.66667%}.col-md-pull-3{right:25%}.col-md-pull-4{right:33.33333%}.col-md-pull-5{right:41.66667%}.col-md-pull-6{right:50%}.col-md-pull-7{right:58.33333%}.col-md-pull-8{right:66.66667%}.col-md-pull-9{right:75%}.col-md-pull-10{right:83.33333%}.col-md-pull-11{right:91.66667%}.col-md-pull-12{right:100%}.col-md-push-0{left:0%}.col-md-push-1{left:8.33333%}.col-md-push-2{left:16.66667%}.col-md-push-3{left:25%}.col-md-push-4{left:33.33333%}.col-md-push-5{left:41.66667%}.col-md-push-6{left:50%}.col-md-push-7{left:58.33333%}.col-md-push-8{left:66.66667%}.col-md-push-9{left:75%}.col-md-push-10{left:83.33333%}.col-md-push-11{left:91.66667%}.col-md-push-12{left:100%}.col-md-offset-0{margin-left:0%}.col-md-offset-1{margin-left:8.33333%}.col-md-offset-2{margin-left:16.66667%}.col-md-offset-3{margin-left:25%}.col-md-offset-4{margin-left:33.33333%}.col-md-offset-5{margin-left:41.66667%}.col-md-offset-6{margin-left:50%}.col-md-offset-7{margin-left:58.33333%}.col-md-offset-8{margin-left:66.66667%}.col-md-offset-9{margin-left:75%}.col-md-offset-10{margin-left:83.33333%}.col-md-offset-11{margin-left:91.66667%}.col-md-offset-12{margin-left:100%}}@media (min-width: 1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-1{width:8.33333%}.col-lg-2{width:16.66667%}.col-lg-3{width:25%}.col-lg-4{width:33.33333%}.col-lg-5{width:41.66667%}.col-lg-6{width:50%}.col-lg-7{width:58.33333%}.col-lg-8{width:66.66667%}.col-lg-9{width:75%}.col-lg-10{width:83.33333%}.col-lg-11{width:91.66667%}.col-lg-12{width:100%}.col-lg-pull-0{right:0%}.col-lg-pull-1{right:8.33333%}.col-lg-pull-2{right:16.66667%}.col-lg-pull-3{right:25%}.col-lg-pull-4{right:33.33333%}.col-lg-pull-5{right:41.66667%}.col-lg-pull-6{right:50%}.col-lg-pull-7{right:58.33333%}.col-lg-pull-8{right:66.66667%}.col-lg-pull-9{right:75%}.col-lg-pull-10{right:83.33333%}.col-lg-pull-11{right:91.66667%}.col-lg-pull-12{right:100%}.col-lg-push-0{left:0%}.col-lg-push-1{left:8.33333%}.col-lg-push-2{left:16.66667%}.col-lg-push-3{left:25%}.col-lg-push-4{left:33.33333%}.col-lg-push-5{left:41.66667%}.col-lg-push-6{left:50%}.col-lg-push-7{left:58.33333%}.col-lg-push-8{left:66.66667%}.col-lg-push-9{left:75%}.col-lg-push-10{left:83.33333%}.col-lg-push-11{left:91.66667%}.col-lg-push-12{left:100%}.col-lg-offset-0{margin-left:0%}.col-lg-offset-1{margin-left:8.33333%}.col-lg-offset-2{margin-left:16.66667%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-4{margin-left:33.33333%}.col-lg-offset-5{margin-left:41.66667%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-7{margin-left:58.33333%}.col-lg-offset-8{margin-left:66.66667%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-10{margin-left:83.33333%}.col-lg-offset-11{margin-left:91.66667%}.col-lg-offset-12{margin-left:100%}}table{max-width:100%;background-color:transparent}th{text-align:left}.table{width:100%;margin-bottom:20px}.table>thead>tr>th,.table>thead>tr>td,.table>tbody>tr>th,.table>tbody>tr>td,.table>tfoot>tr>th,.table>tfoot>tr>td{padding:10px;line-height:1.42857;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>th,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#303030}.table-condensed>thead>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-child(odd)>td,.table-striped>tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover>tbody>tr:hover>td,.table-hover>tbody>tr:hover>th{background-color:#f5f5f5}table col[class*="col-"]{position:static;float:none;display:table-column}table td[class*="col-"],table th[class*="col-"]{position:static;float:none;display:table-cell}.table>thead>tr>td.active,.table>thead>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th{background-color:#e8e8e8}.table>thead>tr>td.success,.table>thead>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th{background-color:#dff0d8}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th{background-color:#d0e9c6}.table>thead>tr>td.info,.table>thead>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th{background-color:#d9edf7}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th{background-color:#c4e3f3}.table>thead>tr>td.warning,.table>thead>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th{background-color:#faf2cc}.table>thead>tr>td.danger,.table>thead>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th{background-color:#f2dede}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th{background-color:#ebcccc}@media (max-width: 767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;overflow-x:scroll;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd;-webkit-overflow-scrolling:touch}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{padding:0;margin:0;border:0;min-width:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#303030;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;margin-bottom:5px;font-weight:bold}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type="file"]{display:block}input[type="range"]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:0 none}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out 0.15s,box-shadow ease-in-out 0.15s;transition:border-color ease-in-out 0.15s,box-shadow ease-in-out 0.15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6)}.form-control::-moz-placeholder{color:#4e4e4e;opacity:1}.form-control:-ms-input-placeholder{color:#4e4e4e}.form-control::-webkit-input-placeholder{color:#4e4e4e}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#d9d9d9;opacity:1}textarea.form-control{height:auto}input[type="search"]{-webkit-appearance:none}input[type="date"]{line-height:34px}.form-group{margin-bottom:15px}.radio,.checkbox{display:block;min-height:20px;margin-top:10px;margin-bottom:10px;padding-left:20px}.radio label,.checkbox label{display:inline;font-weight:normal;cursor:pointer}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{float:left;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:normal;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type="radio"][disabled],fieldset[disabled] input[type="radio"],input[type="checkbox"][disabled],fieldset[disabled] input[type="checkbox"],.radio[disabled],fieldset[disabled] .radio,.radio-inline[disabled],fieldset[disabled] .radio-inline,.checkbox[disabled],fieldset[disabled] .checkbox,.checkbox-inline[disabled],fieldset[disabled] .checkbox-inline{cursor:not-allowed}.input-sm,.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm,.input-group-sm>select.form-control,.input-group-sm>select.input-group-addon,.input-group-sm>.input-group-btn>select.btn{height:30px;line-height:30px}textarea.input-sm,.input-group-sm>textarea.form-control,.input-group-sm>textarea.input-group-addon,.input-group-sm>.input-group-btn>textarea.btn,select[multiple].input-sm,.input-group-sm>select[multiple].form-control,.input-group-sm>select[multiple].input-group-addon,.input-group-sm>.input-group-btn>select[multiple].btn{height:auto}.input-lg,.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-lg,.input-group-lg>select.form-control,.input-group-lg>select.input-group-addon,.input-group-lg>.input-group-btn>select.btn{height:46px;line-height:46px}textarea.input-lg,.input-group-lg>textarea.form-control,.input-group-lg>textarea.input-group-addon,.input-group-lg>.input-group-btn>textarea.btn,select[multiple].input-lg,.input-group-lg>select[multiple].form-control,.input-group-lg>select[multiple].input-group-addon,.input-group-lg>.input-group-btn>select[multiple].btn{height:auto}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.has-feedback .form-control-feedback{position:absolute;top:25px;right:0;display:block;width:34px;height:34px;line-height:34px;text-align:center}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;border-color:#3c763d;background-color:#dff0d8}.has-success .form-control-feedback{color:#3c763d}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;border-color:#8a6d3b;background-color:#fcf8e3}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;border-color:#a94442;background-color:#f2dede}.has-error .form-control-feedback{color:#a94442}.form-control-static{margin-bottom:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#fff}@media (min-width: 768px){.form-inline .form-group,.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control,.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .input-group>.form-control,.navbar-form .input-group>.form-control{width:100%}.form-inline .control-label,.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.navbar-form .radio,.form-inline .checkbox,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;padding-left:0;vertical-align:middle}.form-inline .radio input[type="radio"],.navbar-form .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"],.navbar-form .checkbox input[type="checkbox"]{float:none;margin-left:0}.form-inline .has-feedback .form-control-feedback,.navbar-form .has-feedback .form-control-feedback{top:0}}.form-horizontal .control-label,.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{margin-top:0;margin-bottom:0;padding-top:7px}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-left:-15px;margin-right:-15px}.form-horizontal .form-group:before,.form-horizontal .form-group:after{content:" ";display:table}.form-horizontal .form-group:after{clear:both}.form-horizontal .form-control-static{padding-top:7px}@media (min-width: 768px){.form-horizontal .control-label{text-align:right}}.form-horizontal .has-feedback .form-control-feedback{top:0;right:15px}.btn{display:inline-block;margin-bottom:0;font-weight:normal;text-align:center;vertical-align:middle;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;padding:6px 12px;font-size:14px;line-height:1.42857;border-radius:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus{outline:0 none}.btn:hover,.btn:focus{color:#333;text-decoration:none}.btn:active,.btn.active{outline:0;background-image:none;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;pointer-events:none;opacity:0.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active{color:#333;background-color:#ebebeb;border-color:#adadad}.open .btn-default.dropdown-toggle{color:#333;background-color:#ebebeb;border-color:#adadad}.btn-default:active,.btn-default.active{background-image:none}.open .btn-default.dropdown-toggle{background-image:none}.btn-default.disabled,.btn-default.disabled:hover,.btn-default.disabled:focus,.btn-default.disabled:active,.btn-default.disabled.active,.btn-default[disabled],.btn-default[disabled]:hover,.btn-default[disabled]:focus,.btn-default[disabled]:active,.btn-default[disabled].active,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default:hover,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#fff;border-color:#f2f2f2}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active{color:#fff;background-color:#ebebeb;border-color:#d4d4d4}.open .btn-primary.dropdown-toggle{color:#fff;background-color:#ebebeb;border-color:#d4d4d4}.btn-primary:active,.btn-primary.active{background-image:none}.open .btn-primary.dropdown-toggle{background-image:none}.btn-primary.disabled,.btn-primary.disabled:hover,.btn-primary.disabled:focus,.btn-primary.disabled:active,.btn-primary.disabled.active,.btn-primary[disabled],.btn-primary[disabled]:hover,.btn-primary[disabled]:focus,.btn-primary[disabled]:active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary:hover,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary.active{background-color:#fff;border-color:#f2f2f2}.btn-primary .badge{color:#fff;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active{color:#fff;background-color:#47a447;border-color:#398439}.open .btn-success.dropdown-toggle{color:#fff;background-color:#47a447;border-color:#398439}.btn-success:active,.btn-success.active{background-image:none}.open .btn-success.dropdown-toggle{background-image:none}.btn-success.disabled,.btn-success.disabled:hover,.btn-success.disabled:focus,.btn-success.disabled:active,.btn-success.disabled.active,.btn-success[disabled],.btn-success[disabled]:hover,.btn-success[disabled]:focus,.btn-success[disabled]:active,.btn-success[disabled].active,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success:hover,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active{color:#fff;background-color:#39b3d7;border-color:#269abc}.open .btn-info.dropdown-toggle{color:#fff;background-color:#39b3d7;border-color:#269abc}.btn-info:active,.btn-info.active{background-image:none}.open .btn-info.dropdown-toggle{background-image:none}.btn-info.disabled,.btn-info.disabled:hover,.btn-info.disabled:focus,.btn-info.disabled:active,.btn-info.disabled.active,.btn-info[disabled],.btn-info[disabled]:hover,.btn-info[disabled]:focus,.btn-info[disabled]:active,.btn-info[disabled].active,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info:hover,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active{color:#fff;background-color:#ed9c28;border-color:#d58512}.open .btn-warning.dropdown-toggle{color:#fff;background-color:#ed9c28;border-color:#d58512}.btn-warning:active,.btn-warning.active{background-image:none}.open .btn-warning.dropdown-toggle{background-image:none}.btn-warning.disabled,.btn-warning.disabled:hover,.btn-warning.disabled:focus,.btn-warning.disabled:active,.btn-warning.disabled.active,.btn-warning[disabled],.btn-warning[disabled]:hover,.btn-warning[disabled]:focus,.btn-warning[disabled]:active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning:hover,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active{color:#fff;background-color:#d2322d;border-color:#ac2925}.open .btn-danger.dropdown-toggle{color:#fff;background-color:#d2322d;border-color:#ac2925}.btn-danger:active,.btn-danger.active{background-image:none}.open .btn-danger.dropdown-toggle{background-image:none}.btn-danger.disabled,.btn-danger.disabled:hover,.btn-danger.disabled:focus,.btn-danger.disabled:active,.btn-danger.disabled.active,.btn-danger[disabled],.btn-danger[disabled]:hover,.btn-danger[disabled]:focus,.btn-danger[disabled]:active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger:hover,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{color:#4d99d8;font-weight:normal;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#83b8e4;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:hover,fieldset[disabled] .btn-link:focus{color:#818181;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-sm,.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%;padding-left:0;padding-right:0}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity 0.15s linear;transition:opacity 0.15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height 0.35s ease;transition:height 0.35s ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;font-size:14px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);border-radius:0;-webkit-box-shadow:0 6px 12px rgba(0,0,0,0.175);box-shadow:0 6px 12px rgba(0,0,0,0.175);background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:1.42857;color:#303030;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{text-decoration:none;color:#303030;background-color:#d9d9d9}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;outline:0;background-color:#ee5161}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#4e4e4e}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);cursor:not-allowed}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{left:auto;right:0}.dropdown-menu-left{left:0;right:auto}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857;color:#4e4e4e}.dropdown-backdrop{position:fixed;left:0;right:0;bottom:0;top:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}@media (min-width: 768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{left:0;right:auto}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn:hover,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn.active{z-index:2}.btn-group>.btn:focus,.btn-group-vertical>.btn:focus{outline:none}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar:before,.btn-toolbar:after{content:" ";display:table}.btn-toolbar:after{clear:both}.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px}.btn-group>.btn-lg+.dropdown-toggle,.btn-group-lg.btn-group>.btn+.dropdown-toggle{padding-left:12px;padding-right:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret,.btn-group-lg>.btn .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret,.dropup .btn-group-lg>.btn .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after{content:" ";display:table}.btn-group-vertical>.btn-group:after{clear:both}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-bottom-left-radius:0;border-top-right-radius:0;border-top-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{float:none;display:table-cell;width:1%}.btn-group-justified>.btn-group .btn{width:100%}[data-toggle="buttons"]>.btn>input[type="radio"],[data-toggle="buttons"]>.btn>input[type="checkbox"]{display:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*="col-"]{float:none;padding-left:0;padding-right:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:normal;line-height:1;color:#555;text-align:center;background-color:#d9d9d9;border:1px solid #ccc;border-radius:0}.input-group-addon.input-sm,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.input-group-addon.btn{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.input-group-addon.btn{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type="radio"],.input-group-addon input[type="checkbox"]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{margin-left:-1px}.nav{margin-bottom:0;padding-left:0;list-style:none}.nav:before,.nav:after{content:" ";display:table}.nav:after{clear:both}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#d9d9d9}.nav>li.disabled>a{color:#4e4e4e}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#4e4e4e;text-decoration:none;background-color:transparent;cursor:not-allowed}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#d9d9d9;border-color:#4d99d8}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid transparent}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857;border:1px solid transparent;border-radius:0 0 0 0;color:#fff}.nav-tabs>li>a:hover,.nav-tabs>li>a:focus{background:inherit;border-color:inherit inherit transparent}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#fff;background-color:#4e4e4e;border:1px solid inherit;border-bottom-color:transparent;cursor:default}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:0}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#ee5161}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified,.nav-tabs.nav-justified{width:100%}.nav-justified>li,.nav-tabs.nav-justified>li{float:none}.nav-justified>li>a,.nav-tabs.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width: 768px){.nav-justified>li,.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a,.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified,.nav-tabs.nav-justified{border-bottom:0}.nav-tabs-justified>li>a,.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:0}.nav-tabs-justified>.active>a,.nav-tabs.nav-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media (min-width: 768px){.nav-tabs-justified>li>a,.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:0 0 0 0}.nav-tabs-justified>.active>a,.nav-tabs.nav-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#303030}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}.navbar:before,.navbar:after{content:" ";display:table}.navbar:after{clear:both}@media (min-width: 768px){.navbar{border-radius:0}}.navbar-header:before,.navbar-header:after{content:" ";display:table}.navbar-header:after{clear:both}@media (min-width: 768px){.navbar-header{float:left}}.navbar-collapse{max-height:340px;overflow-x:visible;padding-right:0;padding-left:0;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);-webkit-overflow-scrolling:touch}.navbar-collapse:before,.navbar-collapse:after{content:" ";display:table}.navbar-collapse:after{clear:both}.navbar-collapse.in{overflow-y:auto}@media (min-width: 768px){.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block !important;height:auto !important;padding-bottom:0;overflow:visible !important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-left:0;padding-right:0}}.container>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-header,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}@media (min-width: 768px){.container>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-header,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width: 768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}@media (min-width: 768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:15px 0;font-size:18px;line-height:20px;height:50px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}@media (min-width: 768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:0}}.navbar-toggle{position:relative;float:right;margin-right:0;padding:9px 10px;margin-top:8px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:0}.navbar-toggle:focus{outline:none}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width: 768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px 0}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width: 767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media (min-width: 768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}.navbar-nav.navbar-right:last-child{margin-right:0}}@media (min-width: 768px){.navbar-left{float:left !important}.navbar-right{float:right !important}}.navbar-form{margin-left:0;margin-right:0;padding:10px 0;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);margin-top:8px;margin-bottom:8px}@media (max-width: 767px){.navbar-form .form-group{margin-bottom:5px}}@media (min-width: 768px){.navbar-form{width:auto;border:0;margin-left:0;margin-right:0;padding-top:0;padding-bottom:0;-webkit-box-shadow:none;box-shadow:none}.navbar-form.navbar-right:last-child{margin-right:0}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm,.btn-group-sm>.navbar-btn.btn{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs,.btn-group-xs>.navbar-btn.btn{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width: 768px){.navbar-text{float:left;margin-left:0;margin-right:0}.navbar-text.navbar-right:last-child{margin-right:0}}.navbar-default{background-color:#222;border-color:transparent}.navbar-default .navbar-brand{color:#fff}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#e6e6e6;background-color:#008b44}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#fff}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#fff;background-color:#333}.navbar-default .navbar-nav>.has-dropdown:not(.active):hover>a:first-child{color:#fff;background-color:#333}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#fff;background-color:#ee5161}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:transparent}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{background-color:#ee5161;color:#fff}@media (max-width: 767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#fff}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:#333}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#ee5161}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#fff}.navbar-default .navbar-link:hover{color:#fff}.navbar-inverse{background-color:#4e4e4e;border-color:transparent}.navbar-inverse .navbar-brand{color:#fff}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#fff}.navbar-inverse .navbar-nav>li>a{color:#fff}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#303030;background-color:#d9d9d9}.navbar-inverse .navbar-nav>li.has-dropdown:hover>a:first-child{color:#303030;background-color:#d9d9d9}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#353535}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#3c3c3c}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{background-color:#353535;color:#fff}@media (max-width: 767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#fff}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#303030;background-color:#d9d9d9}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#353535}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#fff}.navbar-inverse .navbar-link:hover{color:#303030}.pager{padding-left:0;margin:20px 0;list-style:none;text-align:center}.pager:before,.pager:after{content:" ";display:table}.pager:after{clear:both}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#d9d9d9}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#4e4e4e;background-color:#fff;cursor:not-allowed}.label{display:inline;padding:.2em .6em .3em;font-size:13px;line-height:1;color:#303030;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em;background:#eee}.label[href]:hover,.label[href]:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#eee;color:white}.label-default[href]:hover,.label-default[href]:focus{background-color:#d4d4d4}.label-primary{background-color:#fff;color:white}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#e6e6e6}.label-success{background-color:#5cb85c;color:white}.label-success[href]:hover,.label-success[href]:focus{background-color:#449d44}.label-info{background-color:#5bc0de;color:white}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f0ad4e;color:white}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#d9534f;color:white}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:bold;color:inherit;line-height:1;vertical-align:baseline;white-space:nowrap;text-align:center;background-color:#4e4e4e;border-radius:0}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge,.btn-group-xs>.btn .badge{top:0;padding:1px 5px}a.badge:hover,a.badge:focus{color:inherit;text-decoration:none;cursor:pointer}a.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#4d99d8;background-color:#fff}.nav-pills>li>a>.badge{margin-left:3px}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:0}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:bold}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable{padding-right:35px}.alert-dismissable .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{background-color:#dff0d8;border-color:#d6e9c6;color:#3c763d}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{background-color:#d9edf7;border-color:#bce8f1;color:#31708f}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{background-color:#fcf8e3;border-color:#faebcc;color:#8a6d3b}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{background-color:#f2dede;border-color:#ebccd1;color:#a94442}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:0;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.05);box-shadow:0 1px 1px rgba(0,0,0,0.05)}.panel-body{padding:15px}.panel-body:before,.panel-body:after{content:" ";display:table}.panel-body:after{clear:both}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:-1;border-top-left-radius:-1}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:-1;border-bottom-left-radius:-1}.panel>.list-group{margin-bottom:0}.panel>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-right-radius:-1;border-top-left-radius:-1}.panel>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:-1;border-bottom-left-radius:-1}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-right-radius:-1;border-top-left-radius:-1}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:-1}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:-1}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:-1;border-bottom-left-radius:-1}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:-1}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:-1}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{border:0;margin-bottom:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:0;overflow:hidden}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse .panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#303030;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse .panel-body{border-top-color:#ddd}.panel-default>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#fff}.panel-primary>.panel-heading{color:#fff;background-color:#fff;border-color:#fff}.panel-primary>.panel-heading+.panel-collapse .panel-body{border-top-color:#fff}.panel-primary>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#fff}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse .panel-body{border-top-color:#d6e9c6}.panel-success>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse .panel-body{border-top-color:#bce8f1}.panel-info>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse .panel-body{border-top-color:#faebcc}.panel-warning>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse .panel-body{border-top-color:#ebccd1}.panel-danger>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ebccd1}.well{min-height:20px;padding:0;margin-bottom:20px;background-color:inherit;border:1px solid inherit;border-radius:0}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:bold;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:0.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:0.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{display:none;overflow:auto;overflow-y:scroll;position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0, -25%);-ms-transform:translate(0, -25%);transform:translate(0, -25%);-webkit-transition:-webkit-transform 0.3s ease-out;-moz-transition:-moz-transform 0.3s ease-out;-o-transition:-o-transform 0.3s ease-out;transition:transform 0.3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);transform:translate(0, 0)}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,0.2);border-radius:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,0.5);box-shadow:0 3px 9px rgba(0,0,0,0.5);background-clip:padding-box;outline:none}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:0.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid transparent;min-height:16.42857px}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857}.modal-body{position:relative;padding:20px}.modal-footer{margin-top:15px;padding:19px 20px 20px;text-align:right;border-top:1px solid transparent}.modal-footer:before,.modal-footer:after{content:" ";display:table}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}@media (min-width: 768px){.modal-dialog{width:760px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,0.5);box-shadow:0 5px 15px rgba(0,0,0,0.5)}.modal-sm{width:300px}}@media (min-width: 992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1030;display:block;visibility:visible;font-size:12px;line-height:1.4;opacity:0;filter:alpha(opacity=0)}.tooltip.in{opacity:0.9;filter:alpha(opacity=90)}.tooltip.top{margin-top:-3px;padding:5px 0}.tooltip.right{margin-left:3px;padding:0 5px}.tooltip.bottom{margin-top:3px;padding:5px 0}.tooltip.left{margin-left:-3px;padding:0 5px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;border-radius:0}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{bottom:0;left:5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;right:5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;left:5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;right:5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;max-width:276px;padding:1px;text-align:left;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);border-radius:3px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);white-space:normal}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{margin:0;padding:8px 14px;font-size:14px;font-weight:normal;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:3px 3px 0 0}.popover-content{padding:5px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{border-width:10px;content:""}.popover.top>.arrow{left:50%;margin-left:-11px;border-bottom-width:0;border-top-color:#999;border-top-color:fadein(rgba(0,0,0,0.2), 5%);bottom:-11px}.popover.top>.arrow:after{content:" ";bottom:1px;margin-left:-10px;border-bottom-width:0;border-top-color:#fff}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-left-width:0;border-right-color:#999;border-right-color:fadein(rgba(0,0,0,0.2), 5%)}.popover.right>.arrow:after{content:" ";left:1px;bottom:-10px;border-left-width:0;border-right-color:#fff}.popover.bottom>.arrow{left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:fadein(rgba(0,0,0,0.2), 5%);top:-11px}.popover.bottom>.arrow:after{content:" ";top:1px;margin-left:-10px;border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:fadein(rgba(0,0,0,0.2), 5%)}.popover.left>.arrow:after{content:" ";right:1px;border-right-width:0;border-left-color:#fff;bottom:-10px}.clearfix:before,.clearfix:after{content:" ";display:table}.clearfix:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right !important}.pull-left{float:left !important}.hide{display:none !important}.show{display:block !important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none !important;visibility:hidden !important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none !important}@media (max-width: 767px){.visible-xs{display:block !important}table.visible-xs{display:table}tr.visible-xs{display:table-row !important}th.visible-xs,td.visible-xs{display:table-cell !important}}@media (min-width: 768px) and (max-width: 991px){.visible-sm{display:block !important}table.visible-sm{display:table}tr.visible-sm{display:table-row !important}th.visible-sm,td.visible-sm{display:table-cell !important}}@media (min-width: 992px) and (max-width: 1199px){.visible-md{display:block !important}table.visible-md{display:table}tr.visible-md{display:table-row !important}th.visible-md,td.visible-md{display:table-cell !important}}@media (min-width: 1200px){.visible-lg{display:block !important}table.visible-lg{display:table}tr.visible-lg{display:table-row !important}th.visible-lg,td.visible-lg{display:table-cell !important}}@media (max-width: 767px){.hidden-xs{display:none !important}}@media (min-width: 768px) and (max-width: 991px){.hidden-sm{display:none !important}}@media (min-width: 992px) and (max-width: 1199px){.hidden-md{display:none !important}}@media (min-width: 1200px){.hidden-lg{display:none !important}}.visible-print{display:none !important}@media print{.visible-print{display:block !important}table.visible-print{display:table}tr.visible-print{display:table-row !important}th.visible-print,td.visible-print{display:table-cell !important}}@media print{.hidden-print{display:none !important}}.spinner{text-align:center}.spinner>div{width:8px;height:8px;background-color:#fff;border-radius:100%;display:inline-block;-webkit-animation:bouncedelay 1.4s infinite ease-in-out;animation:bouncedelay 1.4s infinite ease-in-out;-webkit-animation-fill-mode:both;animation-fill-mode:both}.spinner .bounce1{-webkit-animation-delay:-0.32s;animation-delay:-0.32s}.spinner .bounce2{-webkit-animation-delay:-0.16s;animation-delay:-0.16s}@-webkit-keyframes bouncedelay{0%,80%,100%{-webkit-transform:scale(0)}40%{-webkit-transform:scale(1)}}@keyframes bouncedelay{0%,80%,100%{transform:scale(0);-webkit-transform:scale(0)}40%{transform:scale(1);-webkit-transform:scale(1)}}.line-btn{display:inline-block;text-align:center;opacity:1;background-color:#222;border-bottom:2px solid #222;color:#fff}.line-btn span{display:inline-block;height:100%;line-height:100%;padding:8px}.line-btn:hover,.line-btn:focus{text-decoration:none;opacity:0.85}.line-btn .snf-font-remove{display:inline}.line-btn.disabled{background:transparent !important;border-color:#818181 !important;color:#818181 !important}.line-btn.disabled:hover,.line-btn.disabled:focus{cursor:default;opacity:1}.line-btn.disabled:hover span,.line-btn.disabled:focus span{color:#818181 !important}.line-btn:hover,.line-btn:focus{opacity:1;border-bottom-color:#fff;color:#fff}.outline-btn{display:inline-block;text-align:center;opacity:1;border:1px solid #fff;color:#fff}.outline-btn span{display:inline-block;height:100%;line-height:100%;padding:8px}.outline-btn:hover,.outline-btn:focus{text-decoration:none;opacity:0.85}.outline-btn .snf-font-remove{display:inline}.outline-btn.disabled{background:transparent !important;border-color:#818181 !important;color:#818181 !important}.outline-btn.disabled:hover,.outline-btn.disabled:focus{cursor:default;opacity:1}.outline-btn.disabled:hover span,.outline-btn.disabled:focus span{color:#818181 !important}.outline-btn span{border:1px solid transparent;width:100%}.outline-btn:hover span,.outline-btn:focus span{border-color:#fff}.outline-btn.disabled{@inlcude disabled;;color:#818181}.outline-btn.disabled:hover span,.outline-btn.disabled:focus span{border-color:transparent}.custom-btn{display:inline-block;text-align:center;opacity:1;border:1px solid #3c96e0;color:#fff;background-color:#3c96e0}.custom-btn span{display:inline-block;height:100%;line-height:100%;padding:8px}.custom-btn:hover,.custom-btn:focus{text-decoration:none;opacity:0.85}.custom-btn .snf-font-remove{display:inline}.custom-btn.disabled{background:transparent !important;border-color:#818181 !important;color:#818181 !important}.custom-btn.disabled:hover,.custom-btn.disabled:focus{cursor:default;opacity:1}.custom-btn.disabled:hover span,.custom-btn.disabled:focus span{color:#818181 !important}.custom-btn span{border:1px solid transparent;background:transparent}.custom-btn:hover span,.custom-btn:focus span{color:#fff}.custom-btn.disabled{background:transparent !important;border-color:#818181 !important;color:#818181 !important}.custom-btn.disabled:hover,.custom-btn.disabled:focus{cursor:default;opacity:1}.custom-btn.disabled:hover span,.custom-btn.disabled:focus span{color:#818181 !important}.custom-btn[data-karma="neutral"]{background-color:#3c96e0;border-color:#3c96e0}.custom-btn[data-karma="good"]{background-color:#00a551;border-color:#00a551}.custom-btn[data-karma="bad"]{background-color:#d2881f;border-color:#d2881f}.custom-btn[data-caution="warning"][data-karma="good"],.custom-btn[data-caution="warning"][data-karma="neutral"]{background-color:#d2881f;border-color:#d2881f}.custom-btn[data-caution="dangerous"][data-karma="bad"],.custom-btn[data-caution="dangerous"][data-karma="neutral"]{background-color:#e42a48;border-color:#e42a48}.search-btn{display:inline-block;text-align:center;opacity:1;background-color:#222;border-bottom:2px solid #222;color:#fff;position:relative;top:-2px;margin-left:20px;cursor:pointer}.search-btn span{display:inline-block;height:100%;line-height:100%;padding:8px}.search-btn:hover,.search-btn:focus{text-decoration:none;opacity:0.85}.search-btn .snf-font-remove{display:inline}.search-btn.disabled{background:transparent !important;border-color:#818181 !important;color:#818181 !important}.search-btn.disabled:hover,.search-btn.disabled:focus{cursor:default;opacity:1}.search-btn.disabled:hover span,.search-btn.disabled:focus span{color:#818181 !important}.search-btn:hover,.search-btn:focus{opacity:1;border-bottom-color:#fff;color:#fff}.search-btn span{padding:7px}.search-mode-btn{float:right;line-height:30px}.search-mode-btn:hover{cursor:pointer}.instructions .line-btn{padding:8px 10px}.instructions .line-btn span{padding:0 4px}.instructions .line-btn:hover .arrow{font-weight:bold}.instructions .line-btn.open:hover{border-bottom-color:transparent}.instructions .line-btn .arrow{vertical-align:middle}.sidebar{margin:0 30px 0 0;width:110px;height:auto;float:left}.sidebar .btn-group-vertical{width:100%}@media (max-width: 1200px){.sidebar{width:auto;margin:20px auto;float:none}.sidebar .btn-group-vertical a{margin-right:10px;display:inline-block}}.sidebar .custom-btn{display:block;margin:0 0 1em}.sidebar .custom-btn span{padding:8px}body .custom-buttons{float:left;margin-right:10px}body .custom-buttons .line-btn{margin-right:1em}body .custom-buttons .disabled{display:none}body .custom-buttons .extra-btn{float:right;margin-right:0}body .custom-buttons .extra-btn span{display:inline-block}body .custom-buttons .extra-btn .badge{background:transparent;line-height:0.8;display:inline;padding:0 5px 0 0;font-weight:normal;font-size:1em}body .custom-buttons .extra-btn .badge::before{content:"("}body .custom-buttons .extra-btn .badge::after{content:")"}.show-hide-all{float:right}.show-hide-all em{font-style:normal}.show-hide-all.line-btn{padding:8px}.show-hide-all.line-btn span{display:inline}.actions-per-item .custom-btn{margin:10px 10px 10px 0}.charts .chart{display:none}.charts .sidebar a{display:inline-block;text-align:center;opacity:1;border:1px solid #fff;color:#fff;display:block;margin:20px auto}.charts .sidebar a span{display:inline-block;height:100%;line-height:100%;padding:8px}.charts .sidebar a:hover,.charts .sidebar a:focus{text-decoration:none;opacity:0.85}.charts .sidebar a .snf-font-remove{display:inline}.charts .sidebar a.disabled{background:transparent !important;border-color:#818181 !important;color:#818181 !important}.charts .sidebar a.disabled:hover,.charts .sidebar a.disabled:focus{cursor:default;opacity:1}.charts .sidebar a.disabled:hover span,.charts .sidebar a.disabled:focus span{color:#818181 !important}.charts .sidebar a span{border:1px solid transparent;width:100%}.charts .sidebar a:hover span,.charts .sidebar a:focus span{border-color:#fff}.charts .sidebar a.disabled{@inlcude disabled;;color:#818181}.charts .sidebar a.disabled:hover span,.charts .sidebar a.disabled:focus span{border-color:transparent}.charts .sidebar a.active{display:inline-block;text-align:center;opacity:1;border:1px solid #3c96e0;color:#fff;background-color:#3c96e0;display:block}.charts .sidebar a.active span{display:inline-block;height:100%;line-height:100%;padding:8px}.charts .sidebar a.active:hover,.charts .sidebar a.active:focus{text-decoration:none;opacity:0.85}.charts .sidebar a.active .snf-font-remove{display:inline}.charts .sidebar a.active.disabled{background:transparent !important;border-color:#818181 !important;color:#818181 !important}.charts .sidebar a.active.disabled:hover,.charts .sidebar a.active.disabled:focus{cursor:default;opacity:1}.charts .sidebar a.active.disabled:hover span,.charts .sidebar a.active.disabled:focus span{color:#818181 !important}.charts .sidebar a.active span{border:1px solid transparent;background:transparent}.charts .sidebar a.active:hover span,.charts .sidebar a.active:focus span{color:#fff}.charts .sidebar a.active.disabled{background:transparent !important;border-color:#818181 !important;color:#818181 !important}.charts .sidebar a.active.disabled:hover,.charts .sidebar a.active.disabled:focus{cursor:default;opacity:1}.charts .sidebar a.active.disabled:hover span,.charts .sidebar a.active.disabled:focus span{color:#818181 !important}@media (max-width: 1200px){.charts .sidebar a,.charts .sidebar a.active{margin-right:10px;display:inline-block}}.notify .reload-btn{padding:0 4px;font-size:18px;vertical-align:middle;cursor:pointer}.onoffswitch{display:inline-block;float:right;position:relative;width:134px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}.onoffswitch-checkbox{display:none}.onoffswitch-label{display:block;overflow:hidden;cursor:pointer;border-radius:20px}.onoffswitch-inner{display:block;width:200%;margin-left:-100%;-moz-transition:margin 0.3s ease-in 0s;-webkit-transition:margin 0.3s ease-in 0s;-o-transition:margin 0.3s ease-in 0s;transition:margin 0.3s ease-in 0s}.onoffswitch-inner:before,.onoffswitch-inner:after{display:block;float:left;width:50%;height:30px;padding:0;line-height:30px;font-size:12px;color:white;font-family:Trebuchet, Arial, sans-serif;font-weight:normal;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.onoffswitch-inner:before{content:"Standard View";padding-left:10px;background-color:#222;color:#fff}.onoffswitch-inner:after{content:"Compact View";padding-right:10px;background-color:#222;color:#fff;text-align:right}.onoffswitch-switch{display:block;width:19px;margin:6px;background:#fff;border:2px solid #F7EFEF;border-radius:20px;position:absolute;top:0;bottom:4px;right:103px;-moz-transition:all 0.3s ease-in 0s;-webkit-transition:all 0.3s ease-in 0s;-o-transition:all 0.3s ease-in 0s;transition:all 0.3s ease-in 0s}.onoffswitch-checkbox:checked+.onoffswitch-label .onoffswitch-inner{margin-left:0}.onoffswitch-checkbox:checked+.onoffswitch-label .onoffswitch-switch{right:0px}li.active .snf-checkbox-unchecked,li.active .snf-radio-unchecked{display:none}li:not(.active) .snf-checkbox-checked,li:not(.active) .snf-radio-checked{display:none}table.dataTable tbody tr.selected .snf-checkbox-unchecked{display:none}table.dataTable tbody tr:not(.selected) .snf-checkbox-checked{display:none}.show-hide-all.open .snf-font-arrow-down{display:none}.show-hide-all:not(.open) .snf-font-arrow-up{display:none}.instructions .line-btn.open .snf-angle-down{display:none}.instructions .line-btn:not(.open) .snf-angle-up{display:none}@font-face{font-family:'font-icons';src:url("../fonts/font-icons.eot?hm0cup");src:url("../fonts/font-icons.eot?#iefixhm0cup") format("embedded-opentype"),url("../fonts/font-icons.woff?hm0cup") format("woff"),url("../fonts/font-icons.ttf?hm0cup") format("truetype"),url("../fonts/font-icons.svg?hm0cup#font-icons") format("svg");font-weight:normal;font-style:normal}@font-face{font-family:"snf-font";src:url("../fonts/snf-font.eot");src:url("../fonts/snf-font.eot?#iefix") format("embedded-opentype"),url("../fonts/snf-font.woff") format("woff"),url("../fonts/snf-font.ttf") format("truetype"),url("../fonts/snf-font.svg#snf-font") format("svg");font-weight:normal;font-style:normal}.snf-ok{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-ok:before{content:"\61"}.snf-remove,body .custom-buttons .snf-font-remove{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-remove:before,body .custom-buttons .snf-font-remove:before{content:"\62"}.snf-envelope{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-envelope:before{content:"\63"}.snf-envelope-alt{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-envelope-alt:before{content:"\64"}.snf-angle-up{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-angle-up:before{content:"\65"}.snf-angle-down{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-angle-down:before{content:"\66"}.snf-exclamation-sign{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-exclamation-sign:before{content:"\67"}.snf-clipboard-h,.snf-details-project{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-clipboard-h:before,.snf-details-project:before{content:"\68"}.snf-clipboard-i{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-clipboard-i:before{content:"\69"}.snf-copy{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-copy:before{content:"\6c"}.snf-search{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-search:before{content:"\6d"}.snf-sign-out{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-sign-out:before{content:"\6e"}.snf-archive{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-archive:before{content:"\6b"}.snf-checkbox-checked{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-checkbox-checked:before{content:"\6f"}.snf-checkbox-unchecked{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-checkbox-unchecked:before{content:"\70"}.snf-radio-checked{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-radio-checked:before{content:"\71"}.snf-radio-unchecked{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-radio-unchecked:before{content:"\72"}.snf-info{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-info:before{content:"\73"}.snf-user-outline{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-user-outline:before{content:"\75"}.snf-user-full,.snf-details-user{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-user-full:before,.snf-details-user:before{content:"\74"}.snf-wallet-full,.snf-details-quota{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-wallet-full:before,.snf-details-quota:before{content:"\78"}.snf-wallet-outline{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-wallet-outline:before{content:"\79"}.snf-keyboard{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-keyboard:before{content:"\7a"}.snf-book-2{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-book-2:before{content:"\42"}.snf-bell-1{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-bell-1:before{content:"\43"}.snf-bulb{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-bulb:before{content:"\46"}.snf-sun-1{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-sun-1:before{content:"\47"}.snf-moon-1{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-moon-1:before{content:"\76"}.snf-sun-2-full{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-sun-2-full:before{content:"\77"}.snf-sun-2-outline{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-sun-2-outline:before{content:"\6a"}.snf-moon-2-full:before{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-moon-2-full:before:before{content:"\44"}.snf-moon-2-outline{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-moon-2-outline:before{content:"\45"}.snf-sun-3{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-sun-3:before{content:"\41"}.snf-filter{font-family:"font-icons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-filter:before{content:"\7b"}.snf-eye{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-eye:before{content:"\41"}.snf-radio-checked{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-radio-checked:before{content:"\42"}.snf-radio-unchecked{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-radio-unchecked:before{content:"\43"}.snf-close{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-close:before{content:"\44"}.snf-www{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-www:before{content:"\49"}.snf-arrow-up,.show-hide-all span.snf-font-arrow-up{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-arrow-up:before,.show-hide-all span.snf-font-arrow-up:before{content:"\4c"}.snf-arrow-down,.show-hide-all span.snf-font-arrow-down{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-arrow-down:before,.show-hide-all span.snf-font-arrow-down:before{content:"\4d"}.snf-checkbox-unchecked{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-checkbox-unchecked:before{content:"\61"}.snf-checkbox-checked{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-checkbox-checked:before{content:"\62"}.snf-cancel-circled{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-cancel-circled:before{content:"\63"}.snf-search{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-search:before{content:"\64"}.snf-twitter-logo{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-twitter-logo:before{content:"\67"}.snf-ok{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-ok:before{content:"\68"}.snf-switch{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-switch:before{content:"\69"}.snf-ban-circle{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-ban-circle:before{content:"\6a"}.snf-ok-sign{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-ok-sign:before{content:"\6c"}.snf-minus-sign{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-minus-sign:before{content:"\6e"}.snf-edit{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-edit:before{content:"\71"}.snf-listview{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-listview:before{content:"\73"}.snf-gridview{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-gridview:before{content:"\74"}.snf-dashboard-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-dashboard-outline:before{content:"\7a"}.snf-pithos-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-pithos-outline:before{content:"\79"}.snf-info-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-info-full:before{content:"\70"}.snf-volume-create-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-volume-create-full:before{content:"\36"}.snf-image-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-image-full:before{content:"\51"}.snf-pc-create-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-pc-create-full:before{content:"\53"}.snf-network-create-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-network-create-outline:before{content:"\54"}.snf-network-create-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-network-create-full:before{content:"\55"}.snf-ram-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-ram-outline:before{content:"\4a"}.snf-nic-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-nic-outline:before{content:"\50"}.snf-ram-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-ram-full:before{content:"\52"}.snf-nic-full,.snf-details-nic{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-nic-full:before,.snf-details-nic:before{content:"\72"}.snf-network-broken-1-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-network-broken-1-full:before{content:"\56"}.snf-network-broken-2-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-network-broken-2-full:before{content:"\57"}.snf-pc-broken-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-pc-broken-full:before{content:"\58"}.snf-pc-reboot-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-pc-reboot-full:before{content:"\59"}.snf-pc-switch-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-pc-switch-full:before{content:"\5a"}.snf-key-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-key-full:before{content:"\31"}.snf-router-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-router-full:before{content:"\32"}.snf-chip-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-chip-full:before{content:"\33"}.snf-plus-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-plus-full:before{content:"\34"}.snf-snapshot-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-snapshot-full:before{content:"\4e"}.snf-pithos-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-pithos-full:before{content:"\35"}.snf-volume-full,.snf-details-volume{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-volume-full:before,.snf-details-volume:before{content:"\4f"}.snf-network-full,.snf-details-network{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-network-full:before,.snf-details-network:before{content:"\4b"}.snf-pc-full,.snf-details-vm{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-pc-full:before,.snf-details-vm:before{content:"\78"}.snf-network-broken-1-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-network-broken-1-outline:before{content:"\37"}.snf-network-broken-2-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-network-broken-2-outline:before{content:"\38"}.snf-pc-broken-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-pc-broken-outline:before{content:"\39"}.snf-volume-broken-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-volume-broken-outline:before{content:"\30"}.snf-pc-reboot-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-pc-reboot-outline:before{content:"\21"}.snf-pc-switch-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-pc-switch-outline:before{content:"\40"}.snf-key-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-key-outline:before{content:"\23"}.snf-router-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-router-outline:before{content:"\48"}.snf-chip-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-chip-outline:before{content:"\45"}.snf-image-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-image-outline:before{content:"\66"}.snf-plus-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-plus-outline:before{content:"\6d"}.snf-snapshot-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-snapshot-outline:before{content:"\65"}.snf-volume-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-volume-outline:before{content:"\75"}.snf-network-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-network-outline:before{content:"\76"}.snf-pc-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-pc-outline:before{content:"\77"}.snf-info-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-info-outline:before{content:"\6f"}.snf-thunder-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-thunder-full:before{content:"\6b"}.snf-lock-closed-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-lock-closed-full:before{content:"\46"}.snf-lock-open-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-lock-open-full:before{content:"\47"}.snf-link-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-link-outline:before{content:"\26"}.snf-refresh-outline,body .custom-buttons .snf-font-reload{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-refresh-outline:before,body .custom-buttons .snf-font-reload:before{content:"\29"}.snf-download-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-download-full:before{content:"\25"}.snf-person-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-person-outline:before{content:"\2a"}.snf-upload-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-upload-full:before{content:"\28"}.snf-arrow-right-small-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-arrow-right-small-full:before{content:"\2d"}.snf-copy-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-copy-outline:before{content:"\3f"}.snf-copy-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-copy-full:before{content:"\22"}.snf-arrow-left-small-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-arrow-left-small-full:before{content:"\5f"}.snf-trash-full{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-trash-full:before{content:"\3d"}.snf-trash-outline{font-family:"snf-font";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.snf-trash-outline:before{content:"\24"}.main{margin:2em 0 5em}.main h4 .title{font-size:24px}.main span[class^="snf-details"]{float:left;margin-right:8px;font-size:35px}.main .lt{line-height:35px}.main .rt{padding-top:5px}.main .actions-per-item{padding:0}.object-anchor{height:2px}.object-details h4{font-size:14px;letter-spacing:1px}.object-details h4 .lt{display:block;float:left;max-width:60%;word-wrap:break-word}.object-details h4 .rt{padding-top:5px;display:block;overflow:hidden}.object-details h4 .arrow{position:relative;padding:0 8px}.object-details h4 .arrow:hover,.object-details h4 .arrow:focus{top:2px;cursor:pointer;outline:0 none}.object-details h4 .label{float:right;margin-left:15px;margin-bottom:10px}.object-details h4 .label.important{font-weight:bold}.object-details h4 em{float:none}.object-details h4 em.os-info{float:right;position:relative;bottom:3px}.object-details h4 em.os-info img{height:26px;margin-right:5px}.object-details h3{font-size:18px;margin:0 0 1em;font-weight:400;line-height:35px}.object-details h3 em{margin-left:10px;font-size:14px;display:inline-block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:50%;vertical-align:top}.object-details h3 span[class^="snf-details"]{float:left;margin-right:8px;font-size:25px;height:35px;line-height:35px}.object-details h3 .popover-dismiss{display:inline-block;width:18px;height:18px;background:#4e4e4e;-webkit-border-radius:9px;-moz-border-radius:9px;border-radius:9px;text-align:center;font-weight:bold;vertical-align:middle;line-height:18px;font-size:16px;vertical-align:super;cursor:pointer;margin-left:10px;color:#818181}.object-details h3 .popover-dismiss:hover,.object-details h3 .popover-dismiss:focus{background:#686868;color:#eee}.object-details h3 .popover .popover-content{font-size:12px;line-height:130%}.object-details .icon-link{margin-right:10px}.object-details p{margin:10px 20px;font-style:italic}.object-details .length{margin-left:6px;border:0 none;font-style:italic}.object-details .length::before{content:'( '}.object-details .length::after{content:' )'}.object-details>.object-details{margin-left:-20px;margin-right:-20px;padding:12px 20px}.object-details-content .nav-tabs>li a{opacity:0.7}.object-details-content .nav-tabs>li.active>a{opacity:1}.object-details-content .nav-tabs>li:not(.active)>a:hover,.object-details-content .nav-tabs>li:not(.active)>a:focus{opacity:1}.tab-pane{overflow:auto}.parts-separator{border-top:2px solid #4e4e4e;padding-top:1em}.parts-separator h2{font-size:24px;margin-bottom:2em;padding-top:1em}.parts-separator h2 em{max-width:50%;display:inline;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:top}.part-two>.object-details{border-bottom:2px solid #818181;background:#383838;padding:14px 20px;overflow-x:auto}.part-two>.object-details .object-details{padding:5px 20px}.part-two>.object-details .object-details:hover,.part-two>.object-details .object-details:focus{background:#3d3d3d}.part-two>.object-details .custom-btn span{padding:5px}.part-two .object-details-content{display:none;padding:0 35px}.show-hide-all span.snf-font-arrow-up{padding:0}.show-hide-all span.snf-font-arrow-down{padding:0}.filters-area{margin-bottom:40px;margin-left:140px}@media (max-width: 1200px){.filters-area{margin:0 10px 10px 0}}.filters-area.no-margin-left{margin-left:0}.filters-area a:focus,.filters-area input:focus{outline:none}.filters-area .badge{margin-left:6px;opacity:0.9;padding:2px 9px}.filters-area ul.nav a{padding-bottom:10px}.filter{height:30px;margin:0 10px 10px 0;display:inline-block;background:#eee;border:1px solid transparent}.filter .form-group{margin:0;height:30px}.filter label,.filter .dropdown{height:30px;line-height:30px;border:0 none;padding:0 10px;color:#303030;background:transparent;font-weight:normal;margin:0}.filter label>a .selected-value,.filter .dropdown>a .selected-value{margin-left:4px}.filter label>a .arrow,.filter .dropdown>a .arrow{font-weight:bold}.filter label.open a,.filter .dropdown.open a{text-decoration:none;color:#303030}.filter label a,.filter .dropdown a{color:#303030}.filter .dropdown-menu,.filter .dropdown-list{background:#eee;margin:0;width:auto}.filter .dropdown-menu>.active>a,.filter .dropdown-list>.active>a{background:#eee}.filter .dropdown-menu>li:hover>a,.filter .dropdown-list>li:hover>a{background:#d9d9d9;color:inherit}.filter .dropdown-menu a,.filter .dropdown-list a{padding-left:12px;padding-right:12px}.filter .dropdown-menu a span,.filter .dropdown-list a span{margin-right:6px}.filter input{border:0 none;background:transparent;height:30px;line-height:30px;padding:0 5px;font-weight:normal;color:#303030}.filter .dropdown-list>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:1.42857;color:#303030;white-space:nowrap}.input-with-btn{border-width:0px;background-color:transparent;display:inline}@media screen and (min-width: 400px){.input-with-btn input{width:200px}}@media screen and (min-width: 600px){.input-with-btn input{width:300px}}@media screen and (min-width: 800px){.input-with-btn input{width:500px}}@media screen and (min-width: 1000px){.input-with-btn input{width:700px}}.input-with-btn .form-group{display:inline-block;background:#eee;border:1px solid transparent;margin-bottom:0.6em}.input-with-btn .filter-error{word-wrap:break-word}.input-with-btn .error-sign{display:block;opacity:0;position:static;display:inline-block;margin-right:6px;margin-left:10px;vertical-align:bottom}.input-with-btn .instructions{margin-top:0.6em}.input-with-btn .instructions *{color:#fff}.input-with-btn .instructions .content-area{display:none;background:#222;padding:12px 13px 18px}.input-with-btn .instructions .content-area dt{width:200px}.input-with-btn .instructions .content-area dd{margin-left:220px}.input-with-btn .instructions .clarifications{font-style:italic}.filter:not(.visible-filter):not(.visible-filter-fade){display:none;opacity:0}.visible-filter-fade{opacity:1;transition:opacity 0.5s}.filters .filters-list{border-radius:15px;background:#222;border:1px solid #fff;height:28px}.filters .filters-list>a{color:#fff;line-height:28px;font-weight:bold;padding:8px 7px;background:transparent}.filters .filters-list .popover{padding:0}.filters .filters-list .popover-content{padding:0}.filters .filters-list .popover ul{list-style:none;padding:5px 0px;min-width:160px}.filters .filters-list .popover ul li{white-space:nowrap}.filters .filters-list .popover ul li a{color:#303030}.filters .filters-list .popover ul li span{margin-right:10px}.filters .filters-list .popover ul .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.filters .filters-list .popover.bottom>.arrow:after{border-bottom-color:#eee}p.progress-area{visibility:hidden}.in-progress .modal-body{background-color:#818181}.in-progress .modal-body p.progress-area{visibility:visible}.modal[data-item="user"]:not([data-type="contact"]) .table-selected td:nth-child(3){display:none}.modal#user-contact p{margin-top:18px;position:relative}.modal p{position:relative}.modal p>.error-sign{top:0}.modal h3{margin-top:0;font-weight:bold}.modal textarea{resize:vertical}.modal textarea,.modal input{width:87%;vertical-align:text-top;padding:4px 8px;border:1px solid #d9d9d9;color:#303030}.modal textarea.body,.modal input.body{min-height:160px}.modal label{margin-right:6px;width:70px;vertical-align:sub}.modal .modal-body{background-color:white}.modal .modal-footer{margin-top:0}.modal .modal-footer form{display:inline}.modal .modal-footer .custom-btn:first-child{float:left;background-color:#303030;border-color:#303030}.modal .custom-btn{color:white;opacity:0.9}.modal .custom-btn:hover,.modal .custom-btn:focus{opacity:1}.modal[data-karma="dark"] .elem{color:#4e4e4e}.modal[data-karma="neutral"] .elem{color:#207dc9}.modal[data-karma="good"] .elem{color:#007238}.modal[data-karma="bad"] .elem{color:#a66b18}.modal[data-caution="warning"][data-karma="good"] .elem,.modal[data-caution="warning"][data-karma="neutral"] .elem{color:#a66b18}.modal[data-caution="dangerous"][data-karma="bad"] .elem,.modal[data-caution="dangerous"][data-karma="neutral"] .elem{color:#c21934}.custom-btn[data-karma="dark"]{background-color:#222222;border-color:transparent}.modal em{font-weight:bold;font-style:normal}.modal .popover{z-index:2000}.modal .popover dl{color:black;font-weight:normal}.modal .popover dl dt{width:90px}.modal .popover dl dd{margin-left:110px}.modal .popover h2{font-size:16px;color:#303030;font-weight:bold;text-align:center}.modal .popover-content{min-width:150px}.modal-content{padding:20px;color:#303030}.modal-content .badge{background-color:transparent}.instructions-icon{color:#3c96e0;font-size:22px;margin-left:78px}.instructions-icon:hover{text-decoration:none}.extra-info{margin-top:10px}.error-sign{color:red;font-size:20px;margin-left:10px;position:absolute;top:6px;display:none}.error-sign:hover,.error-sign:focus{color:red;text-decoration:none}.form-area{position:relative}.form-subject{margin-bottom:15px}.toggle-more{margin-top:-16px;display:none}.modal .table-selected th,.modal .table-selected td{word-break:break-word}.modal .table-selected td:last-child .wrap{padding-right:36px}.modal .table-selected tr:nth-child(2n){background:#f2f2f2}.modal .table-selected tr a{font-weight:bold}.modal .table-selected tr:hover,.modal .table-selected tr:focus{background:#d9d9d9}.modal .table-selected tr:hover a,.modal .table-selected tr:focus a{color:red}.modal .table-selected .remove{position:absolute;right:14px;color:transparent}.modal .table-selected .remove:hover{cursor:pointer;text-decoration:none}table thead th{white-space:nowrap}table td,table th{vertical-align:top}table .wrap{position:relative}.table-items .snf-search{opacity:0.7;font-size:15px}.table-items .snf-search:hover,.table-items .snf-search:focus{opacity:1}.table-items .login-method{padding:2px 16px 2px 0px;text-align:center}.table-items th .badge{margin:0 2px 0 4px;display:inline;padding-top:2px}.table-items td{padding:8px 6px 0 6px}.table-selected-main:not(.table-selected) td:last-child,.table-items:not(.table-selected) td:last-child{max-width:60px;min-width:60px;padding:8px 5px}.table-selected-main:not(.table-selected) td:last-child .details-link:hover,.table-items:not(.table-selected) td:last-child .details-link:hover{text-decoration:none}.table-selected-main:not(.table-selected) td:last-child .summary-expand,.table-items:not(.table-selected) td:last-child .summary-expand{position:relative;z-index:10;float:right;padding-left:8px;padding-right:8px;background-color:#4d99d8;color:#fff}.table-selected-main:not(.table-selected) td:last-child .summary-expand:hover,.table-selected-main:not(.table-selected) td:last-child .summary-expand:focus,.table-items:not(.table-selected) td:last-child .summary-expand:hover,.table-items:not(.table-selected) td:last-child .summary-expand:focus{text-decoration:none;background-color:#83b8e4}.table-selected-main:not(.table-selected) td:last-child dl,.table-items:not(.table-selected) td:last-child dl{z-index:0;position:relative;padding:8px;display:none;margin:0}.table-items .headerSortUp span.caret{border-top:0;border-bottom:4px solid}#table-items-selected_filter label,#table-items-total_filter label{color:#fff}#table-items-selected_filter input,#table-items-total_filter input{color:#303030;background:#eee;border:1px solid transparent;padding:3px 5px}#table-items-selected_filter input:focus,#table-items-total_filter input:focus{outline:0 none}#table-items-selected_wrapper{padding:10px;border:1px solid gray;margin-bottom:20px;display:none}div.dataTables_length{padding-left:2em;padding-top:0.55em}div.dataTables_length select{width:55px;display:inline-block;margin-left:4px;vertical-align:baseline;color:#222}table.dataTable tbody tr{background-color:inherit}table.dataTable tbody tr.even{background-color:#3d3d3d}table.dataTable thead th,table.dataTable thead td{border-bottom:1px solid white;border-top:1px solid #fff}table.dataTable tbody tr:hover{background-color:#4f4f4f}table.dataTable tbody tr.selected{color:#303030;background-color:#eee}html body .dataTables_wrapper label{font-weight:normal}html body .dataTables_wrapper table th.sorting,html body .dataTables_wrapper table th.sorting_asc,html body .dataTables_wrapper table th.sorting_desc{background-position:center left;padding-left:22px}.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_paginate{padding-top:0;margin-bottom:0.5em;color:#fff;line-height:35px}table.dataTable.no-footer{border-bottom:1px solid #eee;margin:2em 0}.dataTables_wrapper .dataTables_paginate .paginate_button{color:#fff !important;padding:0 1em}.container .dataTables_wrapper .dataTables_paginate .paginate_button:hover,.container .dataTables_wrapper .dataTables_paginate .paginate_button:focus{background:transparent;border-color:#fff;color:#fff !important}.container .dataTables_wrapper .dataTables_paginate .paginate_button.disabled{border-color:transparent;color:#818181 !important}.container .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover,.container .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:focus,.container .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active{color:#818181 !important}.dataTables_wrapper .dataTables_paginate .paginate_button.current,.dataTables_wrapper .dataTables_paginate .paginate_button.current:focus,.dataTables_wrapper .dataTables_paginate .paginate_button.current:hover{background:#fff;color:#303030 !important;border:transparent}.dataTables_wrapper>.custom-buttons{margin-bottom:1em;width:100%}.dataTables_wrapper .dataTables_processing{background:#4e4e4e;color:#fff;padding:5px 10px;-webkit-box-shadow:inset 0 0 5px #888;box-shadow:inset 0 0 5px #888;z-index:1}.fixed{position:fixed}.ip_log tr td:nth-child(2),.ip_log tr th:nth-child(2){word-break:break-word;max-width:250px}.ip_log tr td:nth-child(3),.ip_log tr th:nth-child(3){word-break:break-word;max-width:150px}.ip_log tr td:nth-child(4),.ip_log tr th:nth-child(4){word-break:break-word;max-width:150px}html,body{height:100%}body{padding-top:100px}.wrapper{padding-bottom:50px}.container:not(.container-solid){max-width:960px}h1,h2,h3,h4{word-wrap:break-word}.info{overflow:auto}.dl-horizontal dd,dt,.tooltip-inner{word-wrap:break-word}.disabled{cursor:default !important}.app-list{position:relative;text-align:center;padding-top:100px}.app-list a{width:210px;font-size:24px;margin:0 20px;display:inline-block;text-align:center;opacity:1;border:1px solid #fff;color:#fff;opacity:1}.app-list a span{display:inline-block;height:100%;line-height:100%;padding:12px 10px}.app-list a:hover,.app-list a:focus{text-decoration:none;opacity:0.85}.app-list a .snf-font-remove{display:inline}.app-list a.disabled{background:transparent !important;border-color:#818181 !important;color:#818181 !important}.app-list a.disabled:hover,.app-list a.disabled:focus{cursor:default;opacity:1}.app-list a.disabled:hover span,.app-list a.disabled:focus span{color:#818181 !important}.app-list a span{border:1px solid transparent;width:100%}.app-list a:hover span,.app-list a:focus span{border-color:#fff}.app-list a.disabled{@inlcude disabled;;color:#818181}.app-list a.disabled:hover span,.app-list a.disabled:focus span{border-color:transparent}.app-list a.disabled{border-color:#a7a7a7;color:gray}.app-list a.disabled:hover span,.app-list a.disabled:focus span{border-color:transparent}.nav-simple{padding:20px;border-bottom:1px solid #fff}.nav-simple .header{float:left;line-height:40px;font-size:26px}.nav-simple .header img{max-height:50px}.nav-simple .login-info{float:right;position:relative;line-height:40px;font-size:16px}.nav-simple .login-info .has-dropdown{display:inline;position:relative}.nav-simple .login-info .has-dropdown:hover>a,.nav-simple .login-info .has-dropdown:focus>a{background:#4d4d4d}.nav-simple .login-info .has-dropdown>a{color:#fff;display:inline-block;padding:0 10px}.nav-simple .login-info .dropdown-menu{left:auto;right:0;top:27px}.navbar-default{border:0 none;border-bottom:1px solid transparent;z-index:1040;margin:0 auto}.navbar-default .container-fluid{padding:0}.navbar-default .home-icon{padding:0;height:50px;width:50px;text-align:center;line-height:50px;font-size:2px;background:#00a551}.navbar-default .home-icon img{max-height:50px}.sub-nav{top:50px;min-height:inherit}.sub-nav .nav>li>a{padding-top:8px;padding-bottom:8px}@media (max-width: 768px){.sub-nav{display:none}}.dropdown-menu{overflow-y:auto}.nav .has-dropdown:hover>ul.dropdown-menu,.nav-simple .has-dropdown:hover>ul.dropdown-menu{display:block}svg>text:last-child{display:none}.has-dropdown .arrow{margin-left:6px;vertical-align:middle}.hidden-row{display:none}.with-shift *::selection{background-color:transparent}.with-shift *::-moz-selection{background:transparent}.tab-content{background:#4e4e4e;color:#fff;padding:20px;border:0 none}.tab-content .well{margin-bottom:0}.selection-indicator{cursor:pointer;padding:6px 12px 6px 6px}.notify{padding:30px 10px 15px;width:100%;position:fixed;bottom:0;background:#fff;color:#303030}.notify .container>*:not(:last-child){margin-bottom:16px}.notify .remove-icon{color:transparent;margin-left:20px;font-weight:bold}.notify .container>*:hover .remove-icon{color:#d9534f}.notify .state-icon{margin-right:10px}.notify .success{color:#449d44}.notify .error{color:#d9534f}.notify .pending{color:#f0ad4e}.notify .warning,.notify .no-notifications{font-style:italic;font-weight:bold;display:inline-block;text-align:right}.notify .warning>.wrap,.notify .no-notifications>.wrap{display:block;padding-right:4px}.notify .warning a:hover,.notify .no-notifications a:hover{cursor:pointer}.notify .close-notify{position:absolute;right:20px;top:20px;color:#303030}.notify .close-notify:hover,.notify .close-notify:focus{color:inherit}.notify .dl-horizontal{margin-left:21px}.notify .dl-horizontal dt{width:80px;vertical-align:top;text-align:left}.notify .dl-horizontal dt span{font-size:20px;vertical-align:text-bottom;margin-right:10px}.notify .dl-horizontal dd{margin-left:80px}.lowercase{text-transform:lowercase}.shortcuts-btn .book-icon{padding-right:2px;vertical-align:sub;font-size:17px}body .shortcuts dt{width:119px;margin-bottom:12px}body .shortcuts dd{margin-left:139px}body .shortcuts .key{padding:2px 9px;font-style:normal;font-weight:bold;border:1px solid #ddd;background:#f5f5f5;border-radius:6px}.filters-examples dt{font-weight:normal;margin-bottom:0}.filters-examples dd{margin-bottom:12px}.filters-examples dd .highlight{background:#f5f5f5;padding:2px 6px;border-bottom:1px solid #ddd}.filters-examples dd.divider{margin-bottom:8px;border-bottom:1px solid #ddd}.notes dt{width:50px}.notes dd{margin-left:60px}.popover{z-index:1999;max-width:none;color:#303030;margin-bottom:20px}.popover h2{text-align:center;font-size:1.3em;font-weight:bold;margin-top:0}.popover h3{font-size:1.2em;font-weight:bold}.popover h4{font-size:1.1em;font-weight:bold}.popover dt{margin-bottom:8px;overflow:visible}.popover .panel-default{border-color:transparent;box-shadow:none}.sign-out{text-align:right}.sign-out span{margin-right:10px;vertical-align:middle;font-size:18px}.stats section{margin-bottom:3em}.stats section h3{margin-bottom:1em}.stats section h3 span{margin-right:0.5em}.stats section .custom-btn{float:left;margin-right:32px}.stats section .custom-btn span{padding-left:0}.stats section .custom-btn .snf-download-full{padding-right:0;padding-left:8px}.stats section .spinner{display:none;float:left;padding:8px}.navbar-right .dropdown-menu,.login-info .dropdown-menu{min-width:0}@media (min-width: 1200px){.stick{position:fixed;top:100px;width:inherit}}.themes{position:fixed;left:10px;bottom:10px}.charts .info{overflow:hidden}.charts h3{text-align:center;margin-bottom:1em}.charts .c3-axis{fill:#fff}.charts .c3 path,.charts .c3 line{stroke:#fff}.charts .c3-legend-item text{fill:#fff}.charts .c3-tooltip{color:#222}.popover-content{max-width:800px} diff --git a/snf-admin-app/synnefo_admin/admin/static/min-css/screen.css b/snf-admin-app/synnefo_admin/admin/static/min-css/screen.css new file mode 100644 index 0000000000000000000000000000000000000000..bf0dca8532eebbc7a4d8d3fb33c473fb1991e71c --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/min-css/screen.css @@ -0,0 +1 @@ +html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font:inherit;font-size:100%;vertical-align:baseline}html{line-height:1}ol,ul{list-style:none}table{border-collapse:collapse;border-spacing:0}caption,th,td{text-align:left;font-weight:normal;vertical-align:middle}q,blockquote{quotes:none}q:before,q:after,blockquote:before,blockquote:after{content:"";content:none}a img{border:none}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section,summary{display:block} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/_bars-btns.scss b/snf-admin-app/synnefo_admin/admin/static/sass/_bars-btns.scss new file mode 100644 index 0000000000000000000000000000000000000000..0bde0ecf1a8c9d3d8a2bf29d6381a991b4a75388 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/_bars-btns.scss @@ -0,0 +1,431 @@ +$default-btn-padding: 8px; +// ====== BUTTONS ====== // +// All buttons imply a structure <a><span>button</span></a> + +/* +Disabled buttons are transparent with light gray border +and light gray font colors +*/ + +@mixin disabled() { + background: transparent!important; + border-color: $btn-link-disabled-color!important; + color: $btn-link-disabled-color!important; + &:hover, + &:focus{ + cursor: default; + opacity: 1; + span { + color: $btn-link-disabled-color!important; + } + } +} + +// All buttons extend default-btn +@mixin default-btn($padding: $default-btn-padding) { + display: inline-block; + text-align: center; + opacity: 1; + span { + display: inline-block; + height: 100%; + line-height: 100%; + padding: $padding; + } + &:hover, + &:focus{ + text-decoration: none; + opacity: 0.85; + } + // span[class^="snf-font"] { + // display: inline; + // } + + // temporary + .snf-font-remove { + display: inline; + } + &.disabled { + @include disabled; + } +} + + +// Transparent buttons with thin $color border that get thicker on hover +@mixin outline-btn($color:$btn-outline-color, $background:inherit, $padding:$default-btn-padding) { + @include default-btn($padding); + border: 1px solid $color; + color: $color; + span { + border: 1px solid transparent; + width: 100%; + } + &:hover, + &:focus{ + span { + border-color: $color; + } + } + &.disabled { + @inlcude disabled; + color: $btn-link-disabled-color; + &:hover, + &:focus { + span { + border-color: transparent; + } + } + } +} + +// Normal button with background-color and white font color +@mixin custom-btn($color:$default-btn-color, $padding:$default-btn-padding) { + @include default-btn($padding); + border: 1px solid $color; + color: $secondary-link-color; + background-color: $color; + span { + border: 1px solid transparent; + background: transparent; + } + &:hover, + &:focus{ + span { + color: $secondary-link-color; + } + } + &.disabled { + @include disabled; + } +} + +// Buttons with thick bottom border on hover +@mixin line-btn($color:$btn-line-bg, $padding:$default-btn-padding, $border:$btn-line-border ) { + @include default-btn($padding); + background-color: $color; + border-bottom: 2px solid $color; + color: $border; + &:hover, + &:focus{ + opacity:1; + border-bottom-color: $border; + color: $border; + } +} + +.line-btn { + @include line-btn(); +} + +.outline-btn { + @include outline-btn(); +} + +.custom-btn { + @include custom-btn(); + &[data-karma="neutral"] { + background-color: $blue-intense; + border-color: $blue-intense; + } + &[data-karma="good"] { + background-color: $green-intense; + border-color: $green-intense; + } + + &[data-karma="bad"] { + background-color: $orange-intense; + border-color: $orange-intense; + } + + &[data-caution="warning"][data-karma="good"], &[data-caution="warning"][data-karma="neutral"] { + background-color: $orange-intense; + border-color: $orange-intense; + } + + &[data-caution="dangerous"][data-karma="bad"], &[data-caution="dangerous"][data-karma="neutral"] { + background-color: $red-intense; + border-color: $red-intense; + } +} + +.search-btn { + @include line-btn(); + position: relative; + top: -2px; + margin-left: 20px; + span { + padding: 7px; + } + cursor: pointer; +} + +.search-mode-btn { + float: right; + line-height: 30px; + &:hover { + cursor: pointer; + } +} + +.instructions .line-btn { + padding: 8px 10px; + span { + padding: 0 4px; + } + &:hover { + .arrow { + font-weight: bold; + } + } + &.open:hover { + border-bottom-color: transparent; + } + .arrow { + vertical-align: middle; + } +} + +// ====== STUFF ====== // + + +/* Sidebar */ + +.sidebar { + margin: 0 30px 0 0; + width: $sidebar-width; + height: auto; + float: left; + .btn-group-vertical { + width: 100%; + } + @media (max-width: $screen-lg-min) { + width: auto; + margin: 20px auto; + float: none; + .btn-group-vertical { + a { + margin-right: 10px; + display: inline-block; + } + } + } +} + +/* +Positioning or customizing buttons +*/ + +.sidebar { + .custom-btn { + display: block; + margin: 0 0 1em; + span { + padding: 8px; + } + } +} + +body .custom-buttons { + float: left; + margin-right: 10px; + .line-btn { + margin-right: 1em; + } + .disabled { + display: none; + } + .snf-font-reload { + @extend .snf-refresh-outline; + } + .snf-font-remove { + @extend .snf-remove; + } +} + + + +/* +Extra-button is used to show total selected rows +*/ + +body .custom-buttons .extra-btn { + float: right; + margin-right: 0; + span { + display: inline-block; + } + .badge { + background: transparent; + line-height: 0.8; + display: inline; + padding: 0 5px 0 0; + font-weight: normal; + font-size: 1em; + &::before { + content: "(" + } + &::after { + content: ")" + } + } +} + +.show-hide-all { + float:right; + em { + font-style: normal; + } + &.line-btn { + padding: $default-btn-padding; + span { + display:inline; + } + } +} + +.actions-per-item { + .custom-btn { + margin: 10px 10px 10px 0; + } +} +.charts { + .chart { + display: none; + } + .sidebar { + a { + @include outline-btn(); + display: block; + margin: 20px auto; + } + a.active { + @include custom-btn(); + display: block; + } + @media (max-width: $screen-lg-min) { + a, a.active { + margin-right: 10px; + display: inline-block; + } + } + } +} + +.notify .reload-btn { + padding: 0 4px; + font-size: 18px; + vertical-align: middle; + cursor: pointer; +} + +/* Switch in filters */ + +.onoffswitch { + display: inline-block; + float: right; + position: relative; + width: 134px; + -webkit-user-select:none; + -moz-user-select:none; + -ms-user-select: none; +} +.onoffswitch-checkbox { + display: none; +} +.onoffswitch-label { + display: block; + overflow: hidden; + cursor: pointer; + /*border: 2px solid #F7EFEF;*/ + border-radius: 20px; +} +.onoffswitch-inner { + display: block; width: 200%; margin-left: -100%; + -moz-transition: margin 0.3s ease-in 0s; + -webkit-transition: margin 0.3s ease-in 0s; + -o-transition: margin 0.3s ease-in 0s; + transition: margin 0.3s ease-in 0s; +} +.onoffswitch-inner:before, .onoffswitch-inner:after { + display: block; + float: left; + width: 50%; + height: 30px; + padding: 0; + line-height: 30px; + font-size: 12px; + color: white; + font-family: Trebuchet, Arial, sans-serif; + font-weight: normal; + -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; +} +.onoffswitch-inner:before { + content: "Standard View"; + padding-left: 10px; + background-color: $btn-line-bg; + color: $btn-line-border; +} +.onoffswitch-inner:after { + content: "Compact View"; + padding-right: 10px; + background-color: $btn-line-bg; + color: $btn-line-border; + text-align: right; +} +.onoffswitch-switch { + display: block; + width: 19px; + margin: 6px; + background: $btn-line-border; + border: 2px solid #F7EFEF; + border-radius: 20px; + position: absolute; + top: 0; + bottom: 4px; + right: 103px; + -moz-transition: all 0.3s ease-in 0s; + -webkit-transition: all 0.3s ease-in 0s; + -o-transition: all 0.3s ease-in 0s; + transition: all 0.3s ease-in 0s; +} +.onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-inner { + margin-left: 0; +} +.onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-switch { + right: 0px; +} + +/* Clickable elements that change state */ + +/* These are icon-fonts. We insert in html two icons (one for each state) */ +/* The icon with the false state is hidden and only the correct one is displayed */ +/* Which state is the correct it comes from the class of a parent element */ + +li.active .snf-checkbox-unchecked, li.active .snf-radio-unchecked { + display: none; +} +li:not(.active) .snf-checkbox-checked, , li:not(.active) .snf-radio-checked { + display: none; +} + +table.dataTable tbody tr.selected .snf-checkbox-unchecked{ + display: none; +} + +table.dataTable tbody tr:not(.selected) .snf-checkbox-checked { + display: none; +} +.show-hide-all.open .snf-font-arrow-down { + display: none; +} +.show-hide-all:not(.open) .snf-font-arrow-up { + display: none; +} + +.instructions .line-btn.open .snf-angle-down { + display: none; +} + +.instructions .line-btn:not(.open) .snf-angle-up { + display: none; +} \ No newline at end of file diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/_bootstrap.scss b/snf-admin-app/synnefo_admin/admin/static/sass/_bootstrap.scss new file mode 100644 index 0000000000000000000000000000000000000000..be913b77b0430667792d6025db916ba15ceaedcb --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/_bootstrap.scss @@ -0,0 +1,48 @@ +// Core variables and mixins +@import "bootstrap/mixins"; + +// Reset +@import "bootstrap/normalize"; +@import "bootstrap/print"; + +// Core CSS +@import "bootstrap/scaffolding"; +@import "bootstrap/type"; +@import "bootstrap/code"; +@import "bootstrap/grid"; +@import "bootstrap/tables"; +@import "bootstrap/forms"; +@import "bootstrap/buttons"; + +// Components +@import "bootstrap/component-animations"; +//@import "bootstrap/glyphicons"; +@import "bootstrap/dropdowns"; +@import "bootstrap/button-groups"; +@import "bootstrap/input-groups"; +@import "bootstrap/navs"; +@import "bootstrap/navbar"; +//@import "bootstrap/breadcrumbs"; +//@import "bootstrap/pagination"; +@import "bootstrap/pager"; +@import "bootstrap/labels"; +@import "bootstrap/badges"; +//@import "bootstrap/jumbotron"; +//@import "bootstrap/thumbnails"; +@import "bootstrap/alerts"; +//@import "bootstrap/progress-bars"; +//@import "bootstrap/media"; +//@import "bootstrap/list-group"; +@import "bootstrap/panels"; +@import "bootstrap/wells"; +@import "bootstrap/close"; + +// Components w/ JavaScript +@import "bootstrap/modals"; +@import "bootstrap/tooltip"; +@import "bootstrap/popovers"; +//@import "bootstrap/carousel"; + +// Utility classes +@import "bootstrap/utilities"; +@import "bootstrap/responsive-utilities"; diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/_details.scss b/snf-admin-app/synnefo_admin/admin/static/sass/_details.scss new file mode 100644 index 0000000000000000000000000000000000000000..ffacd3b3c51077b0fae38c72765c04a136348a59 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/_details.scss @@ -0,0 +1,238 @@ +@import "icon-fonts"; +@import "bootstrap/mixins"; +.main { + margin:2em 0 5em; + h4 .title{ + font-size: 24px; + } + span[class^="snf-details"] { + float: left; + margin-right: 8px; + font-size: $details-title-height; + } + .lt{ + line-height: $details-title-height; + } + .rt { + padding-top:5px; + } + .actions-per-item { + padding: 0; + } +} + +.object-anchor {height:2px; } + +.object-details { + h4 { + font-size: 14px; + letter-spacing: 1px; + .lt{ + display: block; + float: left; + max-width: 60%; + word-wrap: break-word; + } + .rt { + padding-top: 5px; + display: block; + overflow: hidden; + } + .arrow { + position: relative; + padding: 0 8px; + &:hover, + &:focus{ + top: 2px; + cursor: pointer; + outline: 0 none; + } + } + .label{ + float: right; + margin-left: 15px; + margin-bottom: 10px; + &.important { + font-weight: bold; + } + } + em { + float: none; + &.os-info { + float: right; + position: relative; + bottom: 3px; + img { + height: 26px; // *** i think that is the size of all images temp use for preview + margin-right: 5px; + } + } + } + } + h3 { + font-size: 18px; + margin: 0 0 1em; + font-weight: 400; + line-height: $details-title-height; + em { + margin-left: 10px; + font-size: 14px; + display: inline-block; + @include text-overflow(); + max-width: 50%; + vertical-align: top; + } + span[class^="snf-details"] { + float: left; + margin-right: 8px; + font-size: $details-title-height - 10px; + height: $details-title-height; + line-height: $details-title-height; + } + .popover-dismiss { + display: inline-block; + @include circle(18px,$popover-dismiss-bg); + text-align: center; + font-weight: bold; + vertical-align: middle; + line-height: 18px; + font-size: 16px; + vertical-align: super; + cursor: pointer; + margin-left:10px; + color: $popover-dismiss-color; + &:hover, + &:focus{ + background: $popover-dismiss-bg-hover; + color: $popover-dismiss-color-hover; + } + } + .popover { + .popover-content { + font-size: 12px; + line-height: 130%; + } + } + } + .icon-link { + margin-right: 10px; + } + p { + margin: 10px 20px; + font-style: italic; + } + + .length { + margin-left: 6px; + border: 0 none; + font-style: italic; + &::before { + content: '( '; + } + &::after { + content: ' )'; + } + } + &>.object-details { + margin-left: -20px; + margin-right: -20px; + padding:12px 20px; + } +} + +.object-details-content .nav-tabs>li { + a { + opacity: 0.7; + } + &.active>a { + opacity: 1; + } + + &:not(.active)>a:hover, + &:not(.active)>a:focus{ + opacity: 1; + } +} + +.tab-pane { + overflow: auto; +} + +.parts-separator { + border-top: 2px solid $parts-separator-color; + padding-top: 1em; + h2 { + font-size: 24px; + margin-bottom: 2em; + padding-top: 1em; + em { + max-width: 50%; + display: inline; + @include text-overflow(); + vertical-align: top; + + } + } +} + +.part-two { + &>.object-details { + border-bottom: 2px solid $object-details-border; + background: $object-details-bg; + padding:14px 20px; + overflow-x: auto; + .object-details { + padding: 5px 20px; + &:hover, + &:focus{ + background: $object-details-row-hover-bg; + } + } + .custom-btn { + span { + padding: 5px; + } + } + } + .object-details-content { + display: none; + padding: 0 $details-title-height; + } + +} + + +.snf-details-volume { + @extend .snf-volume-full; +} + +.snf-details-vm { + @extend .snf-pc-full; +} +.snf-details-project { + @extend .snf-clipboard-h; +} +.snf-details-network { + @extend .snf-network-full; +} +.snf-details-quota { + @extend .snf-wallet-full; +} + +.snf-details-nic { + @extend .snf-nic-full; +} +.snf-details-user { + @extend .snf-user-full; +} + +.show-hide-all { + span.snf-font-arrow-up { + @extend .snf-arrow-up; + padding: 0; + } + span.snf-font-arrow-down { + @extend .snf-arrow-down; + padding: 0; + } +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/_extra.scss b/snf-admin-app/synnefo_admin/admin/static/sass/_extra.scss new file mode 100644 index 0000000000000000000000000000000000000000..f4cc72af0b9ef8cc02b5f171072369beb44abb80 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/_extra.scss @@ -0,0 +1,435 @@ + +/* Layout & general stuff */ + +html, body { + height: 100%; +} + +body { + padding-top: 2*$navbar-height; +} + +.wrapper { + padding-bottom: 50px; +} +/* +.container-solid{ + min-width: 1050px!important; +} +*/ +.container:not(.container-solid){ + max-width: 960px; +} + +h1,h2,h3,h4 { + word-wrap: break-word; +} + +.info { + overflow: auto; +} + +.dl-horizontal dd, dt, +.tooltip-inner{ + word-wrap: break-word; +} +.disabled { + cursor: default !important; +} + + +/* Home */ +.app-list { + position: relative; + text-align: center; + padding-top: 100px; + a { + width: 210px; + font-size: 24px; + margin: 0 20px; + @include outline-btn($padding:12px 10px); + opacity: 1; + &.disabled { + border-color: lighten($btn-link-disabled-color,15%); + color: darken(#fff, 50%); + &:hover, + &:focus{ + span { + border-color: transparent; + } + } + } + } +} + +.nav-simple { + padding: $nav-side-padding; + border-bottom: 1px solid $text-color; + .header { + float: left; + line-height: 40px; + font-size: 26px; + img { + max-height: 50px; + } + } + .login-info { + float: right; + position: relative; + line-height: 40px; + font-size: 16px; + .has-dropdown { + display: inline; + position: relative; + &:hover, + &:focus{ + &>a { + background: lighten($hover-nav-color, 10%); + } + } + &> a { + color: $text-color; + display: inline-block; + padding: 0 10px; + } + } + .dropdown-menu { + left: auto; + right: 0; + top: 27px; + } + } +} + +/* Navigation */ + +.navbar-default { + border:0 none; + border-bottom: 1px solid $navbar-default-border; + z-index: 1040; + margin: 0 auto; + .container-fluid { + padding: 0; + } + .home-icon { + padding: 0; + height: $navbar-height; + width: $navbar-height; + text-align: center; + line-height: $navbar-height; + font-size: 2px; + background: $navbar-default-brand-bg; + img { + max-height: $navbar-height; + } + } + +} + +.sub-nav { + top: $navbar-height; + min-height: inherit; + .nav { + >li>a { + padding-top: 8px; + padding-bottom: 8px; + } + } + @media (max-width: $grid-float-breakpoint) { + display: none; + } +} +.dropdown-menu { + overflow-y: auto; +} + +.nav .has-dropdown:hover > ul.dropdown-menu, +.nav-simple .has-dropdown:hover > ul.dropdown-menu { + display: block; +} + + + + + +/* More */ + +svg>text:last-child { + display: none; +} + +.has-dropdown .arrow { + margin-left: 6px; + vertical-align: middle; +} + +.hidden-row { + display: none; +} + +.with-shift *::selection { + background-color: transparent; +} + +.with-shift *::-moz-selection { + background: transparent; +} +.tab-content { + background: $tab-content-bg; + color: $tab-content-color; + padding: 20px; + border: 0 none; + .well { + margin-bottom: 0; + } +} + +.selection-indicator { + cursor: pointer; + padding: 6px 12px 6px 6px; +} + +/* Notification area */ + +.notify { + // display: none; + padding: 30px 10px 15px; + width: 100%; + position: fixed; + bottom: 0; + background: $notify-bg; + color: $notify-color; + .container>*:not(:last-child) { + margin-bottom: 16px; + } + .remove-icon { + color: transparent; + margin-left: 20px; + font-weight: bold; + } + .container>*:hover { + .remove-icon { + color: $brand-danger; + } + } + .state-icon { + margin-right: 10px; + } + .success { + color: darken($brand-success, 10%); + } + .error { + color: $brand-danger; + } + .pending { + color: $brand-warning; + } + .warning, .no-notifications { + font-style: italic; + font-weight: bold; + display: inline-block; + text-align: right; + &>.wrap { + display: block; + padding-right: 4px; + } + a:hover { + cursor: pointer; + } + } + .close-notify { + position: absolute; + right: 20px; + top: 20px; + color: $notify-close-color; + &:hover, + &:focus{ + color: inherit; + } + } + .dl-horizontal { + margin-left: 21px; + dt{ + width: 80px; + vertical-align: top; + text-align: left; + span { + font-size: 20px; + vertical-align: text-bottom; + margin-right: 10px; + } + } + dd { + margin-left: 80px; + } + } +} + +.lowercase { + text-transform:lowercase; +} + +.shortcuts-btn { + .book-icon { + padding-right: 2px; + vertical-align: sub; + font-size: 17px; + } +} + +body .shortcuts { + dt { + width: 119px; + margin-bottom: 12px; + } + dd { + margin-left: 139px; + } + .key { + padding: 2px 9px; + font-style: normal; + font-weight: bold; + border: 1px solid $gray-light-extra-2; + background: $gray-light-extra-3; + border-radius: 6px; + } +} +.filters-examples { + dt { + font-weight: normal; + margin-bottom: 0; + } + dd { + margin-bottom: 12px; + .highlight { + background: $gray-light-extra-3; + padding: 2px 6px; + border-bottom: 1px solid $gray-light-extra-2; + } + &.divider { + margin-bottom: 8px; + border-bottom: 1px solid $gray-light-extra-2; + } + } +} + +.notes { + dt { + width: 50px; + } + dd { + margin-left: 60px; + } +} + +.popover { + z-index: 1999; + max-width: none; + color: $popover-color; + margin-bottom: 20px; + h2 { + text-align: center; + font-size: 1.3em; + font-weight: bold; + margin-top: 0; + } + h3 { + font-size: 1.2em; + font-weight: bold; + } + h4 { + font-size: 1.1em; + font-weight: bold; + } + dt { + margin-bottom: 8px; + overflow: visible; + } + .panel-default { + border-color: transparent; + box-shadow: none; + } +} + +.sign-out { + text-align: right; + span { + margin-right: 10px; + vertical-align: middle; + font-size: 18px; + } +} + +.stats { + section { + margin-bottom: 3em; + h3 { + margin-bottom: 1em; + span { + margin-right: 0.5em; + } + } + .custom-btn { + float: left; + span { + padding-left: 0; + } + .snf-download-full { + padding-right: 0; + padding-left: 8px; + } + margin-right: 32px; + } + .spinner { + display: none; + float: left; + padding: 8px; + } + } +} + +.navbar-right, .login-info { + .dropdown-menu { + min-width: 0; + } +} + +.stick { + @media (min-width: $screen-lg-min) { + position: fixed; + top: 2* $navbar-height; + width: inherit; + } +} + + +.themes { + position: fixed; + left: 10px; + bottom: 10px; +} + +.charts { + .info { + overflow: hidden; + } + h3 { + text-align: center; + margin-bottom: 1em; + } + .c3-axis { + fill: $text-color; + } + .c3 path, .c3 line { + stroke: $text-color; + } + .c3-legend-item text { + fill: $text-color; + } + .c3-tooltip { + color: #222; + } +} +.popover-content { + max-width: 800px; +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/_filters.scss b/snf-admin-app/synnefo_admin/admin/static/sass/_filters.scss new file mode 100644 index 0000000000000000000000000000000000000000..de548a00fb4e152780b9ac8efcf440739447cef6 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/_filters.scss @@ -0,0 +1,215 @@ +$filter-margin: 10px; +$filter-height: 30px; + + +.filters-area { + margin-bottom: $navbar-height - $filter-margin; + margin-left: $sidebar-width + 30px; + @media (max-width: $screen-lg-min) { + margin: 0 $filter-margin $filter-margin 0; + } + &.no-margin-left { + margin-left: 0; + } + a:focus, input:focus { + outline: none; + } + .badge { + margin-left: 6px; + opacity: 0.9; + padding: 2px 9px; + } + ul.nav a { + padding-bottom: 10px; + } +} + +.filter { + height: $filter-height; + margin: 0 $filter-margin $filter-margin 0; + display: inline-block; + background: $filter-bg; + border: 1px solid $filter-border-color; + .form-group { + margin: 0; + height: $filter-height; + } + label, + .dropdown{ + height: $filter-height; + line-height: $filter-height; + border: 0 none; + padding: 0 10px; + color: $filter-font-color; + background: transparent; + font-weight: normal; + margin: 0; + &>a { + .selected-value { + margin-left: 4px; + } + .arrow { + font-weight: bold; + } + } + &.open { + a { + text-decoration: none; + color: $filter-font-color; + } + } + a { + color: $filter-font-color; + } + } + .dropdown-menu, .dropdown-list { + background: $filter-bg; + margin: 0; + width: auto; + > .active > a{ + background: $filter-active-bg; + } + > li:hover >a { + background: $filter-hover-bg; + color: $filter-hover-color; + } + a { + span { + margin-right: 6px; + } + padding-left: 12px; + padding-right: 12px; + } + } + input { + border: 0 none; + background: transparent; + height: $filter-height; + line-height: $filter-height; + padding: 0 5px; + font-weight: normal; + color: $filter-font-color; + } + .dropdown-list > li > a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 1.42857; + color: #303030; // ? + white-space: nowrap; + } +} + +.input-with-btn { + border-width: 0px; + background-color: transparent; + display: inline; + input { + @media screen and (min-width: 400px) { + width: 200px; + } + @media screen and (min-width: 600px) { + width: 300px; + } + @media screen and (min-width: 800px) { + width: 500px; + } + @media screen and (min-width: 1000px) { + width: 700px; + } + } + .form-group { + display: inline-block; + background: $filter-bg; + border: 1px solid $filter-border-color; + margin-bottom: 0.6em; + } + .filter-error { + word-wrap: break-word; + } + .error-sign { + display: block; + opacity: 0; + position: static; + display: inline-block; + margin-right: 6px; + margin-left: 10px; + vertical-align: bottom; + } + + .instructions { + margin-top: 0.6em; + * { + color: $btn-line-border; + } + .content-area { + display: none; + background: $btn-line-bg; + padding: 12px 13px 18px; + dt { + width: 200px; + } + dd { + margin-left: 220px; + } + } + .clarifications { + font-style: italic; + } + } + +} + +.filter:not(.visible-filter):not(.visible-filter-fade) { + display: none; + opacity: 0; +} + +.visible-filter-fade { + opacity: 1; + transition: opacity 0.5s; +} + +.filters .filters-list { + border-radius: 15px; + background: $btn-line-bg; + border: 1px solid $filter-border-color-alt; + height: 28px; + &>a { + color: $btn-line-border; + line-height: 28px; + font-weight: bold; + padding: 8px 7px; + background: transparent; + } + .popover { + padding: 0; + } + .popover-content { + padding: 0; + } + .popover ul { + list-style: none; + padding: 5px 0px; + min-width: 160px; + li { + white-space: nowrap; + a { + color: $filter-font-color; + } + span { + margin-right: 10px; + } + } + .divider { + height: 1px; + margin: 9px 0; + overflow: hidden; + background-color: #e5e5e5; + } + } + .popover.bottom > .arrow:after { + border-bottom-color: $filter-bg; + } +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/_global.scss b/snf-admin-app/synnefo_admin/admin/static/sass/_global.scss new file mode 100644 index 0000000000000000000000000000000000000000..e8794af42a7ebd7b58318fc7c9b53982d6941bb1 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/_global.scss @@ -0,0 +1,7 @@ +@import "loaders"; +@import "bars-btns"; +@import "details"; +@import "filters"; +@import "modals"; +@import "tables"; +@import "extra"; diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/_loaders.scss b/snf-admin-app/synnefo_admin/admin/static/sass/_loaders.scss new file mode 100644 index 0000000000000000000000000000000000000000..ba659d00bf69cac7e68cd8c9a56862bf1b535404 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/_loaders.scss @@ -0,0 +1,42 @@ +.spinner { + text-align: center; +} + +.spinner > div { + width: 8px; + height: 8px; + background-color: $text-color; + + border-radius: 100%; + display: inline-block; + -webkit-animation: bouncedelay 1.4s infinite ease-in-out; + animation: bouncedelay 1.4s infinite ease-in-out; + /* Prevent first frame from flickering when animation starts */ + -webkit-animation-fill-mode: both; + animation-fill-mode: both; +} + +.spinner .bounce1 { + -webkit-animation-delay: -0.32s; + animation-delay: -0.32s; +} + +.spinner .bounce2 { + -webkit-animation-delay: -0.16s; + animation-delay: -0.16s; +} + +@-webkit-keyframes bouncedelay { + 0%, 80%, 100% { -webkit-transform: scale(0.0) } + 40% { -webkit-transform: scale(1.0) } +} + +@keyframes bouncedelay { + 0%, 80%, 100% { + transform: scale(0.0); + -webkit-transform: scale(0.0); + } 40% { + transform: scale(1.0); + -webkit-transform: scale(1.0); + } +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/_modals.scss b/snf-admin-app/synnefo_admin/admin/static/sass/_modals.scss new file mode 100644 index 0000000000000000000000000000000000000000..cb20efadaac679d03c8f3d9ae2f4cb76477d64b0 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/_modals.scss @@ -0,0 +1,250 @@ +p.progress-area { + visibility: hidden; + +} + +.in-progress .modal-body { + background-color: $gray-light-extra-1; + p.progress-area { + visibility: visible; + } + +} + +.modal { + &[data-item="user"] { + &:not([data-type="contact"]) .table-selected td:nth-child(3){ + display: none; + } + } + + &#user-contact { + p { + margin-top: 18px; + position: relative; + } + } + p { + position: relative; + } + p>.error-sign { + top: 0; + } + + h3 { + margin-top: 0; + font-weight: bold + } + textarea { + resize: vertical; + } + + textarea, input { + width: 87%; + vertical-align: text-top; + padding: 4px 8px; + border: 1px solid $gray-lighter; + color: $modal-text-color; + &.body { + min-height: 160px; + } + } + + label { + // float: left; + margin-right: 6px; + width: 70px; + vertical-align: sub; + } + + .modal-body { + background-color: white; + + } + + .modal-footer { + margin-top: 0; + form { + display: inline; + } + .custom-btn:first-child { + float: left; + background-color: $gray-dark; + border-color: $gray-dark; + } + } + +} + +.modal { + .custom-btn { + color: white; + opacity: 0.9; + &:hover, + &:focus{ + opacity: 1; + } + } + &[data-karma="dark"] { + .elem { + color: $gray-light + } + } + &[data-karma="neutral"] { + .elem { + color: darken($blue-intense, 10%); + + } + } + &[data-karma="good"] { + .elem { + color: darken($green-intense, 10%); + + } + } + + + &[data-karma="bad"] { + .elem { + color: darken($orange-intense, 10%); + + } + } + + &[data-caution="warning"][data-karma="good"], &[data-caution="warning"][data-karma="neutral"] { + .elem { + color: darken($orange-intense, 10%); + + } + } + + &[data-caution="dangerous"][data-karma="bad"], &[data-caution="dangerous"][data-karma="neutral"] { + .elem { + color: darken($red-intense, 10%); + + } + } +} + +.custom-btn[data-karma="dark"] { + background-color: #222222; + border-color: transparent; +} +.modal { + em { + font-weight: bold; + font-style: normal; + } + .popover { + z-index: 2000; + dl { + color: black; + font-weight: normal; + dt { + width: 90px; + } + dd { + margin-left: 110px; + } + } + h2 { + font-size: 16px; + color: $gray-dark; + font-weight: bold; + text-align: center; + } + } + .popover-content { + min-width: 150px; + } + +} + +.modal-content { + padding: 20px; + color: $gray-dark; + .badge { + background-color: transparent; + } +} + + + +.instructions-icon { + color: $blue-intense; + font-size: 22px; + margin-left: 78px; + &:hover { + text-decoration: none; + } +} + +.extra-info { + margin-top: 10px; +} + +.error-sign { + color: red; + font-size: 20px; + margin-left: 10px; + position: absolute; + // right: 8px; + top: 6px; + display: none; + &:hover, + &:focus{ + color: red; + // color:#A80A0A; + text-decoration: none; + } +} + +.form-area { + position: relative; +} +.form-subject { + margin-bottom: 15px; +} + +.toggle-more { + margin-top: -16px; + display: none; +} + +.modal { + .table-selected { + th,td { + word-break: break-word; + } + td:last-child { + .wrap { + padding-right: 36px; + } + } + tr:nth-child(2n){ + background: darken(#fff, 5%); + } + tr { + a { + font-weight: bold; + } + } + tr:hover, + tr:focus{ + background: $gray-lighter; + a { + color: red; + } + } + + .remove { + position: absolute; + right: 14px; + color: transparent; + &:hover{ + cursor: pointer; + text-decoration: none; + } + } + } +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/_palette1.scss b/snf-admin-app/synnefo_admin/admin/static/sass/_palette1.scss new file mode 100644 index 0000000000000000000000000000000000000000..bb93860977e0f1079fb3d35a108edb376c7daa1a --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/_palette1.scss @@ -0,0 +1,12 @@ +$theme-blue: #005B9A; +$theme-ciel: #00ADF1; +$theme-tyrquoise: #16C0B2; +$theme-yellow: #FFA914; +$theme-red: #EE5161; +$theme-gray-light: #f5f5f5; +$theme-black: #222; +$theme-gray-medium: #ccc; +$theme-gray-dark: #444; + + +$theme-border-light: lighten($theme-gray-medium,10%); diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/_settings.scss b/snf-admin-app/synnefo_admin/admin/static/sass/_settings.scss new file mode 100644 index 0000000000000000000000000000000000000000..558632394d0bf3aab3d90924e9225fd567d05ec9 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/_settings.scss @@ -0,0 +1,60 @@ +// Palette + +$snf_gray-dark: #303030; +$snf_gray-light: #4e4e4e; +$ciel: #4d99d8; +$blue-light: #B2CAE9; +$blue-light-extra: #AFC6D6; +$blue-intense: #3C96E0; +$gray-light-extra-1: lighten($snf_gray-light, 20%); +$gray-light-extra-2: #dddddd; +$gray-light-extra-3: #f5f5f5; // whitesmoke +$green-intense: #00a551; +$green-intense-extra: darken($green-intense, 10%); +$red-intense: #E42A48; +$orange-intense: #D2881F; +$almost-white: #eeeeee; +$total-black: #222; + +// Dimensions + +$nav-side-padding: 20px; +$sidebar-width: 110px; +$datatabled-actions-height: 35px; +$details-title-height: 35px; + + +@mixin circle($width, $color) { + width: $width; + height: $width; + background: $color; + -webkit-border-radius: $width/2; + -moz-border-radius: $width/2; + border-radius: $width/2; +} + +@mixin tdWrap($i, $width){ + tr { + td:nth-child(#{$i}), th:nth-child(#{$i}) { + word-break: break-word; + max-width: $width; + } + } +} +$theme-blue: #005B9A; +$theme-ciel: #00ADF1; +$theme-tyrquoise: #16C0B2; +$theme-yellow: #FFA914; +$theme-red: #EE5161; +$theme-gray-light: #ECECEC; +$theme-black: #222; +$theme-gray-medium: #ccc; +$theme-gray-dark: #444; + + +$theme-border-light: lighten($theme-gray-medium,8%); + +@import "bootstrap/variables"; +//@import "theme1"; + + diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/_tables.scss b/snf-admin-app/synnefo_admin/admin/static/sass/_tables.scss new file mode 100644 index 0000000000000000000000000000000000000000..750e785204a5eb524d54f65bc3d2d1f3392cb8cc --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/_tables.scss @@ -0,0 +1,213 @@ +table thead th { + white-space: nowrap; +} + +table td, +table th { + vertical-align: top; +} + +table .wrap { + position: relative; +} + +.table-items { + .snf-search { + opacity: 0.7; + font-size: 15px; + &:hover, + &:focus{ + opacity: 1; + } + } + .login-method { + padding: 2px 16px 2px 0px; + text-align: center; + } + + th .badge { + margin: 0 2px 0 4px; + display: inline; + padding-top: 2px; + } + td { + padding:8px 6px 0 6px; + } +} + + + +.table-selected-main:not(.table-selected) td:last-child, +.table-items:not(.table-selected) td:last-child{ // this is meant for summary td + max-width: 60px; + min-width: 60px; + padding: 8px 5px; + .details-link:hover { + text-decoration: none; + } + .summary-expand { + position: relative; + z-index: 10; + float: right; + padding-left: 8px; + padding-right: 8px; + background-color: $link-color; + color: #fff; + &:hover, + &:focus{ + text-decoration: none; + background-color: $link-hover-color; + } + } + dl { + z-index: 0; + position: relative; + padding: 8px; + display: none; + margin: 0; + } +} + + +.table-items .headerSortUp span.caret { + border-top: 0; + border-bottom: 4px solid; +} + +#table-items-selected_filter, +#table-items-total_filter{ + label { + color: $text-color; + } + input { + color: $filter-font-color; + background: $filter-bg; + border: 1px solid $filter-border-color; + padding: 3px 5px; + &:focus { + outline: 0 none; + } + } +} + +#table-items-selected_wrapper { + padding: 10px; + border: 1px solid $table-selected-border; + margin-bottom: 20px; + display: none; +} + +// datatables + + +div.dataTables_length { + padding-left: 2em; + padding-top: 0.55em; + select { + width: 55px; + display: inline-block; + margin-left: 4px; + vertical-align: baseline; + color: #222; + } +} + +table.dataTable tbody tr { + background-color: inherit; + &.even { + background-color: $table-zebra-row-bg; + } +} + +table.dataTable thead th, +table.dataTable thead td { + border-bottom: 1px solid white; + border-top: 1px solid $table-datatable-border-color; +} +table.dataTable tbody tr:hover { + background-color: $table-row-hover; +} +table.dataTable tbody tr.selected { + color: $table-selected-row-color; + background-color: $table-selected-row-bg; +} + +html body .dataTables_wrapper { + label { + font-weight: normal; + } + table { + th.sorting, th.sorting_asc, th.sorting_desc{ + background-position: center left; + padding-left: 22px; + } + } +} + + +.dataTables_wrapper .dataTables_length, +.dataTables_wrapper .dataTables_info, +.dataTables_wrapper .dataTables_paginate { + padding-top: 0; + margin-bottom: 0.5em; + color: $text-color; + line-height: $datatabled-actions-height; +} +table.dataTable.no-footer { + border-bottom: 1px solid $almost-white; + margin: 2em 0; +} + +.dataTables_wrapper .dataTables_paginate .paginate_button { + color: $text-color!important; + padding: 0 1em; +} + +.container .dataTables_wrapper .dataTables_paginate .paginate_button:hover, +.container .dataTables_wrapper .dataTables_paginate .paginate_button:focus, +{ + background: transparent; + border-color: $text-color; + color: $text-color!important; +} + +.container .dataTables_wrapper .dataTables_paginate .paginate_button.disabled { + border-color: transparent; + color: $btn-link-disabled-color!important; + &:hover, + &:focus, + &:active { + color: $btn-link-disabled-color!important; + } +} + +.dataTables_wrapper .dataTables_paginate .paginate_button.current, +.dataTables_wrapper .dataTables_paginate .paginate_button.current:focus, +.dataTables_wrapper .dataTables_paginate .paginate_button.current:hover { + background: $table-paginate-current-bg; + color: $table-paginate-current-color!important; + border: transparent; +} + +.dataTables_wrapper > .custom-buttons { + margin-bottom: 1em; + width: 100%; +} + +.dataTables_wrapper .dataTables_processing { + background: $table-processing-bg; + color: $table-processing-color; + padding: 5px 10px; + @include box-shadow(inset 0 0 5px #888); + z-index: 1; +} + +.fixed { + position: fixed; +} + +.ip_log { + @include tdWrap(2, 250px); + @include tdWrap(3, 150px); + @include tdWrap(4, 150px); +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/_theme-light.scss b/snf-admin-app/synnefo_admin/admin/static/sass/_theme-light.scss new file mode 100644 index 0000000000000000000000000000000000000000..7233057fcf27cca5ea30bfe0a30cb3bbea70bcc1 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/_theme-light.scss @@ -0,0 +1,908 @@ +// a flag to toggle asset pipeline / compass integration +// defaults to true if twbs-font-path function is present (no function => twbs-font-path('') parsed as string == right side) +// in Sass 3.3 this can be improved with: function-exists(twbs-font-path) +$bootstrap-sass-asset-helper: (twbs-font-path("") != unquote('twbs-font-path("")')) !default; +// +// Variables +// -------------------------------------------------- + + +//== Colors +// +//## Gray and brand colors for use across Bootstrap. + +$synnefo-green: #00a551; +$gray-darker: lighten(#000, 13.5%) !default; // #222 +$gray-dark: $snf_gray-dark; +$gray: lighten(#000, 33.5%) !default; // #555 +$gray-light: $snf_gray-light; +$gray-lighter: lighten(#000, 85%) !default; // #eee + +$brand-primary: #fff; +$brand-success: #5cb85c !default; +$brand-info: #5bc0de !default; +$brand-warning: #f0ad4e !default; +$brand-danger: #d9534f !default; + + +$primary-color: $total-black; +$secondary-color: $snf_gray-dark; +//== Scaffolding +// +// ## Settings for some of the most global styles. + +//** Background color for `<body>`. +$body-bg: #fff; +//** Global text color on `<body>`. +$text-color: $theme-black; +$reverse-text-color: #fff; + +//** Global textual link color. +$link-color: $theme-blue; +$link-hover-color: $theme-red; + +// ----- EXTRA COLOR-RELATED SETTINGS + +$hover-nav-color: darken($theme-gray-light,3%); + + +$secondary-link-color: white; + +// Î’utton colors +$default-btn-color: $blue-intense; +$bad-karma-color: $red-intense; +$neutral-karma-color: $orange-intense; +$good-karma-color: $green-intense; + +$btn-outline-color: $total-black; +$btn-line-bg: $theme-border-light; +$btn-line-border: $total-black; + +// Tables +$table-selected-row-bg: $theme-gray-medium; +$table-selected-row-color: $text-color; +$table-zebra-row-bg: $theme-gray-light; +$table-row-hover: lighten($theme-gray-medium,8%); +$table-datatable-border-color: $theme-border-light; +$table-processing-bg: $theme-yellow; +$table-processing-color: $reverse-text-color; +$table-paginate-current-bg: $theme-gray-medium; +$table-paginate-current-color: $text-color; +$table-selected-border: $theme-border-light; + + +// Filters +$filter-bg: $theme-gray-light; +$filter-font-color: $text-color; +$filter-active-bg: darken($filter-bg,10%); +$filter-hover-bg: darken($filter-bg,5%); +$filter-hover-color: inherit; +$filter-border-color: $theme-gray-medium; +$filter-border-color-alt: $theme-gray-medium; + + +// Tabs +$tab-content-bg: lighten($theme-gray-medium,5%); +$tab-content-color: $text-color; +$tab-border: $theme-border-light; + + +$object-details-bg: $theme-gray-light; +$object-details-border: $theme-border-light; + +$object-details-row-hover-bg: darken($object-details-bg,4%); + +// Popover dismiss icon +$popover-dismiss-bg: $theme-gray-medium; +$popover-dismiss-color: lighten($popover-dismiss-bg,20%); +$popover-dismiss-bg-hover: darken($popover-dismiss-bg,10%); +$popover-dismiss-color-hover: $almost-white; + +$popover-color: $text-color; +$modal-text-color: $text-color; + +// Notification area +$notify-bg: $theme-gray-dark; +$notify-color: $reverse-text-color; +$notify-close-color: #fff; + +$parts-separator-color: $theme-border-light; +// ----- END OF EXTRA COLOR-RELATED SETIINGS + + +//== Typography +// +//## Font, line-height, and color for body text, headings, and more. + +$font-family-sans-serif: 'Open Sans', sans-serif; +$font-family-serif: Georgia, "Times New Roman", Times, serif !default; +//** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`. +$font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace !default; +$font-family-base: $font-family-sans-serif !default; + +$font-size-base: 14px !default; +$font-size-large: ceil(($font-size-base * 1.25)) !default; // ~18px +$font-size-small: ceil(($font-size-base * 0.85)) !default; // ~12px + +$font-size-h1: floor(($font-size-base * 2.6)) !default; // ~36px +$font-size-h2: floor(($font-size-base * 2.15)) !default; // ~30px +$font-size-h3: ceil(($font-size-base * 1.7)) !default; // ~24px +$font-size-h4: ceil(($font-size-base * 1.25)) !default; // ~18px +$font-size-h5: $font-size-base !default; +$font-size-h6: ceil(($font-size-base * 0.85)) !default; // ~12px + +//** Unit-less `line-height` for use in components like buttons. +$line-height-base: 1.428571429 !default; // 20/14 +//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc. +$line-height-computed: floor(($font-size-base * $line-height-base)) !default; // ~20px + +//** By default, this inherits from the `<body>`. +$headings-font-family: inherit !default; +$headings-font-weight: 500 !default; +$headings-line-height: 1.1 !default; +$headings-color: inherit !default; + + +//-- Iconography +// +//## Specify custom locations of the include Glyphicons icon font. Useful for those including Bootstrap via Bower. + +$icon-font-path: "bootstrap/" !default; +$icon-font-name: "glyphicons-halflings-regular" !default; +$icon-font-svg-id: "glyphicons_halflingsregular" !default; + +//== Components +// +//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start). + +$padding-base-vertical: 6px !default; +$padding-base-horizontal: 12px !default; + +$padding-large-vertical: 10px !default; +$padding-large-horizontal: 16px !default; + +$padding-small-vertical: 5px !default; +$padding-small-horizontal: 10px !default; + +$padding-xs-vertical: 1px !default; +$padding-xs-horizontal: 5px !default; + +$line-height-large: 1.33 !default; +$line-height-small: 1.5 !default; + +$border-radius-base: 0; +$border-radius-large: 6px !default; +$border-radius-small: 3px !default; + +//** Global color for active items (e.g., navs or dropdowns). +$component-active-color: #fff !default; +//** Global background color for active items (e.g., navs or dropdowns). +$component-active-bg: $brand-primary !default; + +//** Width of the `border` for generating carets that indicator dropdowns. +$caret-width-base: 4px !default; +//** Carets increase slightly in size for larger components. +$caret-width-large: 5px !default; + + +//== Tables +// +//## Customizes the `.table` component with basic values, each used across all table variations. + +//** Padding for `<th>`s and `<td>`s. +$table-cell-padding: 10px; +//** Padding for cells in `.table-condensed`. +$table-condensed-cell-padding: 5px !default; + +//** Default background color used for all tables. +$table-bg: transparent !default; +//** Background color used for `.table-striped`. +$table-bg-accent: #f9f9f9 !default; +//** Background color used for `.table-hover`. +$table-bg-hover: #f5f5f5 !default; +$table-bg-active: $table-bg-hover !default; + +//** Border color for table and cell borders. +$table-border-color: $theme-gray-medium; + + + +//== Buttons +// +//## For each of Bootstrap's buttons, define text, background and border color. + +$btn-font-weight: normal !default; + +$btn-default-color: #333 !default; +$btn-default-bg: #fff !default; +$btn-default-border: #ccc !default; + +$btn-primary-color: #fff !default; +$btn-primary-bg: $brand-primary !default; +$btn-primary-border: darken($btn-primary-bg, 5%) !default; + +$btn-success-color: #fff !default; +$btn-success-bg: $brand-success !default; +$btn-success-border: darken($btn-success-bg, 5%) !default; + +$btn-info-color: #fff !default; +$btn-info-bg: $brand-info !default; +$btn-info-border: darken($btn-info-bg, 5%) !default; + +$btn-warning-color: #fff !default; +$btn-warning-bg: $brand-warning !default; +$btn-warning-border: darken($btn-warning-bg, 5%) !default; + +$btn-danger-color: #fff !default; +$btn-danger-bg: $brand-danger !default; +$btn-danger-border: darken($btn-danger-bg, 5%) !default; + +$btn-link-disabled-color: lighten($snf_gray-light,20%); + + + +//== Forms +// +//## + +//** `<input>` background color +$input-bg: #fff !default; +//** `<input disabled>` background color +$input-bg-disabled: $gray-lighter !default; + +//** Text color for `<input>`s +$input-color: $gray !default; +//** `<input>` border color +$input-border: #ccc !default; +//** `<input>` border radius +$input-border-radius: $border-radius-base !default; +//** Border color for inputs on focus +$input-border-focus: #66afe9 !default; + +//** Placeholder text color +$input-color-placeholder: $gray-light !default; + +//** Default `.form-control` height +$input-height-base: ($line-height-computed + ($padding-base-vertical * 2) + 2) !default; +//** Large `.form-control` height +$input-height-large: (ceil($font-size-large * $line-height-large) + ($padding-large-vertical * 2) + 2) !default; +//** Small `.form-control` height +$input-height-small: (floor($font-size-small * $line-height-small) + ($padding-small-vertical * 2) + 2) !default; + +$legend-color: $gray-dark !default; +$legend-border-color: #e5e5e5 !default; + +//** Background color for textual input addons +$input-group-addon-bg: $gray-lighter !default; +//** Border color for textual input addons +$input-group-addon-border-color: $input-border !default; + + +//== Dropdowns +// +//## Dropdown menu container and contents. + +//** Background for the dropdown menu. +$dropdown-bg: #fff !default; +//** Dropdown menu `border-color`. +$dropdown-border: rgba(0,0,0,.15) !default; +//** Dropdown menu `border-color` **for IE8**. +$dropdown-fallback-border: #ccc !default; +//** Divider color for between dropdown items. +$dropdown-divider-bg: #e5e5e5 !default; + +//** Dropdown link text color. +$dropdown-link-color: $gray-dark !default; +//** Hover color for dropdown links. +$dropdown-link-hover-color: $gray-dark; +//** Hover background for dropdown links. +$dropdown-link-hover-bg: $gray-lighter; + +//** Active dropdown menu item text color. +$dropdown-link-active-color: $component-active-color !default; +//** Active dropdown menu item background color. +$dropdown-link-active-bg: $component-active-bg !default; + +//** Disabled dropdown menu item background color. +$dropdown-link-disabled-color: $gray-light !default; + +//** Text color for headers within dropdown menus. +$dropdown-header-color: $gray-light !default; + +// Note: Deprecated $dropdown-caret-color as of v3.1.0 +$dropdown-caret-color: #000 !default; + + +//-- Z-index master list +// +// Warning: Avoid customizing these values. They're used for a bird's eye view +// of components dependent on the z-axis and are designed to all work together. +// +// Note: These variables are not generated into the Customizer. + +$zindex-navbar: 1000 !default; +$zindex-dropdown: 1000 !default; +$zindex-popover: 1010 !default; +$zindex-tooltip: 1030 !default; +$zindex-navbar-fixed: 1030 !default; +$zindex-modal-background: 1040 !default; +$zindex-modal: 1050 !default; + + +//== Media queries breakpoints +// +//## Define the breakpoints at which your layout will change, adapting to different screen sizes. + +// Extra small screen / phone +// Note: Deprecated $screen-xs and $screen-phone as of v3.0.1 +$screen-xs: 480px !default; +$screen-xs-min: $screen-xs !default; +$screen-phone: $screen-xs-min !default; + +// Small screen / tablet +// Note: Deprecated $screen-sm and $screen-tablet as of v3.0.1 +$screen-sm: 768px !default; +$screen-sm-min: $screen-sm !default; +$screen-tablet: $screen-sm-min !default; + +// Medium screen / desktop +// Note: Deprecated $screen-md and $screen-desktop as of v3.0.1 +$screen-md: 992px !default; +$screen-md-min: $screen-md !default; +$screen-desktop: $screen-md-min !default; + +// Large screen / wide desktop +// Note: Deprecated $screen-lg and $screen-lg-desktop as of v3.0.1 +$screen-lg: 1200px !default; +$screen-lg-min: $screen-lg !default; +$screen-lg-desktop: $screen-lg-min !default; + +// So media queries don't overlap when required, provide a maximum +$screen-xs-max: ($screen-sm-min - 1) !default; +$screen-sm-max: ($screen-md-min - 1) !default; +$screen-md-max: ($screen-lg-min - 1) !default; + + +//== Grid system +// +//## Define your custom responsive grid. + +//** Number of columns in the grid. +$grid-columns: 12 !default; +//** Padding between columns. Gets divided in half for the left and right. +$grid-gutter-width: 30px !default; +// Navbar collapse +//** Point at which the navbar becomes uncollapsed. +$grid-float-breakpoint: $screen-sm-min !default; +//** Point at which the navbar begins collapsing. +$grid-float-breakpoint-max: ($grid-float-breakpoint - 1) !default; + + +//== Container sizes +// +//## Define the maximum width of `.container` for different screen sizes. + +// Small screen / tablet +$container-tablet: ((780px + $grid-gutter-width)) !default; +//** For `$screen-sm-min` and up. +$container-sm: $container-tablet !default; + +// Medium screen / desktop +$container-desktop: ((980px + $grid-gutter-width)) !default; +//** For `$screen-md-min` and up. +$container-md: $container-desktop !default; + +// Large screen / wide desktop +$container-large-desktop: ((1140px + $grid-gutter-width)) !default; +//** For `$screen-lg-min` and up. +$container-lg: $container-large-desktop !default; + + +//== Navbar +// +//## + +// Basics of a navbar +$navbar-height: 50px; +$navbar-margin-bottom: $line-height-computed !default; +$navbar-border-radius: $border-radius-base !default; +$navbar-padding-horizontal: 0; +$navbar-padding-vertical: (($navbar-height - $line-height-computed) / 2) !default; +$navbar-collapse-max-height: 340px !default; + +$navbar-default-color: #777 !default; +$navbar-default-bg: $theme-gray-light; +$navbar-default-border: $theme-border-light; + +// Navbar links +$navbar-default-link-color: $text-color; +$navbar-default-link-hover-color: $text-color; +$navbar-default-link-hover-bg: $hover-nav-color; +$navbar-default-link-active-color: $reverse-text-color; +$navbar-default-link-active-bg: $theme-red; +$navbar-default-link-disabled-color: #ccc !default; +$navbar-default-link-disabled-bg: transparent !default; + +// Navbar brand label +$navbar-default-brand-color: $navbar-default-link-color !default; +$navbar-default-brand-hover-color: darken($navbar-default-brand-color, 10%) !default; +$navbar-default-brand-hover-bg: darken($synnefo-green,5%); +$navbar-default-brand-bg: $synnefo-green; + +// Navbar toggle +$navbar-default-toggle-hover-bg: #ddd !default; +$navbar-default-toggle-icon-bar-bg: #888 !default; +$navbar-default-toggle-border-color: #ddd !default; + + +// Inverted navbar +// Reset inverted navbar basics +$navbar-inverse-color: $text-color; +$navbar-inverse-bg: $theme-gray-medium; +$navbar-inverse-border: transparent; + +// Inverted navbar links +$navbar-inverse-link-color: $text-color; +$navbar-inverse-link-hover-color: $text-color; +$navbar-inverse-link-hover-bg: $gray-lighter; +$navbar-inverse-link-active-color: white; +$navbar-inverse-link-active-bg: darken($navbar-inverse-bg, 10%) !default; +$navbar-inverse-link-disabled-color: #444 !default; +$navbar-inverse-link-disabled-bg: transparent !default; + +// Inverted navbar brand label +$navbar-inverse-brand-color: $navbar-inverse-link-color !default; +$navbar-inverse-brand-hover-color: #fff !default; +$navbar-inverse-brand-hover-bg: transparent !default; + +// Inverted navbar toggle +$navbar-inverse-toggle-hover-bg: #333 !default; +$navbar-inverse-toggle-icon-bar-bg: #fff !default; +$navbar-inverse-toggle-border-color: #333 !default; + + +//== Navs +// +//## + +//=== Shared nav styles +$nav-link-padding: 10px 15px !default; +$nav-link-hover-bg: $gray-lighter !default; + +$nav-disabled-link-color: $gray-light !default; +$nav-disabled-link-hover-color: $gray-light !default; + +$nav-open-link-hover-color: #fff !default; + +//== Tabs +$nav-tabs-border-color: $theme-border-light; + +$nav-tabs-link-hover-border-color: inherit; + +$nav-tabs-active-link-hover-bg: $tab-content-bg; +$nav-tabs-active-link-hover-color: $tab-content-color; +$nav-tabs-active-link-hover-border-color: inherit; +$nav-tabs-link-color: $tab-content-color; + +$nav-tabs-justified-link-border-color: #ddd !default; +$nav-tabs-justified-active-link-border-color: $body-bg !default; + +//== Pills +$nav-pills-border-radius: $border-radius-base !default; +$nav-pills-active-link-hover-bg: $component-active-bg !default; +$nav-pills-active-link-hover-color: $component-active-color !default; + + +//== Pagination +// +//## + +$pagination-color: $link-color !default; +$pagination-bg: #fff !default; +$pagination-border: #ddd !default; + +$pagination-hover-color: $link-hover-color !default; +$pagination-hover-bg: $gray-lighter !default; +$pagination-hover-border: #ddd !default; + +$pagination-active-color: #fff !default; +$pagination-active-bg: $brand-primary !default; +$pagination-active-border: $brand-primary !default; + +$pagination-disabled-color: $gray-light !default; +$pagination-disabled-bg: #fff !default; +$pagination-disabled-border: #ddd !default; + + +//== Pager +// +//## + +$pager-bg: $pagination-bg !default; +$pager-border: $pagination-border !default; +$pager-border-radius: 15px !default; + +$pager-hover-bg: $pagination-hover-bg !default; + +$pager-active-bg: $pagination-active-bg !default; +$pager-active-color: $pagination-active-color !default; + +$pager-disabled-color: $pagination-disabled-color !default; + + +//== Jumbotron +// +//## + +$jumbotron-padding: 30px !default; +$jumbotron-color: inherit !default; +$jumbotron-bg: $gray-lighter !default; +$jumbotron-heading-color: inherit !default; +$jumbotron-font-size: ceil(($font-size-base * 1.5)) !default; + + +//== Form states and alerts +// +//## Define colors for form feedback states and, by default, alerts. + +$state-success-text: #3c763d !default; +$state-success-bg: #dff0d8 !default; +$state-success-border: darken(adjust-hue($state-success-bg, -10), 5%) !default; + +$state-info-text: #31708f !default; +$state-info-bg: #d9edf7 !default; +$state-info-border: darken(adjust-hue($state-info-bg, -10), 7%) !default; + +$state-warning-text: #8a6d3b !default; +$state-warning-bg: #fcf8e3 !default; +$state-warning-border: darken(adjust-hue($state-warning-bg, -10), 5%) !default; + +$state-danger-text: #a94442 !default; +$state-danger-bg: #f2dede !default; +$state-danger-border: darken(adjust-hue($state-danger-bg, -10), 5%) !default; + + +//== Tooltips +// +//## + +//** Tooltip max width +$tooltip-max-width: 200px !default; +//** Tooltip text color +$tooltip-color: #fff !default; +//** Tooltip background color +$tooltip-bg: #000 !default; +$tooltip-opacity: .9 !default; + +//** Tooltip arrow width +$tooltip-arrow-width: 5px !default; +//** Tooltip arrow color +$tooltip-arrow-color: $tooltip-bg !default; + + +//== Popovers +// +//## + +//** Popover body background color +$popover-bg: #fff !default; +//** Popover maximum width +$popover-max-width: 276px !default; +//** Popover border color +$popover-border-color: rgba(0,0,0,.2) !default; +//** Popover fallback border color +$popover-fallback-border-color: #ccc !default; + +//** Popover title background color +$popover-title-bg: darken($popover-bg, 3%) !default; + +//** Popover arrow width +$popover-arrow-width: 10px !default; +//** Popover arrow color +$popover-arrow-color: #fff !default; + +//** Popover outer arrow width +$popover-arrow-outer-width: ($popover-arrow-width + 1) !default; +//** Popover outer arrow color +$popover-arrow-outer-color: fadein($popover-border-color, 5%) !default; +//** Popover outer arrow fallback color +$popover-arrow-outer-fallback-color: darken($popover-fallback-border-color, 20%) !default; + + +//== Labels +// +//## + +//** Default label background color +$label-default-bg: $theme-gray-dark; +//** Primary label background color +$label-primary-bg: $brand-primary !default; +//** Success label background color +$label-success-bg: $brand-success !default; +//** Info label background color +$label-info-bg: $brand-info !default; +//** Warning label background color +$label-warning-bg: $brand-warning !default; +//** Danger label background color +$label-danger-bg: $brand-danger !default; + +//** Default label text color +$label-color: #fff; +//** Default text color of a linked label +$label-link-hover-color: #fff !default; + + +//== Modals +// +//## + +//** Padding applied to the modal body +$modal-inner-padding: 20px !default; + +//** Padding applied to the modal title +$modal-title-padding: 15px !default; +//** Modal title line-height +$modal-title-line-height: $line-height-base !default; + +//** Background color of modal content area +$modal-content-bg: #fff !default; +//** Modal content border color +$modal-content-border-color: rgba(0,0,0,.2) !default; +//** Modal content border color **for IE8** +$modal-content-fallback-border-color: #999 !default; + +//** Modal backdrop background color +$modal-backdrop-bg: #000 !default; +//** Modal backdrop opacity +$modal-backdrop-opacity: .5 !default; +//** Modal header border color +$modal-header-border-color: transparent; +//** Modal footer border color +$modal-footer-border-color: $modal-header-border-color !default; + +$modal-lg: 900px !default; +$modal-md: 760px !default; +$modal-sm: 300px !default; + + +//== Alerts +// +//## Define alert colors, border radius, and padding. + +$alert-padding: 15px !default; +$alert-border-radius: $border-radius-base !default; +$alert-link-font-weight: bold !default; + +$alert-success-bg: $state-success-bg !default; +$alert-success-text: $state-success-text !default; +$alert-success-border: $state-success-border !default; + +$alert-info-bg: $state-info-bg !default; +$alert-info-text: $state-info-text !default; +$alert-info-border: $state-info-border !default; + +$alert-warning-bg: $state-warning-bg !default; +$alert-warning-text: $state-warning-text !default; +$alert-warning-border: $state-warning-border !default; + +$alert-danger-bg: $state-danger-bg !default; +$alert-danger-text: $state-danger-text !default; +$alert-danger-border: $state-danger-border !default; + + +//== Progress bars +// +//## + +//** Background color of the whole progress component +$progress-bg: #f5f5f5 !default; +//** Progress bar text color +$progress-bar-color: #fff !default; + +//** Default progress bar color +$progress-bar-bg: $brand-primary !default; +//** Success progress bar color +$progress-bar-success-bg: $brand-success !default; +//** Warning progress bar color +$progress-bar-warning-bg: $brand-warning !default; +//** Danger progress bar color +$progress-bar-danger-bg: $brand-danger !default; +//** Info progress bar color +$progress-bar-info-bg: $brand-info !default; + + +//== List group +// +//## + +//** Background color on `.list-group-item` +$list-group-bg: #fff !default; +//** `.list-group-item` border color +$list-group-border: #ddd !default; +//** List group border radius +$list-group-border-radius: $border-radius-base !default; + +//** Background color of single list elements on hover +$list-group-hover-bg: #f5f5f5 !default; +//** Text color of active list elements +$list-group-active-color: $component-active-color !default; +//** Background color of active list elements +$list-group-active-bg: $component-active-bg !default; +//** Border color of active list elements +$list-group-active-border: $list-group-active-bg !default; +$list-group-active-text-color: lighten($list-group-active-bg, 40%) !default; + +$list-group-link-color: #555 !default; +$list-group-link-heading-color: #333 !default; + + +//== Panels +// +//## + +$panel-bg: #fff !default; +$panel-body-padding: 15px !default; +$panel-border-radius: $border-radius-base !default; + +//** Border color for elements within panels +$panel-inner-border: #ddd !default; +$panel-footer-bg: #f5f5f5 !default; + +$panel-default-text: $gray-dark !default; +$panel-default-border: #ddd !default; +$panel-default-heading-bg: #f5f5f5 !default; + +$panel-primary-text: #fff !default; +$panel-primary-border: $brand-primary !default; +$panel-primary-heading-bg: $brand-primary !default; + +$panel-success-text: $state-success-text !default; +$panel-success-border: $state-success-border !default; +$panel-success-heading-bg: $state-success-bg !default; + +$panel-info-text: $state-info-text !default; +$panel-info-border: $state-info-border !default; +$panel-info-heading-bg: $state-info-bg !default; + +$panel-warning-text: $state-warning-text !default; +$panel-warning-border: $state-warning-border !default; +$panel-warning-heading-bg: $state-warning-bg !default; + +$panel-danger-text: $state-danger-text !default; +$panel-danger-border: $state-danger-border !default; +$panel-danger-heading-bg: $state-danger-bg !default; + + +//== Thumbnails +// +//## + +//** Padding around the thumbnail image +$thumbnail-padding: 4px !default; +//** Thumbnail background color +$thumbnail-bg: $body-bg !default; +//** Thumbnail border color +$thumbnail-border: #ddd !default; +//** Thumbnail border radius +$thumbnail-border-radius: $border-radius-base !default; + +//** Custom text color for thumbnail captions +$thumbnail-caption-color: $text-color !default; +//** Padding around the thumbnail caption +$thumbnail-caption-padding: 9px !default; + + +//== Wells +// +//## + +$well-bg: inherit; +$well-border: inherit; + + +//== Badges +// +//## + +$badge-color: inherit; +//** Linked badge text color on hover +$badge-link-hover-color: inherit; +$badge-bg: $gray-light !default; + +//** Badge text color in active nav link +$badge-active-color: $link-color !default; +//** Badge background color in active nav link +$badge-active-bg: #fff !default; + +$badge-font-weight: bold !default; +$badge-line-height: 1 !default; +$badge-border-radius: 0; + + +//== Breadcrumbs +// +//## + +$breadcrumb-padding-vertical: 8px !default; +$breadcrumb-padding-horizontal: 15px !default; +//** Breadcrumb background color +$breadcrumb-bg: #f5f5f5 !default; +//** Breadcrumb text color +$breadcrumb-color: #ccc !default; +//** Text color of current page in the breadcrumb +$breadcrumb-active-color: $gray-light !default; +//** Textual separator for between breadcrumb elements +$breadcrumb-separator: "/" !default; + + +//== Carousel +// +//## + +$carousel-text-shadow: 0 1px 2px rgba(0,0,0,.6) !default; + +$carousel-control-color: #fff !default; +$carousel-control-width: 15% !default; +$carousel-control-opacity: .5 !default; +$carousel-control-font-size: 20px !default; + +$carousel-indicator-active-bg: #fff !default; +$carousel-indicator-border-color: #fff !default; + +$carousel-caption-color: #fff !default; + + +//== Close +// +//## + +$close-font-weight: bold !default; +$close-color: #000 !default; +$close-text-shadow: 0 1px 0 #fff !default; + + +//== Code +// +//## + +$code-color: #c7254e !default; +$code-bg: #f9f2f4 !default; + +$kbd-color: #fff !default; +$kbd-bg: #333 !default; + +$pre-bg: #f5f5f5 !default; +$pre-color: $gray-dark !default; +$pre-border-color: #ccc !default; +$pre-scrollable-max-height: 340px !default; + + +//== Type +// +//## + +//** Text muted color +$text-muted: $gray-light !default; +//** Abbreviations and acronyms border color +$abbr-border-color: $gray-light !default; +//** Headings small color +$headings-small-color: $gray-light !default; +//** Blockquote small color +$blockquote-small-color: $gray-light !default; +//** Blockquote font size +$blockquote-font-size: ($font-size-base * 1.25) !default; +//** Blockquote border color +$blockquote-border-color: $gray-lighter !default; +//** Page header border color +$page-header-border-color: $gray-lighter !default; + + +//== Miscellaneous +// +//## + +//** Horizontal line color. +$hr-border: $gray-lighter !default; + +//** Horizontal offset for forms and lists. +$component-offset-horizontal: 180px !default; diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_alerts.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_alerts.scss new file mode 100644 index 0000000000000000000000000000000000000000..4685ac3a9d4354248baada7a258a0cffc5a4237b --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_alerts.scss @@ -0,0 +1,67 @@ +// +// Alerts +// -------------------------------------------------- + + +// Base styles +// ------------------------- + +.alert { + padding: $alert-padding; + margin-bottom: $line-height-computed; + border: 1px solid transparent; + border-radius: $alert-border-radius; + + // Headings for larger alerts + h4 { + margin-top: 0; + // Specified for the h4 to prevent conflicts of changing $headings-color + color: inherit; + } + // Provide class for links that match alerts + .alert-link { + font-weight: $alert-link-font-weight; + } + + // Improve alignment and spacing of inner content + > p, + > ul { + margin-bottom: 0; + } + > p + p { + margin-top: 5px; + } +} + +// Dismissable alerts +// +// Expand the right padding and account for the close button's positioning. + +.alert-dismissable { + padding-right: ($alert-padding + 20); + + // Adjust close link position + .close { + position: relative; + top: -2px; + right: -21px; + color: inherit; + } +} + +// Alternate styles +// +// Generate contextual modifier classes for colorizing the alert. + +.alert-success { + @include alert-variant($alert-success-bg, $alert-success-border, $alert-success-text); +} +.alert-info { + @include alert-variant($alert-info-bg, $alert-info-border, $alert-info-text); +} +.alert-warning { + @include alert-variant($alert-warning-bg, $alert-warning-border, $alert-warning-text); +} +.alert-danger { + @include alert-variant($alert-danger-bg, $alert-danger-border, $alert-danger-text); +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_badges.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_badges.scss new file mode 100644 index 0000000000000000000000000000000000000000..4014a80bd4e50de0f1df578cdaf527018cfe3917 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_badges.scss @@ -0,0 +1,55 @@ +// +// Badges +// -------------------------------------------------- + + +// Base classes +.badge { + display: inline-block; + min-width: 10px; + padding: 3px 7px; + font-size: $font-size-small; + font-weight: $badge-font-weight; + color: $badge-color; + line-height: $badge-line-height; + vertical-align: baseline; + white-space: nowrap; + text-align: center; + background-color: $badge-bg; + border-radius: $badge-border-radius; + + // Empty badges collapse automatically (not available in IE8) + &:empty { + display: none; + } + + // Quick fix for badges in buttons + .btn & { + position: relative; + top: -1px; + } + .btn-xs & { + top: 0; + padding: 1px 5px; + } +} + +// Hover state, but only for links +a.badge { + &:hover, + &:focus { + color: $badge-link-hover-color; + text-decoration: none; + cursor: pointer; + } +} + +// Account for counters in navs +a.list-group-item.active > .badge, +.nav-pills > .active > a > .badge { + color: $badge-active-color; + background-color: $badge-active-bg; +} +.nav-pills > li > a > .badge { + margin-left: 3px; +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_breadcrumbs.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_breadcrumbs.scss new file mode 100644 index 0000000000000000000000000000000000000000..3641e333b8d3aa5b6faaea5b3a64c0f01f7c8a7c --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_breadcrumbs.scss @@ -0,0 +1,26 @@ +// +// Breadcrumbs +// -------------------------------------------------- + + +.breadcrumb { + padding: $breadcrumb-padding-vertical $breadcrumb-padding-horizontal; + margin-bottom: $line-height-computed; + list-style: none; + background-color: $breadcrumb-bg; + border-radius: $border-radius-base; + + > li { + display: inline-block; + + + li:before { + content: "#{$breadcrumb-separator}\00a0"; // Unicode space added since inline-block means non-collapsing white-space + padding: 0 5px; + color: $breadcrumb-color; + } + } + + > .active { + color: $breadcrumb-active-color; + } +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_button-groups.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_button-groups.scss new file mode 100644 index 0000000000000000000000000000000000000000..066b4d77d5517bb41fccc7329c4b2fb47fa82cae --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_button-groups.scss @@ -0,0 +1,226 @@ +// +// Button groups +// -------------------------------------------------- + +// Make the div behave like a button +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-block; + vertical-align: middle; // match .btn alignment given font-size hack above + > .btn { + position: relative; + float: left; + // Bring the "active" button to the front + &:hover, + &:focus, + &:active, + &.active { + z-index: 2; + } + &:focus { + // Remove focus outline when dropdown JS adds it after closing the menu + outline: none; + } + } +} + +// Prevent double borders when buttons are next to each other +.btn-group { + .btn + .btn, + .btn + .btn-group, + .btn-group + .btn, + .btn-group + .btn-group { + margin-left: -1px; + } +} + +// Optional: Group multiple button groups together for a toolbar +.btn-toolbar { + margin-left: -5px; // Offset the first child's margin + @include clearfix(); + + .btn-group, + .input-group { + float: left; + } + > .btn, + > .btn-group, + > .input-group { + margin-left: 5px; + } +} + +.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { + border-radius: 0; +} + +// Set corners individual because sometimes a single button can be in a .btn-group and we need :first-child and :last-child to both match +.btn-group > .btn:first-child { + margin-left: 0; + &:not(:last-child):not(.dropdown-toggle) { + @include border-right-radius(0); + } +} +// Need .dropdown-toggle since :last-child doesn't apply given a .dropdown-menu immediately after it +.btn-group > .btn:last-child:not(:first-child), +.btn-group > .dropdown-toggle:not(:first-child) { + @include border-left-radius(0); +} + +// Custom edits for including btn-groups within btn-groups (useful for including dropdown buttons within a btn-group) +.btn-group > .btn-group { + float: left; +} +.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group > .btn-group:first-child { + > .btn:last-child, + > .dropdown-toggle { + @include border-right-radius(0); + } +} +.btn-group > .btn-group:last-child > .btn:first-child { + @include border-left-radius(0); +} + +// On active and open, don't show outline +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} + + +// Sizing +// +// Remix the default button sizing classes into new ones for easier manipulation. + +.btn-group-xs > .btn { @extend .btn-xs; } +.btn-group-sm > .btn { @extend .btn-sm; } +.btn-group-lg > .btn { @extend .btn-lg; } + + +// Split button dropdowns +// ---------------------- + +// Give the line between buttons some depth +.btn-group > .btn + .dropdown-toggle { + padding-left: 8px; + padding-right: 8px; +} +.btn-group > .btn-lg + .dropdown-toggle { + padding-left: 12px; + padding-right: 12px; +} + +// The clickable button for toggling the menu +// Remove the gradient and set the same inset shadow as the :active state +.btn-group.open .dropdown-toggle { + @include box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); + + // Show no shadow for `.btn-link` since it has no other button styles. + &.btn-link { + @include box-shadow(none); + } +} + + +// Reposition the caret +.btn .caret { + margin-left: 0; +} +// Carets in other button sizes +.btn-lg .caret { + border-width: $caret-width-large $caret-width-large 0; + border-bottom-width: 0; +} +// Upside down carets for .dropup +.dropup .btn-lg .caret { + border-width: 0 $caret-width-large $caret-width-large; +} + + +// Vertical button groups +// ---------------------- + +.btn-group-vertical { + > .btn, + > .btn-group, + > .btn-group > .btn { + display: block; + float: none; + width: 100%; + max-width: 100%; + } + + // Clear floats so dropdown menus can be properly placed + > .btn-group { + @include clearfix(); + > .btn { + float: none; + } + } + + > .btn + .btn, + > .btn + .btn-group, + > .btn-group + .btn, + > .btn-group + .btn-group { + margin-top: -1px; + margin-left: 0; + } +} + +.btn-group-vertical > .btn { + &:not(:first-child):not(:last-child) { + border-radius: 0; + } + &:first-child:not(:last-child) { + border-top-right-radius: $border-radius-base; + @include border-bottom-radius(0); + } + &:last-child:not(:first-child) { + border-bottom-left-radius: $border-radius-base; + @include border-top-radius(0); + } +} +.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group-vertical > .btn-group:first-child:not(:last-child) { + > .btn:last-child, + > .dropdown-toggle { + @include border-bottom-radius(0); + } +} +.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { + @include border-top-radius(0); +} + + + +// Justified button groups +// ---------------------- + +.btn-group-justified { + display: table; + width: 100%; + table-layout: fixed; + border-collapse: separate; + > .btn, + > .btn-group { + float: none; + display: table-cell; + width: 1%; + } + > .btn-group .btn { + width: 100%; + } +} + + +// Checkbox and radio options +[data-toggle="buttons"] > .btn > input[type="radio"], +[data-toggle="buttons"] > .btn > input[type="checkbox"] { + display: none; +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_buttons.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_buttons.scss new file mode 100644 index 0000000000000000000000000000000000000000..28110b6519a0d1542f5e37d05f42cc040b378907 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_buttons.scss @@ -0,0 +1,159 @@ +// +// Buttons +// -------------------------------------------------- + + +// Base styles +// -------------------------------------------------- + +.btn { + display: inline-block; + margin-bottom: 0; // For input.btn + font-weight: $btn-font-weight; + text-align: center; + vertical-align: middle; + cursor: pointer; + background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 + border: 1px solid transparent; + white-space: nowrap; + @include button-size($padding-base-vertical, $padding-base-horizontal, $font-size-base, $line-height-base, $border-radius-base); + @include user-select(none); + + &, + &:active, + &.active { + &:focus { + @include tab-focus(); + } + } + + &:hover, + &:focus { + color: $btn-default-color; + text-decoration: none; + } + + &:active, + &.active { + outline: 0; + background-image: none; + @include box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); + } + + &.disabled, + &[disabled], + fieldset[disabled] & { + cursor: not-allowed; + pointer-events: none; // Future-proof disabling of clicks + @include opacity(.65); + @include box-shadow(none); + } +} + + +// Alternate buttons +// -------------------------------------------------- + +.btn-default { + @include button-variant($btn-default-color, $btn-default-bg, $btn-default-border); +} +.btn-primary { + @include button-variant($btn-primary-color, $btn-primary-bg, $btn-primary-border); +} +// Success appears as green +.btn-success { + @include button-variant($btn-success-color, $btn-success-bg, $btn-success-border); +} +// Info appears as blue-green +.btn-info { + @include button-variant($btn-info-color, $btn-info-bg, $btn-info-border); +} +// Warning appears as orange +.btn-warning { + @include button-variant($btn-warning-color, $btn-warning-bg, $btn-warning-border); +} +// Danger and error appear as red +.btn-danger { + @include button-variant($btn-danger-color, $btn-danger-bg, $btn-danger-border); +} + + +// Link buttons +// ------------------------- + +// Make a button look and behave like a link +.btn-link { + color: $link-color; + font-weight: normal; + cursor: pointer; + border-radius: 0; + + &, + &:active, + &[disabled], + fieldset[disabled] & { + background-color: transparent; + @include box-shadow(none); + } + &, + &:hover, + &:focus, + &:active { + border-color: transparent; + } + &:hover, + &:focus { + color: $link-hover-color; + text-decoration: underline; + background-color: transparent; + } + &[disabled], + fieldset[disabled] & { + &:hover, + &:focus { + color: $btn-link-disabled-color; + text-decoration: none; + } + } +} + + +// Button Sizes +// -------------------------------------------------- + +.btn-lg { + // line-height: ensure even-numbered height of button next to large input + @include button-size($padding-large-vertical, $padding-large-horizontal, $font-size-large, $line-height-large, $border-radius-large); +} +.btn-sm { + // line-height: ensure proper height of button next to small input + @include button-size($padding-small-vertical, $padding-small-horizontal, $font-size-small, $line-height-small, $border-radius-small); +} +.btn-xs { + @include button-size($padding-xs-vertical, $padding-xs-horizontal, $font-size-small, $line-height-small, $border-radius-small); +} + + +// Block button +// -------------------------------------------------- + +.btn-block { + display: block; + width: 100%; + padding-left: 0; + padding-right: 0; +} + +// Vertically space out multiple block buttons +.btn-block + .btn-block { + margin-top: 5px; +} + +// Specificity overrides +input[type="submit"], +input[type="reset"], +input[type="button"] { + &.btn-block { + width: 100%; + } +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_carousel.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_carousel.scss new file mode 100644 index 0000000000000000000000000000000000000000..d8f236487ca84b128c58a1987d62b7fb8a4024cc --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_carousel.scss @@ -0,0 +1,232 @@ +// +// Carousel +// -------------------------------------------------- + + +// Wrapper for the slide container and indicators +.carousel { + position: relative; +} + +.carousel-inner { + position: relative; + overflow: hidden; + width: 100%; + + > .item { + display: none; + position: relative; + @include transition(.6s ease-in-out left); + + // Account for jankitude on images + > img, + > a > img { + @include img-responsive(); + line-height: 1; + } + } + + > .active, + > .next, + > .prev { display: block; } + + > .active { + left: 0; + } + + > .next, + > .prev { + position: absolute; + top: 0; + width: 100%; + } + + > .next { + left: 100%; + } + > .prev { + left: -100%; + } + > .next.left, + > .prev.right { + left: 0; + } + + > .active.left { + left: -100%; + } + > .active.right { + left: 100%; + } + +} + +// Left/right controls for nav +// --------------------------- + +.carousel-control { + position: absolute; + top: 0; + left: 0; + bottom: 0; + width: $carousel-control-width; + @include opacity($carousel-control-opacity); + font-size: $carousel-control-font-size; + color: $carousel-control-color; + text-align: center; + text-shadow: $carousel-text-shadow; + // We can't have this transition here because WebKit cancels the carousel + // animation if you trip this while in the middle of another animation. + + // Set gradients for backgrounds + &.left { + @include gradient-horizontal($start-color: rgba(0,0,0,.5), $end-color: rgba(0,0,0,.0001)); + } + &.right { + left: auto; + right: 0; + @include gradient-horizontal($start-color: rgba(0,0,0,.0001), $end-color: rgba(0,0,0,.5)); + } + + // Hover/focus state + &:hover, + &:focus { + outline: none; + color: $carousel-control-color; + text-decoration: none; + @include opacity(.9); + } + + // Toggles + .icon-prev, + .icon-next, + .glyphicon-chevron-left, + .glyphicon-chevron-right { + position: absolute; + top: 50%; + z-index: 5; + display: inline-block; + } + .icon-prev, + .glyphicon-chevron-left { + left: 50%; + } + .icon-next, + .glyphicon-chevron-right { + right: 50%; + } + .icon-prev, + .icon-next { + width: 20px; + height: 20px; + margin-top: -10px; + margin-left: -10px; + font-family: serif; + } + + .icon-prev { + &:before { + content: '\2039';// SINGLE LEFT-POINTING ANGLE QUOTATION MARK (U+2039) + } + } + .icon-next { + &:before { + content: '\203a';// SINGLE RIGHT-POINTING ANGLE QUOTATION MARK (U+203A) + } + } +} + +// Optional indicator pips +// +// Add an unordered list with the following class and add a list item for each +// slide your carousel holds. + +.carousel-indicators { + position: absolute; + bottom: 10px; + left: 50%; + z-index: 15; + width: 60%; + margin-left: -30%; + padding-left: 0; + list-style: none; + text-align: center; + + li { + display: inline-block; + width: 10px; + height: 10px; + margin: 1px; + text-indent: -999px; + border: 1px solid $carousel-indicator-border-color; + border-radius: 10px; + cursor: pointer; + + // IE8-9 hack for event handling + // + // Internet Explorer 8-9 does not support clicks on elements without a set + // `background-color`. We cannot use `filter` since that's not viewed as a + // background color by the browser. Thus, a hack is needed. + // + // For IE8, we set solid black as it doesn't support `rgba()`. For IE9, we + // set alpha transparency for the best results possible. + background-color: #000 \9; // IE8 + background-color: rgba(0,0,0,0); // IE9 + } + .active { + margin: 0; + width: 12px; + height: 12px; + background-color: $carousel-indicator-active-bg; + } +} + +// Optional captions +// ----------------------------- +// Hidden by default for smaller viewports +.carousel-caption { + position: absolute; + left: 15%; + right: 15%; + bottom: 20px; + z-index: 10; + padding-top: 20px; + padding-bottom: 20px; + color: $carousel-caption-color; + text-align: center; + text-shadow: $carousel-text-shadow; + & .btn { + text-shadow: none; // No shadow for button elements in carousel-caption + } +} + + +// Scale up controls for tablets and up +@media screen and (min-width: $screen-sm-min) { + + // Scale up the controls a smidge + .carousel-control { + .glyphicon-chevron-left, + .glyphicon-chevron-right, + .icon-prev, + .icon-next { + width: 30px; + height: 30px; + margin-top: -15px; + margin-left: -15px; + font-size: 30px; + } + } + + // Show and left align the captions + .carousel-caption { + left: 20%; + right: 20%; + padding-bottom: 30px; + } + + // Move up the indicators + .carousel-indicators { + bottom: 20px; + } +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_close.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_close.scss new file mode 100644 index 0000000000000000000000000000000000000000..62ce30fa374f8c997c10c000a61a82d1602f2c50 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_close.scss @@ -0,0 +1,35 @@ +// +// Close icons +// -------------------------------------------------- + + +.close { + float: right; + font-size: ($font-size-base * 1.5); + font-weight: $close-font-weight; + line-height: 1; + color: $close-color; + text-shadow: $close-text-shadow; + @include opacity(.2); + + &:hover, + &:focus { + color: $close-color; + text-decoration: none; + cursor: pointer; + @include opacity(.5); + } + + // [converter] extracted button& to button.close +} + +// Additional properties for button version +// iOS requires the button element instead of an anchor tag. +// If you want the anchor version, it requires `href="#"`. +button.close { + padding: 0; + cursor: pointer; + background: transparent; + border: 0; + -webkit-appearance: none; +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_code.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_code.scss new file mode 100644 index 0000000000000000000000000000000000000000..89536160990a07218788a41b18aa912c13b093ae --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_code.scss @@ -0,0 +1,63 @@ +// +// Code (inline and block) +// -------------------------------------------------- + + +// Inline and block code styles +code, +kbd, +pre, +samp { + font-family: $font-family-monospace; +} + +// Inline code +code { + padding: 2px 4px; + font-size: 90%; + color: $code-color; + background-color: $code-bg; + white-space: nowrap; + border-radius: $border-radius-base; +} + +// User input typically entered via keyboard +kbd { + padding: 2px 4px; + font-size: 90%; + color: $kbd-color; + background-color: $kbd-bg; + border-radius: $border-radius-small; + box-shadow: inset 0 -1px 0 rgba(0,0,0,.25); +} + +// Blocks of code +pre { + display: block; + padding: (($line-height-computed - 1) / 2); + margin: 0 0 ($line-height-computed / 2); + font-size: ($font-size-base - 1); // 14px to 13px + line-height: $line-height-base; + word-break: break-all; + word-wrap: break-word; + color: $pre-color; + background-color: $pre-bg; + border: 1px solid $pre-border-color; + border-radius: $border-radius-base; + + // Account for some code outputs that place code tags in pre tags + code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; + } +} + +// Enable scrollable blocks of code +.pre-scrollable { + max-height: $pre-scrollable-max-height; + overflow-y: scroll; +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_component-animations.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_component-animations.scss new file mode 100644 index 0000000000000000000000000000000000000000..86632fd34a7935d3257892980fd43906145ebf65 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_component-animations.scss @@ -0,0 +1,29 @@ +// +// Component animations +// -------------------------------------------------- + +// Heads up! +// +// We don't use the `.opacity()` mixin here since it causes a bug with text +// fields in IE7-8. Source: https://github.com/twitter/bootstrap/pull/3552. + +.fade { + opacity: 0; + @include transition(opacity .15s linear); + &.in { + opacity: 1; + } +} + +.collapse { + display: none; + &.in { + display: block; + } +} +.collapsing { + position: relative; + height: 0; + overflow: hidden; + @include transition(height .35s ease); +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_dropdowns.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_dropdowns.scss new file mode 100644 index 0000000000000000000000000000000000000000..526be5b84961d8bce2437167644f0bc2411f539a --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_dropdowns.scss @@ -0,0 +1,213 @@ +// +// Dropdown menus +// -------------------------------------------------- + + +// Dropdown arrow/caret +.caret { + display: inline-block; + width: 0; + height: 0; + margin-left: 2px; + vertical-align: middle; + border-top: $caret-width-base solid; + border-right: $caret-width-base solid transparent; + border-left: $caret-width-base solid transparent; +} + +// The dropdown wrapper (div) +.dropdown { + position: relative; +} + +// Prevent the focus on the dropdown toggle when closing dropdowns +.dropdown-toggle:focus { + outline: 0; +} + +// The dropdown menu (ul) +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: $zindex-dropdown; + display: none; // none by default, but block on "open" of the menu + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; // override default ul + list-style: none; + font-size: $font-size-base; + background-color: $dropdown-bg; + border: 1px solid $dropdown-fallback-border; // IE8 fallback + border: 1px solid $dropdown-border; + border-radius: $border-radius-base; + @include box-shadow(0 6px 12px rgba(0,0,0,.175)); + background-clip: padding-box; + + // Aligns the dropdown menu to right + // + // Deprecated as of 3.1.0 in favor of `.dropdown-menu-[dir]` + &.pull-right { + right: 0; + left: auto; + } + + // Dividers (basically an hr) within the dropdown + .divider { + @include nav-divider($dropdown-divider-bg); + } + + // Links within the dropdown menu + > li > a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: $line-height-base; + color: $dropdown-link-color; + white-space: nowrap; // prevent links from randomly breaking onto new lines + } +} + +// Hover/Focus state +.dropdown-menu > li > a { + &:hover, + &:focus { + text-decoration: none; + color: $dropdown-link-hover-color; + background-color: $dropdown-link-hover-bg; + } +} + +// Active state +.dropdown-menu > .active > a { + &, + &:hover, + &:focus { + color: $dropdown-link-active-color; + text-decoration: none; + outline: 0; + background-color: $dropdown-link-active-bg; + } +} + +// Disabled state +// +// Gray out text and ensure the hover/focus state remains gray + +.dropdown-menu > .disabled > a { + &, + &:hover, + &:focus { + color: $dropdown-link-disabled-color; + } +} +// Nuke hover/focus effects +.dropdown-menu > .disabled > a { + &:hover, + &:focus { + text-decoration: none; + background-color: transparent; + background-image: none; // Remove CSS gradient + @include reset-filter(); + cursor: not-allowed; + } +} + +// Open state for the dropdown +.open { + // Show the menu + > .dropdown-menu { + display: block; + } + + // Remove the outline when :focus is triggered + > a { + outline: 0; + } +} + +// Menu positioning +// +// Add extra class to `.dropdown-menu` to flip the alignment of the dropdown +// menu with the parent. +.dropdown-menu-right { + left: auto; // Reset the default from `.dropdown-menu` + right: 0; +} +// With v3, we enabled auto-flipping if you have a dropdown within a right +// aligned nav component. To enable the undoing of that, we provide an override +// to restore the default dropdown menu alignment. +// +// This is only for left-aligning a dropdown menu within a `.navbar-right` or +// `.pull-right` nav component. +.dropdown-menu-left { + left: 0; + right: auto; +} + +// Dropdown section headers +.dropdown-header { + display: block; + padding: 3px 20px; + font-size: $font-size-small; + line-height: $line-height-base; + color: $dropdown-header-color; +} + +// Backdrop to catch body clicks on mobile, etc. +.dropdown-backdrop { + position: fixed; + left: 0; + right: 0; + bottom: 0; + top: 0; + z-index: ($zindex-dropdown - 10); +} + +// Right aligned dropdowns +.pull-right > .dropdown-menu { + right: 0; + left: auto; +} + +// Allow for dropdowns to go bottom up (aka, dropup-menu) +// +// Just add .dropup after the standard .dropdown class and you're set, bro. +// TODO: abstract this so that the navbar fixed styles are not placed here? + +.dropup, +.navbar-fixed-bottom .dropdown { + // Reverse the caret + .caret { + border-top: 0; + border-bottom: $caret-width-base solid; + content: ""; + } + // Different positioning for bottom up menu + .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 1px; + } +} + + +// Component alignment +// +// Reiterate per navbar.less and the modified component alignment there. + +@media (min-width: $grid-float-breakpoint) { + .navbar-right { + .dropdown-menu { + right: 0; left: auto; + } + // Necessary for overrides of the default right aligned menu. + // Will remove come v4 in all likelihood. + .dropdown-menu-left { + left: 0; right: auto; + } + } +} + diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_forms.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_forms.scss new file mode 100644 index 0000000000000000000000000000000000000000..262823850fe26185cb2e9675ecfa2a94127bd9d9 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_forms.scss @@ -0,0 +1,436 @@ +// +// Forms +// -------------------------------------------------- + + +// Normalize non-controls +// +// Restyle and baseline non-control form elements. + +fieldset { + padding: 0; + margin: 0; + border: 0; + // Chrome and Firefox set a `min-width: -webkit-min-content;` on fieldsets, + // so we reset that to ensure it behaves more like a standard block element. + // See https://github.com/twbs/bootstrap/issues/12359. + min-width: 0; +} + +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: $line-height-computed; + font-size: ($font-size-base * 1.5); + line-height: inherit; + color: $legend-color; + border: 0; + border-bottom: 1px solid $legend-border-color; +} + +label { + display: inline-block; + margin-bottom: 5px; + font-weight: bold; +} + + +// Normalize form controls +// +// While most of our form styles require extra classes, some basic normalization +// is required to ensure optimum display with or without those classes to better +// address browser inconsistencies. + +// Override content-box in Normalize (* isn't specific enough) +input[type="search"] { + @include box-sizing(border-box); +} + +// Position radios and checkboxes better +input[type="radio"], +input[type="checkbox"] { + margin: 4px 0 0; + margin-top: 1px \9; /* IE8-9 */ + line-height: normal; +} + +// Set the height of file controls to match text inputs +input[type="file"] { + display: block; +} + +// Make range inputs behave like textual form controls +input[type="range"] { + display: block; + width: 100%; +} + +// Make multiple select elements height not fixed +select[multiple], +select[size] { + height: auto; +} + +// Focus for file, radio, and checkbox +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { + @include tab-focus(); +} + +// Adjust output element +output { + display: block; + padding-top: ($padding-base-vertical + 1); + font-size: $font-size-base; + line-height: $line-height-base; + color: $input-color; +} + + +// Common form controls +// +// Shared size and type resets for form controls. Apply `.form-control` to any +// of the following form controls: +// +// select +// textarea +// input[type="text"] +// input[type="password"] +// input[type="datetime"] +// input[type="datetime-local"] +// input[type="date"] +// input[type="month"] +// input[type="time"] +// input[type="week"] +// input[type="number"] +// input[type="email"] +// input[type="url"] +// input[type="search"] +// input[type="tel"] +// input[type="color"] + +.form-control { + display: block; + width: 100%; + height: $input-height-base; // Make inputs at least the height of their button counterpart (base line-height + padding + border) + padding: $padding-base-vertical $padding-base-horizontal; + font-size: $font-size-base; + line-height: $line-height-base; + color: $input-color; + background-color: $input-bg; + background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 + border: 1px solid $input-border; + border-radius: $input-border-radius; + @include box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); + @include transition(border-color ease-in-out .15s, box-shadow ease-in-out .15s); + + // Customize the `:focus` state to imitate native WebKit styles. + @include form-control-focus(); + + // Placeholder + @include placeholder(); + + // Disabled and read-only inputs + // + // HTML5 says that controls under a fieldset > legend:first-child won't be + // disabled if the fieldset is disabled. Due to implementation difficulty, we + // don't honor that edge case; we style them as disabled anyway. + &[disabled], + &[readonly], + fieldset[disabled] & { + cursor: not-allowed; + background-color: $input-bg-disabled; + opacity: 1; // iOS fix for unreadable disabled content + } + + // [converter] extracted textarea& to textarea.form-control +} + +// Reset height for `textarea`s +textarea.form-control { + height: auto; +} + + +// Search inputs in iOS +// +// This overrides the extra rounded corners on search inputs in iOS so that our +// `.form-control` class can properly style them. Note that this cannot simply +// be added to `.form-control` as it's not specific enough. For details, see +// https://github.com/twbs/bootstrap/issues/11586. + +input[type="search"] { + -webkit-appearance: none; +} + + +// Special styles for iOS date input +// +// In Mobile Safari, date inputs require a pixel line-height that matches the +// given height of the input. + +input[type="date"] { + line-height: $input-height-base; +} + + +// Form groups +// +// Designed to help with the organization and spacing of vertical forms. For +// horizontal forms, use the predefined grid classes. + +.form-group { + margin-bottom: 15px; +} + + +// Checkboxes and radios +// +// Indent the labels to position radios/checkboxes as hanging controls. + +.radio, +.checkbox { + display: block; + min-height: $line-height-computed; // clear the floating input if there is no label text + margin-top: 10px; + margin-bottom: 10px; + padding-left: 20px; + label { + display: inline; + font-weight: normal; + cursor: pointer; + } +} +.radio input[type="radio"], +.radio-inline input[type="radio"], +.checkbox input[type="checkbox"], +.checkbox-inline input[type="checkbox"] { + float: left; + margin-left: -20px; +} +.radio + .radio, +.checkbox + .checkbox { + margin-top: -5px; // Move up sibling radios or checkboxes for tighter spacing +} + +// Radios and checkboxes on same line +.radio-inline, +.checkbox-inline { + display: inline-block; + padding-left: 20px; + margin-bottom: 0; + vertical-align: middle; + font-weight: normal; + cursor: pointer; +} +.radio-inline + .radio-inline, +.checkbox-inline + .checkbox-inline { + margin-top: 0; + margin-left: 10px; // space out consecutive inline controls +} + +// Apply same disabled cursor tweak as for inputs +// +// Note: Neither radios nor checkboxes can be readonly. +input[type="radio"], +input[type="checkbox"], +.radio, +.radio-inline, +.checkbox, +.checkbox-inline { + &[disabled], + fieldset[disabled] & { + cursor: not-allowed; + } +} + + +// Form control sizing +// +// Build on `.form-control` with modifier classes to decrease or increase the +// height and font-size of form controls. + +@include input-size('.input-sm', $input-height-small, $padding-small-vertical, $padding-small-horizontal, $font-size-small, $line-height-small, $border-radius-small); + +@include input-size('.input-lg', $input-height-large, $padding-large-vertical, $padding-large-horizontal, $font-size-large, $line-height-large, $border-radius-large); + + +// Form control feedback states +// +// Apply contextual and semantic states to individual form controls. + +.has-feedback { + // Enable absolute positioning + position: relative; + + // Ensure icons don't overlap text + .form-control { + padding-right: ($input-height-base * 1.25); + } + + // Feedback icon (requires .glyphicon classes) + .form-control-feedback { + position: absolute; + top: ($line-height-computed + 5); // Height of the `label` and its margin + right: 0; + display: block; + width: $input-height-base; + height: $input-height-base; + line-height: $input-height-base; + text-align: center; + } +} + +// Feedback states +.has-success { + @include form-control-validation($state-success-text, $state-success-text, $state-success-bg); +} +.has-warning { + @include form-control-validation($state-warning-text, $state-warning-text, $state-warning-bg); +} +.has-error { + @include form-control-validation($state-danger-text, $state-danger-text, $state-danger-bg); +} + + +// Static form control text +// +// Apply class to a `p` element to make any string of text align with labels in +// a horizontal form layout. + +.form-control-static { + margin-bottom: 0; // Remove default margin from `p` +} + + +// Help text +// +// Apply to any element you wish to create light text for placement immediately +// below a form control. Use for general help, formatting, or instructional text. + +.help-block { + display: block; // account for any element using help-block + margin-top: 5px; + margin-bottom: 10px; + color: lighten($text-color, 25%); // lighten the text some for contrast +} + + + +// Inline forms +// +// Make forms appear inline(-block) by adding the `.form-inline` class. Inline +// forms begin stacked on extra small (mobile) devices and then go inline when +// viewports reach <768px. +// +// Requires wrapping inputs and labels with `.form-group` for proper display of +// default HTML form controls and our custom form controls (e.g., input groups). +// +// Heads up! This is mixin-ed into `.navbar-form` in navbars.less. + +.form-inline { + + // Kick in the inline + @media (min-width: $screen-sm-min) { + // Inline-block all the things for "inline" + .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } + + // In navbar-form, allow folks to *not* use `.form-group` + .form-control { + display: inline-block; + width: auto; // Prevent labels from stacking above inputs in `.form-group` + vertical-align: middle; + } + // Input groups need that 100% width though + .input-group > .form-control { + width: 100%; + } + + .control-label { + margin-bottom: 0; + vertical-align: middle; + } + + // Remove default margin on radios/checkboxes that were used for stacking, and + // then undo the floating of radios and checkboxes to match (which also avoids + // a bug in WebKit: https://github.com/twbs/bootstrap/issues/1969). + .radio, + .checkbox { + display: inline-block; + margin-top: 0; + margin-bottom: 0; + padding-left: 0; + vertical-align: middle; + } + .radio input[type="radio"], + .checkbox input[type="checkbox"] { + float: none; + margin-left: 0; + } + + // Validation states + // + // Reposition the icon because it's now within a grid column and columns have + // `position: relative;` on them. Also accounts for the grid gutter padding. + .has-feedback .form-control-feedback { + top: 0; + } + } +} + + +// Horizontal forms +// +// Horizontal forms are built on grid classes and allow you to create forms with +// labels on the left and inputs on the right. + +.form-horizontal { + + // Consistent vertical alignment of labels, radios, and checkboxes + .control-label, + .radio, + .checkbox, + .radio-inline, + .checkbox-inline { + margin-top: 0; + margin-bottom: 0; + padding-top: ($padding-base-vertical + 1); // Default padding plus a border + } + // Account for padding we're adding to ensure the alignment and of help text + // and other content below items + .radio, + .checkbox { + min-height: ($line-height-computed + ($padding-base-vertical + 1)); + } + + // Make form groups behave like rows + .form-group { + @include make-row(); + } + + .form-control-static { + padding-top: ($padding-base-vertical + 1); + } + + // Only right align form labels here when the columns stop stacking + @media (min-width: $screen-sm-min) { + .control-label { + text-align: right; + } + } + + // Validation states + // + // Reposition the icon because it's now within a grid column and columns have + // `position: relative;` on them. Also accounts for the grid gutter padding. + .has-feedback .form-control-feedback { + top: 0; + right: ($grid-gutter-width / 2); + } +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_glyphicons.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_glyphicons.scss new file mode 100644 index 0000000000000000000000000000000000000000..c508835e3cfc29e13a31a1e57710e6ae57c4a083 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_glyphicons.scss @@ -0,0 +1,233 @@ +// +// Glyphicons for Bootstrap +// +// Since icons are fonts, they can be placed anywhere text is placed and are +// thus automatically sized to match the surrounding child. To use, create an +// inline element with the appropriate classes, like so: +// +// <a href="#"><span class="glyphicon glyphicon-star"></span> Star</a> + +// Import the fonts +@font-face { + font-family: 'Glyphicons Halflings'; + src: url(if($bootstrap-sass-asset-helper, twbs-font-path('#{$icon-font-path}#{$icon-font-name}.eot'), '#{$icon-font-path}#{$icon-font-name}.eot')); + src: url(if($bootstrap-sass-asset-helper, twbs-font-path('#{$icon-font-path}#{$icon-font-name}.eot?#iefix'), '#{$icon-font-path}#{$icon-font-name}.eot?#iefix')) format('embedded-opentype'), + url(if($bootstrap-sass-asset-helper, twbs-font-path('#{$icon-font-path}#{$icon-font-name}.woff'), '#{$icon-font-path}#{$icon-font-name}.woff')) format('woff'), + url(if($bootstrap-sass-asset-helper, twbs-font-path('#{$icon-font-path}#{$icon-font-name}.ttf'), '#{$icon-font-path}#{$icon-font-name}.ttf')) format('truetype'), + url(if($bootstrap-sass-asset-helper, twbs-font-path('#{$icon-font-path}#{$icon-font-name}.svg##{$icon-font-svg-id}'), '#{$icon-font-path}#{$icon-font-name}.svg##{$icon-font-svg-id}')) format('svg'); +} + +// Catchall baseclass +.glyphicon { + position: relative; + top: 1px; + display: inline-block; + font-family: 'Glyphicons Halflings'; + font-style: normal; + font-weight: normal; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +// Individual icons +.glyphicon-asterisk { &:before { content: "\2a"; } } +.glyphicon-plus { &:before { content: "\2b"; } } +.glyphicon-euro { &:before { content: "\20ac"; } } +.glyphicon-minus { &:before { content: "\2212"; } } +.glyphicon-cloud { &:before { content: "\2601"; } } +.glyphicon-envelope { &:before { content: "\2709"; } } +.glyphicon-pencil { &:before { content: "\270f"; } } +.glyphicon-glass { &:before { content: "\e001"; } } +.glyphicon-music { &:before { content: "\e002"; } } +.glyphicon-search { &:before { content: "\e003"; } } +.glyphicon-heart { &:before { content: "\e005"; } } +.glyphicon-star { &:before { content: "\e006"; } } +.glyphicon-star-empty { &:before { content: "\e007"; } } +.glyphicon-user { &:before { content: "\e008"; } } +.glyphicon-film { &:before { content: "\e009"; } } +.glyphicon-th-large { &:before { content: "\e010"; } } +.glyphicon-th { &:before { content: "\e011"; } } +.glyphicon-th-list { &:before { content: "\e012"; } } +.glyphicon-ok { &:before { content: "\e013"; } } +.glyphicon-remove { &:before { content: "\e014"; } } +.glyphicon-zoom-in { &:before { content: "\e015"; } } +.glyphicon-zoom-out { &:before { content: "\e016"; } } +.glyphicon-off { &:before { content: "\e017"; } } +.glyphicon-signal { &:before { content: "\e018"; } } +.glyphicon-cog { &:before { content: "\e019"; } } +.glyphicon-trash { &:before { content: "\e020"; } } +.glyphicon-home { &:before { content: "\e021"; } } +.glyphicon-file { &:before { content: "\e022"; } } +.glyphicon-time { &:before { content: "\e023"; } } +.glyphicon-road { &:before { content: "\e024"; } } +.glyphicon-download-alt { &:before { content: "\e025"; } } +.glyphicon-download { &:before { content: "\e026"; } } +.glyphicon-upload { &:before { content: "\e027"; } } +.glyphicon-inbox { &:before { content: "\e028"; } } +.glyphicon-play-circle { &:before { content: "\e029"; } } +.glyphicon-repeat { &:before { content: "\e030"; } } +.glyphicon-refresh { &:before { content: "\e031"; } } +.glyphicon-list-alt { &:before { content: "\e032"; } } +.glyphicon-lock { &:before { content: "\e033"; } } +.glyphicon-flag { &:before { content: "\e034"; } } +.glyphicon-headphones { &:before { content: "\e035"; } } +.glyphicon-volume-off { &:before { content: "\e036"; } } +.glyphicon-volume-down { &:before { content: "\e037"; } } +.glyphicon-volume-up { &:before { content: "\e038"; } } +.glyphicon-qrcode { &:before { content: "\e039"; } } +.glyphicon-barcode { &:before { content: "\e040"; } } +.glyphicon-tag { &:before { content: "\e041"; } } +.glyphicon-tags { &:before { content: "\e042"; } } +.glyphicon-book { &:before { content: "\e043"; } } +.glyphicon-bookmark { &:before { content: "\e044"; } } +.glyphicon-print { &:before { content: "\e045"; } } +.glyphicon-camera { &:before { content: "\e046"; } } +.glyphicon-font { &:before { content: "\e047"; } } +.glyphicon-bold { &:before { content: "\e048"; } } +.glyphicon-italic { &:before { content: "\e049"; } } +.glyphicon-text-height { &:before { content: "\e050"; } } +.glyphicon-text-width { &:before { content: "\e051"; } } +.glyphicon-align-left { &:before { content: "\e052"; } } +.glyphicon-align-center { &:before { content: "\e053"; } } +.glyphicon-align-right { &:before { content: "\e054"; } } +.glyphicon-align-justify { &:before { content: "\e055"; } } +.glyphicon-list { &:before { content: "\e056"; } } +.glyphicon-indent-left { &:before { content: "\e057"; } } +.glyphicon-indent-right { &:before { content: "\e058"; } } +.glyphicon-facetime-video { &:before { content: "\e059"; } } +.glyphicon-picture { &:before { content: "\e060"; } } +.glyphicon-map-marker { &:before { content: "\e062"; } } +.glyphicon-adjust { &:before { content: "\e063"; } } +.glyphicon-tint { &:before { content: "\e064"; } } +.glyphicon-edit { &:before { content: "\e065"; } } +.glyphicon-share { &:before { content: "\e066"; } } +.glyphicon-check { &:before { content: "\e067"; } } +.glyphicon-move { &:before { content: "\e068"; } } +.glyphicon-step-backward { &:before { content: "\e069"; } } +.glyphicon-fast-backward { &:before { content: "\e070"; } } +.glyphicon-backward { &:before { content: "\e071"; } } +.glyphicon-play { &:before { content: "\e072"; } } +.glyphicon-pause { &:before { content: "\e073"; } } +.glyphicon-stop { &:before { content: "\e074"; } } +.glyphicon-forward { &:before { content: "\e075"; } } +.glyphicon-fast-forward { &:before { content: "\e076"; } } +.glyphicon-step-forward { &:before { content: "\e077"; } } +.glyphicon-eject { &:before { content: "\e078"; } } +.glyphicon-chevron-left { &:before { content: "\e079"; } } +.glyphicon-chevron-right { &:before { content: "\e080"; } } +.glyphicon-plus-sign { &:before { content: "\e081"; } } +.glyphicon-minus-sign { &:before { content: "\e082"; } } +.glyphicon-remove-sign { &:before { content: "\e083"; } } +.glyphicon-ok-sign { &:before { content: "\e084"; } } +.glyphicon-question-sign { &:before { content: "\e085"; } } +.glyphicon-info-sign { &:before { content: "\e086"; } } +.glyphicon-screenshot { &:before { content: "\e087"; } } +.glyphicon-remove-circle { &:before { content: "\e088"; } } +.glyphicon-ok-circle { &:before { content: "\e089"; } } +.glyphicon-ban-circle { &:before { content: "\e090"; } } +.glyphicon-arrow-left { &:before { content: "\e091"; } } +.glyphicon-arrow-right { &:before { content: "\e092"; } } +.glyphicon-arrow-up { &:before { content: "\e093"; } } +.glyphicon-arrow-down { &:before { content: "\e094"; } } +.glyphicon-share-alt { &:before { content: "\e095"; } } +.glyphicon-resize-full { &:before { content: "\e096"; } } +.glyphicon-resize-small { &:before { content: "\e097"; } } +.glyphicon-exclamation-sign { &:before { content: "\e101"; } } +.glyphicon-gift { &:before { content: "\e102"; } } +.glyphicon-leaf { &:before { content: "\e103"; } } +.glyphicon-fire { &:before { content: "\e104"; } } +.glyphicon-eye-open { &:before { content: "\e105"; } } +.glyphicon-eye-close { &:before { content: "\e106"; } } +.glyphicon-warning-sign { &:before { content: "\e107"; } } +.glyphicon-plane { &:before { content: "\e108"; } } +.glyphicon-calendar { &:before { content: "\e109"; } } +.glyphicon-random { &:before { content: "\e110"; } } +.glyphicon-comment { &:before { content: "\e111"; } } +.glyphicon-magnet { &:before { content: "\e112"; } } +.glyphicon-chevron-up { &:before { content: "\e113"; } } +.glyphicon-chevron-down { &:before { content: "\e114"; } } +.glyphicon-retweet { &:before { content: "\e115"; } } +.glyphicon-shopping-cart { &:before { content: "\e116"; } } +.glyphicon-folder-close { &:before { content: "\e117"; } } +.glyphicon-folder-open { &:before { content: "\e118"; } } +.glyphicon-resize-vertical { &:before { content: "\e119"; } } +.glyphicon-resize-horizontal { &:before { content: "\e120"; } } +.glyphicon-hdd { &:before { content: "\e121"; } } +.glyphicon-bullhorn { &:before { content: "\e122"; } } +.glyphicon-bell { &:before { content: "\e123"; } } +.glyphicon-certificate { &:before { content: "\e124"; } } +.glyphicon-thumbs-up { &:before { content: "\e125"; } } +.glyphicon-thumbs-down { &:before { content: "\e126"; } } +.glyphicon-hand-right { &:before { content: "\e127"; } } +.glyphicon-hand-left { &:before { content: "\e128"; } } +.glyphicon-hand-up { &:before { content: "\e129"; } } +.glyphicon-hand-down { &:before { content: "\e130"; } } +.glyphicon-circle-arrow-right { &:before { content: "\e131"; } } +.glyphicon-circle-arrow-left { &:before { content: "\e132"; } } +.glyphicon-circle-arrow-up { &:before { content: "\e133"; } } +.glyphicon-circle-arrow-down { &:before { content: "\e134"; } } +.glyphicon-globe { &:before { content: "\e135"; } } +.glyphicon-wrench { &:before { content: "\e136"; } } +.glyphicon-tasks { &:before { content: "\e137"; } } +.glyphicon-filter { &:before { content: "\e138"; } } +.glyphicon-briefcase { &:before { content: "\e139"; } } +.glyphicon-fullscreen { &:before { content: "\e140"; } } +.glyphicon-dashboard { &:before { content: "\e141"; } } +.glyphicon-paperclip { &:before { content: "\e142"; } } +.glyphicon-heart-empty { &:before { content: "\e143"; } } +.glyphicon-link { &:before { content: "\e144"; } } +.glyphicon-phone { &:before { content: "\e145"; } } +.glyphicon-pushpin { &:before { content: "\e146"; } } +.glyphicon-usd { &:before { content: "\e148"; } } +.glyphicon-gbp { &:before { content: "\e149"; } } +.glyphicon-sort { &:before { content: "\e150"; } } +.glyphicon-sort-by-alphabet { &:before { content: "\e151"; } } +.glyphicon-sort-by-alphabet-alt { &:before { content: "\e152"; } } +.glyphicon-sort-by-order { &:before { content: "\e153"; } } +.glyphicon-sort-by-order-alt { &:before { content: "\e154"; } } +.glyphicon-sort-by-attributes { &:before { content: "\e155"; } } +.glyphicon-sort-by-attributes-alt { &:before { content: "\e156"; } } +.glyphicon-unchecked { &:before { content: "\e157"; } } +.glyphicon-expand { &:before { content: "\e158"; } } +.glyphicon-collapse-down { &:before { content: "\e159"; } } +.glyphicon-collapse-up { &:before { content: "\e160"; } } +.glyphicon-log-in { &:before { content: "\e161"; } } +.glyphicon-flash { &:before { content: "\e162"; } } +.glyphicon-log-out { &:before { content: "\e163"; } } +.glyphicon-new-window { &:before { content: "\e164"; } } +.glyphicon-record { &:before { content: "\e165"; } } +.glyphicon-save { &:before { content: "\e166"; } } +.glyphicon-open { &:before { content: "\e167"; } } +.glyphicon-saved { &:before { content: "\e168"; } } +.glyphicon-import { &:before { content: "\e169"; } } +.glyphicon-export { &:before { content: "\e170"; } } +.glyphicon-send { &:before { content: "\e171"; } } +.glyphicon-floppy-disk { &:before { content: "\e172"; } } +.glyphicon-floppy-saved { &:before { content: "\e173"; } } +.glyphicon-floppy-remove { &:before { content: "\e174"; } } +.glyphicon-floppy-save { &:before { content: "\e175"; } } +.glyphicon-floppy-open { &:before { content: "\e176"; } } +.glyphicon-credit-card { &:before { content: "\e177"; } } +.glyphicon-transfer { &:before { content: "\e178"; } } +.glyphicon-cutlery { &:before { content: "\e179"; } } +.glyphicon-header { &:before { content: "\e180"; } } +.glyphicon-compressed { &:before { content: "\e181"; } } +.glyphicon-earphone { &:before { content: "\e182"; } } +.glyphicon-phone-alt { &:before { content: "\e183"; } } +.glyphicon-tower { &:before { content: "\e184"; } } +.glyphicon-stats { &:before { content: "\e185"; } } +.glyphicon-sd-video { &:before { content: "\e186"; } } +.glyphicon-hd-video { &:before { content: "\e187"; } } +.glyphicon-subtitles { &:before { content: "\e188"; } } +.glyphicon-sound-stereo { &:before { content: "\e189"; } } +.glyphicon-sound-dolby { &:before { content: "\e190"; } } +.glyphicon-sound-5-1 { &:before { content: "\e191"; } } +.glyphicon-sound-6-1 { &:before { content: "\e192"; } } +.glyphicon-sound-7-1 { &:before { content: "\e193"; } } +.glyphicon-copyright-mark { &:before { content: "\e194"; } } +.glyphicon-registration-mark { &:before { content: "\e195"; } } +.glyphicon-cloud-download { &:before { content: "\e197"; } } +.glyphicon-cloud-upload { &:before { content: "\e198"; } } +.glyphicon-tree-conifer { &:before { content: "\e199"; } } +.glyphicon-tree-deciduous { &:before { content: "\e200"; } } diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_grid.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_grid.scss new file mode 100644 index 0000000000000000000000000000000000000000..f71f8b9015bfecf7d3e7afb0a1b9cfc38741f21e --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_grid.scss @@ -0,0 +1,84 @@ +// +// Grid system +// -------------------------------------------------- + + +// Container widths +// +// Set the container width, and override it for fixed navbars in media queries. + +.container { + @include container-fixed(); + + @media (min-width: $screen-sm-min) { + width: $container-sm; + } + @media (min-width: $screen-md-min) { + width: $container-md; + } + @media (min-width: $screen-lg-min) { + width: $container-lg; + } +} + + +// Fluid container +// +// Utilizes the mixin meant for fixed width containers, but without any defined +// width for fluid, full width layouts. + +.container-fluid { + @include container-fixed(); +} + + +// Row +// +// Rows contain and clear the floats of your columns. + +.row { + @include make-row(); +} + + +// Columns +// +// Common styles for small and large grid columns + +@include make-grid-columns(); + + +// Extra small grid +// +// Columns, offsets, pushes, and pulls for extra small devices like +// smartphones. + +@include make-grid(xs); + + +// Small grid +// +// Columns, offsets, pushes, and pulls for the small device range, from phones +// to tablets. + +@media (min-width: $screen-sm-min) { + @include make-grid(sm); +} + + +// Medium grid +// +// Columns, offsets, pushes, and pulls for the desktop device range. + +@media (min-width: $screen-md-min) { + @include make-grid(md); +} + + +// Large grid +// +// Columns, offsets, pushes, and pulls for the large desktop device range. + +@media (min-width: $screen-lg-min) { + @include make-grid(lg); +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_input-groups.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_input-groups.scss new file mode 100644 index 0000000000000000000000000000000000000000..6c26c1dd6babd12a699ac5733d3beec8b721d31d --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_input-groups.scss @@ -0,0 +1,162 @@ +// +// Input groups +// -------------------------------------------------- + +// Base styles +// ------------------------- +.input-group { + position: relative; // For dropdowns + display: table; + border-collapse: separate; // prevent input groups from inheriting border styles from table cells when placed within a table + + // Undo padding and float of grid classes + &[class*="col-"] { + float: none; + padding-left: 0; + padding-right: 0; + } + + .form-control { + // Ensure that the input is always above the *appended* addon button for + // proper border colors. + position: relative; + z-index: 2; + + // IE9 fubars the placeholder attribute in text inputs and the arrows on + // select elements in input groups. To fix it, we float the input. Details: + // https://github.com/twbs/bootstrap/issues/11561#issuecomment-28936855 + float: left; + + width: 100%; + margin-bottom: 0; + } +} + +// Sizing options +// +// Remix the default form control sizing classes into new ones for easier +// manipulation. + +.input-group-lg > .form-control, +.input-group-lg > .input-group-addon, +.input-group-lg > .input-group-btn > .btn { @extend .input-lg; } +.input-group-sm > .form-control, +.input-group-sm > .input-group-addon, +.input-group-sm > .input-group-btn > .btn { @extend .input-sm; } + + +// Display as table-cell +// ------------------------- +.input-group-addon, +.input-group-btn, +.input-group .form-control { + display: table-cell; + + &:not(:first-child):not(:last-child) { + border-radius: 0; + } +} +// Addon and addon wrapper for buttons +.input-group-addon, +.input-group-btn { + width: 1%; + white-space: nowrap; + vertical-align: middle; // Match the inputs +} + +// Text input groups +// ------------------------- +.input-group-addon { + padding: $padding-base-vertical $padding-base-horizontal; + font-size: $font-size-base; + font-weight: normal; + line-height: 1; + color: $input-color; + text-align: center; + background-color: $input-group-addon-bg; + border: 1px solid $input-group-addon-border-color; + border-radius: $border-radius-base; + + // Sizing + &.input-sm { + padding: $padding-small-vertical $padding-small-horizontal; + font-size: $font-size-small; + border-radius: $border-radius-small; + } + &.input-lg { + padding: $padding-large-vertical $padding-large-horizontal; + font-size: $font-size-large; + border-radius: $border-radius-large; + } + + // Nuke default margins from checkboxes and radios to vertically center within. + input[type="radio"], + input[type="checkbox"] { + margin-top: 0; + } +} + +// Reset rounded corners +.input-group .form-control:first-child, +.input-group-addon:first-child, +.input-group-btn:first-child > .btn, +.input-group-btn:first-child > .btn-group > .btn, +.input-group-btn:first-child > .dropdown-toggle, +.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle), +.input-group-btn:last-child > .btn-group:not(:last-child) > .btn { + @include border-right-radius(0); +} +.input-group-addon:first-child { + border-right: 0; +} +.input-group .form-control:last-child, +.input-group-addon:last-child, +.input-group-btn:last-child > .btn, +.input-group-btn:last-child > .btn-group > .btn, +.input-group-btn:last-child > .dropdown-toggle, +.input-group-btn:first-child > .btn:not(:first-child), +.input-group-btn:first-child > .btn-group:not(:first-child) > .btn { + @include border-left-radius(0); +} +.input-group-addon:last-child { + border-left: 0; +} + +// Button input groups +// ------------------------- +.input-group-btn { + position: relative; + // Jankily prevent input button groups from wrapping with `white-space` and + // `font-size` in combination with `inline-block` on buttons. + font-size: 0; + white-space: nowrap; + + // Negative margin for spacing, position for bringing hovered/focused/actived + // element above the siblings. + > .btn { + position: relative; + + .btn { + margin-left: -1px; + } + // Bring the "active" button to the front + &:hover, + &:focus, + &:active { + z-index: 2; + } + } + + // Negative margin to only have a 1px border between the two + &:first-child { + > .btn, + > .btn-group { + margin-right: -1px; + } + } + &:last-child { + > .btn, + > .btn-group { + margin-left: -1px; + } + } +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_jumbotron.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_jumbotron.scss new file mode 100644 index 0000000000000000000000000000000000000000..4e401e7376bfe8f40ef717f6ff2c6ea305802682 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_jumbotron.scss @@ -0,0 +1,44 @@ +// +// Jumbotron +// -------------------------------------------------- + + +.jumbotron { + padding: $jumbotron-padding; + margin-bottom: $jumbotron-padding; + color: $jumbotron-color; + background-color: $jumbotron-bg; + + h1, + .h1 { + color: $jumbotron-heading-color; + } + p { + margin-bottom: ($jumbotron-padding / 2); + font-size: $jumbotron-font-size; + font-weight: 200; + } + + .container & { + border-radius: $border-radius-large; // Only round corners at higher resolutions if contained in a container + } + + .container { + max-width: 100%; + } + + @media screen and (min-width: $screen-sm-min) { + padding-top: ($jumbotron-padding * 1.6); + padding-bottom: ($jumbotron-padding * 1.6); + + .container & { + padding-left: ($jumbotron-padding * 2); + padding-right: ($jumbotron-padding * 2); + } + + h1, + .h1 { + font-size: ($font-size-base * 4.5); + } + } +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_labels.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_labels.scss new file mode 100644 index 0000000000000000000000000000000000000000..6a8b2b04e48c3c01cee39a26503b437b19553f63 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_labels.scss @@ -0,0 +1,64 @@ +// +// Labels +// -------------------------------------------------- + +.label { + display: inline; + padding: .2em .6em .3em; + font-size: 13px; + line-height: 1; + color: $label-color; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25em; + background: $label-default-bg; + + // Add hover effects, but only for links + &[href] { + &:hover, + &:focus { + color: $label-link-hover-color; + text-decoration: none; + cursor: pointer; + } + } + + // Empty labels collapse automatically (not available in IE8) + &:empty { + display: none; + } + + // Quick fix for labels in buttons + .btn & { + position: relative; + top: -1px; + } +} + +// Colors +// Contextual variations (linked labels get darker on :hover) + +.label-default { + @include label-variant($label-default-bg); +} + +.label-primary { + @include label-variant($label-primary-bg); +} + +.label-success { + @include label-variant($label-success-bg); +} + +.label-info { + @include label-variant($label-info-bg); +} + +.label-warning { + @include label-variant($label-warning-bg); +} + +.label-danger { + @include label-variant($label-danger-bg); +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_list-group.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_list-group.scss new file mode 100644 index 0000000000000000000000000000000000000000..b6089912f9c6f626688f31544f9cb63a5b8b273d --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_list-group.scss @@ -0,0 +1,110 @@ +// +// List groups +// -------------------------------------------------- + + +// Base class +// +// Easily usable on <ul>, <ol>, or <div>. + +.list-group { + // No need to set list-style: none; since .list-group-item is block level + margin-bottom: 20px; + padding-left: 0; // reset padding because ul and ol +} + + +// Individual list items +// +// Use on `li`s or `div`s within the `.list-group` parent. + +.list-group-item { + position: relative; + display: block; + padding: 10px 15px; + // Place the border on the list items and negative margin up for better styling + margin-bottom: -1px; + background-color: $list-group-bg; + border: 1px solid $list-group-border; + + // Round the first and last items + &:first-child { + @include border-top-radius($list-group-border-radius); + } + &:last-child { + margin-bottom: 0; + @include border-bottom-radius($list-group-border-radius); + } + + // Align badges within list items + > .badge { + float: right; + } + > .badge + .badge { + margin-right: 5px; + } +} + + +// Linked list items +// +// Use anchor elements instead of `li`s or `div`s to create linked list items. +// Includes an extra `.active` modifier class for showing selected items. + +a.list-group-item { + color: $list-group-link-color; + + .list-group-item-heading { + color: $list-group-link-heading-color; + } + + // Hover state + &:hover, + &:focus { + text-decoration: none; + background-color: $list-group-hover-bg; + } + + // Active class on item itself, not parent + &.active, + &.active:hover, + &.active:focus { + z-index: 2; // Place active items above their siblings for proper border styling + color: $list-group-active-color; + background-color: $list-group-active-bg; + border-color: $list-group-active-border; + + // Force color to inherit for custom content + .list-group-item-heading { + color: inherit; + } + .list-group-item-text { + color: $list-group-active-text-color; + } + } +} + + +// Contextual variants +// +// Add modifier classes to change text and background color on individual items. +// Organizationally, this must come after the `:hover` states. + +@include list-group-item-variant(success, $state-success-bg, $state-success-text); +@include list-group-item-variant(info, $state-info-bg, $state-info-text); +@include list-group-item-variant(warning, $state-warning-bg, $state-warning-text); +@include list-group-item-variant(danger, $state-danger-bg, $state-danger-text); + + +// Custom content options +// +// Extra classes for creating well-formatted content within `.list-group-item`s. + +.list-group-item-heading { + margin-top: 0; + margin-bottom: 5px; +} +.list-group-item-text { + margin-bottom: 0; + line-height: 1.3; +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_media.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_media.scss new file mode 100644 index 0000000000000000000000000000000000000000..5ad22cd6d540fa378940c97910eabad478b09cba --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_media.scss @@ -0,0 +1,56 @@ +// Media objects +// Source: http://stubbornella.org/content/?p=497 +// -------------------------------------------------- + + +// Common styles +// ------------------------- + +// Clear the floats +.media, +.media-body { + overflow: hidden; + zoom: 1; +} + +// Proper spacing between instances of .media +.media, +.media .media { + margin-top: 15px; +} +.media:first-child { + margin-top: 0; +} + +// For images and videos, set to block +.media-object { + display: block; +} + +// Reset margins on headings for tighter default spacing +.media-heading { + margin: 0 0 5px; +} + + +// Media image alignment +// ------------------------- + +.media { + > .pull-left { + margin-right: 10px; + } + > .pull-right { + margin-left: 10px; + } +} + + +// Media list variation +// ------------------------- + +// Undo default ul/ol styles +.media-list { + padding-left: 0; + list-style: none; +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_mixins.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_mixins.scss new file mode 100644 index 0000000000000000000000000000000000000000..b48c437a15e036d6083502b5c4c6e96fe471b7b5 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_mixins.scss @@ -0,0 +1,948 @@ +// +// Mixins +// -------------------------------------------------- + + +// Utilities +// ------------------------- + +// Clearfix +// Source: http://nicolasgallagher.com/micro-clearfix-hack/ +// +// For modern browsers +// 1. The space content is one way to avoid an Opera bug when the +// contenteditable attribute is included anywhere else in the document. +// Otherwise it causes space to appear at the top and bottom of elements +// that are clearfixed. +// 2. The use of `table` rather than `block` is only necessary if using +// `:before` to contain the top-margins of child elements. +@mixin clearfix() { + &:before, + &:after { + content: " "; // 1 + display: table; // 2 + } + &:after { + clear: both; + } +} + +// WebKit-style focus +@mixin tab-focus() { + // Default + outline: 0 none; + // WebKit + //outline: 5px auto -webkit-focus-ring-color; + // outline-offset: -2px; +} + +// Center-align a block level element +@mixin center-block() { + display: block; + margin-left: auto; + margin-right: auto; +} + +// Sizing shortcuts +@mixin size($width, $height) { + width: $width; + height: $height; +} +@mixin square($size) { + @include size($size, $size); +} + +// Placeholder text +@mixin placeholder($color: $input-color-placeholder) { + &::-moz-placeholder { color: $color; // Firefox + opacity: 1; } // See https://github.com/twbs/bootstrap/pull/11526 + &:-ms-input-placeholder { color: $color; } // Internet Explorer 10+ + &::-webkit-input-placeholder { color: $color; } // Safari and Chrome +} + +// Text overflow +// Requires inline-block or block for proper styling +@mixin text-overflow() { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +// CSS image replacement +// +// Heads up! v3 launched with with only `.hide-text()`, but per our pattern for +// mixins being reused as classes with the same name, this doesn't hold up. As +// of v3.0.1 we have added `.text-hide()` and deprecated `.hide-text()`. Note +// that we cannot chain the mixins together in Less, so they are repeated. +// +// Source: https://github.com/h5bp/html5-boilerplate/commit/aa0396eae757 + +// Deprecated as of v3.0.1 (will be removed in v4) +@mixin hide-text() { + font: #{0/0} a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} +// New mixin to use as of v3.0.1 +@mixin text-hide() { + @include hide-text(); +} + + + +// CSS3 PROPERTIES +// -------------------------------------------------- + +// Single side border-radius +@mixin border-top-radius($radius) { + border-top-right-radius: $radius; + border-top-left-radius: $radius; +} +@mixin border-right-radius($radius) { + border-bottom-right-radius: $radius; + border-top-right-radius: $radius; +} +@mixin border-bottom-radius($radius) { + border-bottom-right-radius: $radius; + border-bottom-left-radius: $radius; +} +@mixin border-left-radius($radius) { + border-bottom-left-radius: $radius; + border-top-left-radius: $radius; +} + +// Drop shadows +// +// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's +// supported browsers that have box shadow capabilities now support the +// standard `box-shadow` property. +@mixin box-shadow($shadow...) { + -webkit-box-shadow: $shadow; // iOS <4.3 & Android <4.1 + box-shadow: $shadow; +} + +// Transitions +@mixin transition($transition...) { + -webkit-transition: $transition; + transition: $transition; +} +@mixin transition-property($transition-property...) { + -webkit-transition-property: $transition-property; + transition-property: $transition-property; +} +@mixin transition-delay($transition-delay) { + -webkit-transition-delay: $transition-delay; + transition-delay: $transition-delay; +} +@mixin transition-duration($transition-duration...) { + -webkit-transition-duration: $transition-duration; + transition-duration: $transition-duration; +} +@mixin transition-transform($transition...) { + -webkit-transition: -webkit-transform $transition; + -moz-transition: -moz-transform $transition; + -o-transition: -o-transform $transition; + transition: transform $transition; +} + +// Transformations +@mixin rotate($degrees) { + -webkit-transform: rotate($degrees); + -ms-transform: rotate($degrees); // IE9 only + transform: rotate($degrees); +} +@mixin scale($scale-args...) { + -webkit-transform: scale($scale-args); + -ms-transform: scale($scale-args); // IE9 only + transform: scale($scale-args); +} +@mixin translate($x, $y) { + -webkit-transform: translate($x, $y); + -ms-transform: translate($x, $y); // IE9 only + transform: translate($x, $y); +} +@mixin skew($x, $y) { + -webkit-transform: skew($x, $y); + -ms-transform: skewX($x) skewY($y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+ + transform: skew($x, $y); +} +@mixin translate3d($x, $y, $z) { + -webkit-transform: translate3d($x, $y, $z); + transform: translate3d($x, $y, $z); +} + +@mixin rotateX($degrees) { + -webkit-transform: rotateX($degrees); + -ms-transform: rotateX($degrees); // IE9 only + transform: rotateX($degrees); +} +@mixin rotateY($degrees) { + -webkit-transform: rotateY($degrees); + -ms-transform: rotateY($degrees); // IE9 only + transform: rotateY($degrees); +} +@mixin perspective($perspective) { + -webkit-perspective: $perspective; + -moz-perspective: $perspective; + perspective: $perspective; +} +@mixin perspective-origin($perspective) { + -webkit-perspective-origin: $perspective; + -moz-perspective-origin: $perspective; + perspective-origin: $perspective; +} +@mixin transform-origin($origin) { + -webkit-transform-origin: $origin; + -moz-transform-origin: $origin; + -ms-transform-origin: $origin; // IE9 only + transform-origin: $origin; +} + +// Animations +@mixin animation($animation) { + -webkit-animation: $animation; + animation: $animation; +} +@mixin animation-name($name) { + -webkit-animation-name: $name; + animation-name: $name; +} +@mixin animation-duration($duration) { + -webkit-animation-duration: $duration; + animation-duration: $duration; +} +@mixin animation-timing-function($timing-function) { + -webkit-animation-timing-function: $timing-function; + animation-timing-function: $timing-function; +} +@mixin animation-delay($delay) { + -webkit-animation-delay: $delay; + animation-delay: $delay; +} +@mixin animation-iteration-count($iteration-count) { + -webkit-animation-iteration-count: $iteration-count; + animation-iteration-count: $iteration-count; +} +@mixin animation-direction($direction) { + -webkit-animation-direction: $direction; + animation-direction: $direction; +} + +// Backface visibility +// Prevent browsers from flickering when using CSS 3D transforms. +// Default value is `visible`, but can be changed to `hidden` +@mixin backface-visibility($visibility){ + -webkit-backface-visibility: $visibility; + -moz-backface-visibility: $visibility; + backface-visibility: $visibility; +} + +// Box sizing +@mixin box-sizing($boxmodel) { + -webkit-box-sizing: $boxmodel; + -moz-box-sizing: $boxmodel; + box-sizing: $boxmodel; +} + +// User select +// For selecting text on the page +@mixin user-select($select) { + -webkit-user-select: $select; + -moz-user-select: $select; + -ms-user-select: $select; // IE10+ + user-select: $select; +} + +// Resize anything +@mixin resizable($direction) { + resize: $direction; // Options: horizontal, vertical, both + overflow: auto; // Safari fix +} + +// CSS3 Content Columns +@mixin content-columns($column-count, $column-gap: $grid-gutter-width) { + -webkit-column-count: $column-count; + -moz-column-count: $column-count; + column-count: $column-count; + -webkit-column-gap: $column-gap; + -moz-column-gap: $column-gap; + column-gap: $column-gap; +} + +// Optional hyphenation +@mixin hyphens($mode: auto) { + word-wrap: break-word; + -webkit-hyphens: $mode; + -moz-hyphens: $mode; + -ms-hyphens: $mode; // IE10+ + -o-hyphens: $mode; + hyphens: $mode; +} + +// Opacity +@mixin opacity($opacity) { + opacity: $opacity; + // IE8 filter + $opacity-ie: ($opacity * 100); + filter: #{alpha(opacity=$opacity-ie)}; +} + + + +// GRADIENTS +// -------------------------------------------------- + + + +// Horizontal gradient, from left to right +// +// Creates two color stops, start and end, by specifying a color and position for each color stop. +// Color stops are not available in IE9 and below. +@mixin gradient-horizontal($start-color: #555, $end-color: #333, $start-percent: 0%, $end-percent: 100%) { + background-image: -webkit-linear-gradient(left, color-stop($start-color $start-percent), color-stop($end-color $end-percent)); // Safari 5.1-6, Chrome 10+ + background-image: linear-gradient(to right, $start-color $start-percent, $end-color $end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+ + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#{ie-hex-str($start-color)}', endColorstr='#{ie-hex-str($end-color)}', GradientType=1); // IE9 and down +} + +// Vertical gradient, from top to bottom +// +// Creates two color stops, start and end, by specifying a color and position for each color stop. +// Color stops are not available in IE9 and below. +@mixin gradient-vertical($start-color: #555, $end-color: #333, $start-percent: 0%, $end-percent: 100%) { + background-image: -webkit-linear-gradient(top, $start-color $start-percent, $end-color $end-percent); // Safari 5.1-6, Chrome 10+ + background-image: linear-gradient(to bottom, $start-color $start-percent, $end-color $end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+ + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#{ie-hex-str($start-color)}', endColorstr='#{ie-hex-str($end-color)}', GradientType=0); // IE9 and down +} + +@mixin gradient-directional($start-color: #555, $end-color: #333, $deg: 45deg) { + background-repeat: repeat-x; + background-image: -webkit-linear-gradient($deg, $start-color, $end-color); // Safari 5.1-6, Chrome 10+ + background-image: linear-gradient($deg, $start-color, $end-color); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+ +} +@mixin gradient-horizontal-three-colors($start-color: #00b3ee, $mid-color: #7a43b6, $color-stop: 50%, $end-color: #c3325f) { + background-image: -webkit-linear-gradient(left, $start-color, $mid-color $color-stop, $end-color); + background-image: linear-gradient(to right, $start-color, $mid-color $color-stop, $end-color); + background-repeat: no-repeat; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#{ie-hex-str($start-color)}', endColorstr='#{ie-hex-str($end-color)}', GradientType=1); // IE9 and down, gets no color-stop at all for proper fallback +} +@mixin gradient-vertical-three-colors($start-color: #00b3ee, $mid-color: #7a43b6, $color-stop: 50%, $end-color: #c3325f) { + background-image: -webkit-linear-gradient($start-color, $mid-color $color-stop, $end-color); + background-image: linear-gradient($start-color, $mid-color $color-stop, $end-color); + background-repeat: no-repeat; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#{ie-hex-str($start-color)}', endColorstr='#{ie-hex-str($end-color)}', GradientType=0); // IE9 and down, gets no color-stop at all for proper fallback +} +@mixin gradient-radial($inner-color: #555, $outer-color: #333) { + background-image: -webkit-radial-gradient(circle, $inner-color, $outer-color); + background-image: radial-gradient(circle, $inner-color, $outer-color); + background-repeat: no-repeat; +} +@mixin gradient-striped($color: rgba(255,255,255,.15), $angle: 45deg) { + background-image: -webkit-linear-gradient($angle, $color 25%, transparent 25%, transparent 50%, $color 50%, $color 75%, transparent 75%, transparent); + background-image: linear-gradient($angle, $color 25%, transparent 25%, transparent 50%, $color 50%, $color 75%, transparent 75%, transparent); +} + +// Reset filters for IE +// +// When you need to remove a gradient background, do not forget to use this to reset +// the IE filter for IE9 and below. +@mixin reset-filter() { + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); +} + + + +// Retina images +// +// Short retina mixin for setting background-image and -size + +@mixin img-retina($file-1x, $file-2x, $width-1x, $height-1x) { + background-image: url(if($bootstrap-sass-asset-helper, twbs-image-path("#{$file-1x}"), "#{$file-1x}")); + + @media + only screen and (-webkit-min-device-pixel-ratio: 2), + only screen and ( min--moz-device-pixel-ratio: 2), + only screen and ( -o-min-device-pixel-ratio: 2/1), + only screen and ( min-device-pixel-ratio: 2), + only screen and ( min-resolution: 192dpi), + only screen and ( min-resolution: 2dppx) { + background-image: url(if($bootstrap-sass-asset-helper, twbs-image-path("#{$file-2x}"), "#{$file-2x}")); + background-size: $width-1x $height-1x; + } +} + + +// Responsive image +// +// Keep images from scaling beyond the width of their parents. + +@mixin img-responsive($display: block) { + display: $display; + max-width: 100%; // Part 1: Set a maximum relative to the parent + height: auto; // Part 2: Scale the height according to the width, otherwise you get stretching +} + + +// COMPONENT MIXINS +// -------------------------------------------------- + +// Horizontal dividers +// ------------------------- +// Dividers (basically an hr) within dropdowns and nav lists +@mixin nav-divider($color: #e5e5e5) { + height: 1px; + margin: (($line-height-computed / 2) - 1) 0; + overflow: hidden; + background-color: $color; +} + +// Panels +// ------------------------- +@mixin panel-variant($border, $heading-text-color, $heading-bg-color, $heading-border) { + border-color: $border; + + & > .panel-heading { + color: $heading-text-color; + background-color: $heading-bg-color; + border-color: $heading-border; + + + .panel-collapse .panel-body { + border-top-color: $border; + } + } + & > .panel-footer { + + .panel-collapse .panel-body { + border-bottom-color: $border; + } + } +} + +// Alerts +// ------------------------- +@mixin alert-variant($background, $border, $text-color) { + background-color: $background; + border-color: $border; + color: $text-color; + + hr { + border-top-color: darken($border, 5%); + } + .alert-link { + color: darken($text-color, 10%); + } +} + +// Tables +// ------------------------- +@mixin table-row-variant($state, $background) { + // Exact selectors below required to override `.table-striped` and prevent + // inheritance to nested tables. + .table > thead > tr, + .table > tbody > tr, + .table > tfoot > tr { + > td.#{$state}, + > th.#{$state}, + &.#{$state} > td, + &.#{$state} > th { + background-color: $background; + } + } + + // Hover states for `.table-hover` + // Note: this is not available for cells or rows within `thead` or `tfoot`. + .table-hover > tbody > tr { + > td.#{$state}:hover, + > th.#{$state}:hover, + &.#{$state}:hover > td, + &.#{$state}:hover > th { + background-color: darken($background, 5%); + } + } +} + +// List Groups +// ------------------------- +@mixin list-group-item-variant($state, $background, $color) { + .list-group-item-#{$state} { + color: $color; + background-color: $background; + + // [converter] extracted a& to a.list-group-item-#{$state} + } + + a.list-group-item-#{$state} { + color: $color; + + .list-group-item-heading { color: inherit; } + + &:hover, + &:focus { + color: $color; + background-color: darken($background, 5%); + } + &.active, + &.active:hover, + &.active:focus { + color: #fff; + background-color: $color; + border-color: $color; + } + } +} + +// Button variants +// ------------------------- +// Easily pump out default styles, as well as :hover, :focus, :active, +// and disabled options for all buttons +@mixin button-variant($color, $background, $border) { + color: $color; + background-color: $background; + border-color: $border; + + &:hover, + &:focus, + &:active, + &.active { + color: $color; + background-color: darken($background, 8%); + border-color: darken($border, 12%); + } + .open & { &.dropdown-toggle { + color: $color; + background-color: darken($background, 8%); + border-color: darken($border, 12%); + } } + &:active, + &.active { + background-image: none; + } + .open & { &.dropdown-toggle { + background-image: none; + } } + &.disabled, + &[disabled], + fieldset[disabled] & { + &, + &:hover, + &:focus, + &:active, + &.active { + background-color: $background; + border-color: $border; + } + } + + .badge { + color: $background; + background-color: $color; + } +} + +// Button sizes +// ------------------------- +@mixin button-size($padding-vertical, $padding-horizontal, $font-size, $line-height, $border-radius) { + padding: $padding-vertical $padding-horizontal; + font-size: $font-size; + line-height: $line-height; + border-radius: $border-radius; +} + +// Pagination +// ------------------------- +@mixin pagination-size($padding-vertical, $padding-horizontal, $font-size, $border-radius) { + > li { + > a, + > span { + padding: $padding-vertical $padding-horizontal; + font-size: $font-size; + } + &:first-child { + > a, + > span { + @include border-left-radius($border-radius); + } + } + &:last-child { + > a, + > span { + @include border-right-radius($border-radius); + } + } + } +} + +// Labels +// ------------------------- +@mixin label-variant($color) { + background-color: $color; + color: white; + &[href] { + &:hover, + &:focus { + background-color: darken($color, 10%); + } + } +} + +// Contextual backgrounds +// ------------------------- +// [converter] $parent hack +@mixin bg-variant($parent, $color) { + #{$parent} { + background-color: $color; + } + a#{$parent}:hover { + background-color: darken($color, 10%); + } +} + +// Typography +// ------------------------- +// [converter] $parent hack +@mixin text-emphasis-variant($parent, $color) { + #{$parent} { + color: $color; + } + a#{$parent}:hover { + color: darken($color, 10%); + } +} + +// Navbar vertical align +// ------------------------- +// Vertically center elements in the navbar. +// Example: an element has a height of 30px, so write out `.navbar-vertical-align(30px);` to calculate the appropriate top margin. +@mixin navbar-vertical-align($element-height) { + margin-top: (($navbar-height - $element-height) / 2); + margin-bottom: (($navbar-height - $element-height) / 2); +} + +// Progress bars +// ------------------------- +@mixin progress-bar-variant($color) { + background-color: $color; + .progress-striped & { + @include gradient-striped(); + } +} + +// Responsive utilities +// ------------------------- +// More easily include all the states for responsive-utilities.less. +// [converter] $parent hack +@mixin responsive-visibility($parent) { + #{$parent} { + display: block !important; + } + table#{$parent} { display: table; } + tr#{$parent} { display: table-row !important; } + th#{$parent}, + td#{$parent} { display: table-cell !important; } +} + +// [converter] $parent hack +@mixin responsive-invisibility($parent) { + #{$parent} { + display: none !important; + } +} + + +// Grid System +// ----------- + +// Centered container element +@mixin container-fixed() { + margin-right: auto; + margin-left: auto; + padding-left: ($grid-gutter-width / 2); + padding-right: ($grid-gutter-width / 2); + @include clearfix(); +} + +// Creates a wrapper for a series of columns +@mixin make-row($gutter: $grid-gutter-width) { + margin-left: ($gutter / -2); + margin-right: ($gutter / -2); + @include clearfix(); +} + +// Generate the extra small columns +@mixin make-xs-column($columns, $gutter: $grid-gutter-width) { + position: relative; + float: left; + width: percentage(($columns / $grid-columns)); + min-height: 1px; + padding-left: ($gutter / 2); + padding-right: ($gutter / 2); +} +@mixin make-xs-column-offset($columns) { + @media (min-width: $screen-xs-min) { + margin-left: percentage(($columns / $grid-columns)); + } +} +@mixin make-xs-column-push($columns) { + @media (min-width: $screen-xs-min) { + left: percentage(($columns / $grid-columns)); + } +} +@mixin make-xs-column-pull($columns) { + @media (min-width: $screen-xs-min) { + right: percentage(($columns / $grid-columns)); + } +} + + +// Generate the small columns +@mixin make-sm-column($columns, $gutter: $grid-gutter-width) { + position: relative; + min-height: 1px; + padding-left: ($gutter / 2); + padding-right: ($gutter / 2); + + @media (min-width: $screen-sm-min) { + float: left; + width: percentage(($columns / $grid-columns)); + } +} +@mixin make-sm-column-offset($columns) { + @media (min-width: $screen-sm-min) { + margin-left: percentage(($columns / $grid-columns)); + } +} +@mixin make-sm-column-push($columns) { + @media (min-width: $screen-sm-min) { + left: percentage(($columns / $grid-columns)); + } +} +@mixin make-sm-column-pull($columns) { + @media (min-width: $screen-sm-min) { + right: percentage(($columns / $grid-columns)); + } +} + + +// Generate the medium columns +@mixin make-md-column($columns, $gutter: $grid-gutter-width) { + position: relative; + min-height: 1px; + padding-left: ($gutter / 2); + padding-right: ($gutter / 2); + + @media (min-width: $screen-md-min) { + float: left; + width: percentage(($columns / $grid-columns)); + } +} +@mixin make-md-column-offset($columns) { + @media (min-width: $screen-md-min) { + margin-left: percentage(($columns / $grid-columns)); + } +} +@mixin make-md-column-push($columns) { + @media (min-width: $screen-md-min) { + left: percentage(($columns / $grid-columns)); + } +} +@mixin make-md-column-pull($columns) { + @media (min-width: $screen-md-min) { + right: percentage(($columns / $grid-columns)); + } +} + + +// Generate the large columns +@mixin make-lg-column($columns, $gutter: $grid-gutter-width) { + position: relative; + min-height: 1px; + padding-left: ($gutter / 2); + padding-right: ($gutter / 2); + + @media (min-width: $screen-lg-min) { + float: left; + width: percentage(($columns / $grid-columns)); + } +} +@mixin make-lg-column-offset($columns) { + @media (min-width: $screen-lg-min) { + margin-left: percentage(($columns / $grid-columns)); + } +} +@mixin make-lg-column-push($columns) { + @media (min-width: $screen-lg-min) { + left: percentage(($columns / $grid-columns)); + } +} +@mixin make-lg-column-pull($columns) { + @media (min-width: $screen-lg-min) { + right: percentage(($columns / $grid-columns)); + } +} + + +// Framework grid generation +// +// Used only by Bootstrap to generate the correct number of grid classes given +// any value of `$grid-columns`. + +// [converter] This is defined recursively in LESS, but Sass supports real loops +@mixin make-grid-columns() { + $list: ''; + $i: 1; + $list: ".col-xs-#{$i}, .col-sm-#{$i}, .col-md-#{$i}, .col-lg-#{$i}"; + @for $i from (1 + 1) through $grid-columns { + $list: "#{$list}, .col-xs-#{$i}, .col-sm-#{$i}, .col-md-#{$i}, .col-lg-#{$i}"; + } + #{$list} { + position: relative; + // Prevent columns from collapsing when empty + min-height: 1px; + // Inner gutter via padding + padding-left: ($grid-gutter-width / 2); + padding-right: ($grid-gutter-width / 2); + } +} + + +// [converter] This is defined recursively in LESS, but Sass supports real loops +@mixin float-grid-columns($class) { + $list: ''; + $i: 1; + $list: ".col-#{$class}-#{$i}"; + @for $i from (1 + 1) through $grid-columns { + $list: "#{$list}, .col-#{$class}-#{$i}"; + } + #{$list} { + float: left; + } +} + + +@mixin calc-grid-column($index, $class, $type) { + @if ($type == width) and ($index > 0) { + .col-#{$class}-#{$index} { + width: percentage(($index / $grid-columns)); + } + } + @if ($type == push) { + .col-#{$class}-push-#{$index} { + left: percentage(($index / $grid-columns)); + } + } + @if ($type == pull) { + .col-#{$class}-pull-#{$index} { + right: percentage(($index / $grid-columns)); + } + } + @if ($type == offset) { + .col-#{$class}-offset-#{$index} { + margin-left: percentage(($index / $grid-columns)); + } + } +} + +// [converter] This is defined recursively in LESS, but Sass supports real loops +@mixin loop-grid-columns($columns, $class, $type) { + @for $i from 0 through $columns { + @include calc-grid-column($i, $class, $type); + } +} + + +// Create grid for specific class +@mixin make-grid($class) { + @include float-grid-columns($class); + @include loop-grid-columns($grid-columns, $class, width); + @include loop-grid-columns($grid-columns, $class, pull); + @include loop-grid-columns($grid-columns, $class, push); + @include loop-grid-columns($grid-columns, $class, offset); +} + +// Form validation states +// +// Used in forms.less to generate the form validation CSS for warnings, errors, +// and successes. + +@mixin form-control-validation($text-color: #555, $border-color: #ccc, $background-color: #f5f5f5) { + // Color the label and help text + .help-block, + .control-label, + .radio, + .checkbox, + .radio-inline, + .checkbox-inline { + color: $text-color; + } + // Set the border and box shadow on specific inputs to match + .form-control { + border-color: $border-color; + @include box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); // Redeclare so transitions work + &:focus { + border-color: darken($border-color, 10%); + $shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px lighten($border-color, 20%); + @include box-shadow($shadow); + } + } + // Set validation states also for addons + .input-group-addon { + color: $text-color; + border-color: $border-color; + background-color: $background-color; + } + // Optional feedback icon + .form-control-feedback { + color: $text-color; + } +} + +// Form control focus state +// +// Generate a customized focus state and for any input with the specified color, +// which defaults to the `$input-focus-border` variable. +// +// We highly encourage you to not customize the default value, but instead use +// this to tweak colors on an as-needed basis. This aesthetic change is based on +// WebKit's default styles, but applicable to a wider range of browsers. Its +// usability and accessibility should be taken into account with any change. +// +// Example usage: change the default blue border and shadow to white for better +// contrast against a dark gray background. + +@mixin form-control-focus($color: $input-border-focus) { + $color-rgba: rgba(red($color), green($color), blue($color), .6); + &:focus { + border-color: $color; + outline: 0; + @include box-shadow(inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px $color-rgba); + } +} + +// Form control sizing +// +// Relative text size, padding, and border-radii changes for form controls. For +// horizontal sizing, wrap controls in the predefined grid classes. `<select>` +// element gets special love because it's special, and that's a fact! + +// [converter] $parent hack +@mixin input-size($parent, $input-height, $padding-vertical, $padding-horizontal, $font-size, $line-height, $border-radius) { + #{$parent} { + height: $input-height; + padding: $padding-vertical $padding-horizontal; + font-size: $font-size; + line-height: $line-height; + border-radius: $border-radius; + } + + select#{$parent} { + height: $input-height; + line-height: $input-height; + } + + textarea#{$parent}, + select[multiple]#{$parent} { + height: auto; + } +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_modals.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_modals.scss new file mode 100644 index 0000000000000000000000000000000000000000..e5f320ca91b01fd5f0af00a1fc995c3cf26176e2 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_modals.scss @@ -0,0 +1,139 @@ +// +// Modals +// -------------------------------------------------- + +// .modal-open - body class for killing the scroll +// .modal - container to scroll within +// .modal-dialog - positioning shell for the actual modal +// .modal-content - actual modal w/ bg and corners and shit + +// Kill the scroll on the body +.modal-open { + overflow: hidden; +} + +// Container that the modal scrolls within +.modal { + display: none; + overflow: auto; + overflow-y: scroll; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: $zindex-modal; + -webkit-overflow-scrolling: touch; + + // Prevent Chrome on Windows from adding a focus outline. For details, see + // https://github.com/twbs/bootstrap/pull/10951. + outline: 0; + + // When fading in the modal, animate it to slide down + &.fade .modal-dialog { + @include translate(0, -25%); + @include transition-transform(0.3s ease-out); + } + &.in .modal-dialog { @include translate(0, 0)} +} + +// Shell div to position the modal with bottom padding +.modal-dialog { + position: relative; + width: auto; + margin: 10px; +} + +// Actual modal +.modal-content { + position: relative; + background-color: $modal-content-bg; + border: 1px solid $modal-content-fallback-border-color; //old browsers fallback (ie8 etc) + border: 1px solid $modal-content-border-color; + border-radius: $border-radius-base; + @include box-shadow(0 3px 9px rgba(0,0,0,.5)); + background-clip: padding-box; + // Remove focus outline from opened modal + outline: none; +} + +// Modal background +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: $zindex-modal-background; + background-color: $modal-backdrop-bg; + // Fade for backdrop + &.fade { @include opacity(0); } + &.in { @include opacity($modal-backdrop-opacity); } +} + +// Modal header +// Top section of the modal w/ title and dismiss +.modal-header { + padding: $modal-title-padding; + border-bottom: 1px solid $modal-header-border-color; + min-height: ($modal-title-padding + $modal-title-line-height); +} +// Close icon +.modal-header .close { + margin-top: -2px; +} + +// Title text within header +.modal-title { + margin: 0; + line-height: $modal-title-line-height; +} + +// Modal body +// Where all modal content resides (sibling of .modal-header and .modal-footer) +.modal-body { + position: relative; + padding: $modal-inner-padding; +} + +// Footer (for actions) +.modal-footer { + margin-top: 15px; + padding: ($modal-inner-padding - 1) $modal-inner-padding $modal-inner-padding; + text-align: right; // right align buttons + border-top: 1px solid $modal-footer-border-color; + @include clearfix(); // clear it in case folks use .pull-* classes on buttons + + // Properly space out buttons + .btn + .btn { + margin-left: 5px; + margin-bottom: 0; // account for input[type="submit"] which gets the bottom margin like all other inputs + } + // but override that for button groups + .btn-group .btn + .btn { + margin-left: -1px; + } + // and override it for block buttons as well + .btn-block + .btn-block { + margin-left: 0; + } +} + +// Scale up the modal +@media (min-width: $screen-sm-min) { + // Automatically set modal's width for larger viewports + .modal-dialog { + width: $modal-md; + margin: 30px auto; + } + .modal-content { + @include box-shadow(0 5px 15px rgba(0,0,0,.5)); + } + + // Modal sizes + .modal-sm { width: $modal-sm; } +} + +@media (min-width: $screen-md-min) { + .modal-lg { width: $modal-lg; } +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_navbar.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_navbar.scss new file mode 100644 index 0000000000000000000000000000000000000000..cf2172c679cf316bb64a2c943c0e6d11cd6b3eff --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_navbar.scss @@ -0,0 +1,632 @@ +// +// Navbars +// -------------------------------------------------- + + +// Wrapper and base class +// +// Provide a static navbar from which we expand to create full-width, fixed, and +// other navbar variations. + +.navbar { + position: relative; + min-height: $navbar-height; // Ensure a navbar always shows (e.g., without a .navbar-brand in collapsed mode) + margin-bottom: $navbar-margin-bottom; + border: 1px solid transparent; + + // Prevent floats from breaking the navbar + @include clearfix(); + + @media (min-width: $grid-float-breakpoint) { + border-radius: $navbar-border-radius; + } +} + + +// Navbar heading +// +// Groups `.navbar-brand` and `.navbar-toggle` into a single component for easy +// styling of responsive aspects. + +.navbar-header { + @include clearfix(); + + @media (min-width: $grid-float-breakpoint) { + float: left; + } +} + + +// Navbar collapse (body) +// +// Group your navbar content into this for easy collapsing and expanding across +// various device sizes. By default, this content is collapsed when <768px, but +// will expand past that for a horizontal display. +// +// To start (on mobile devices) the navbar links, forms, and buttons are stacked +// vertically and include a `max-height` to overflow in case you have too much +// content for the user's viewport. + +.navbar-collapse { + max-height: $navbar-collapse-max-height; + overflow-x: visible; + padding-right: $navbar-padding-horizontal; + padding-left: $navbar-padding-horizontal; + border-top: 1px solid transparent; + box-shadow: inset 0 1px 0 rgba(255,255,255,.1); + @include clearfix(); + -webkit-overflow-scrolling: touch; + + &.in { + overflow-y: auto; + } + + @media (min-width: $grid-float-breakpoint) { + width: auto; + border-top: 0; + box-shadow: none; + + &.collapse { + display: block !important; + height: auto !important; + padding-bottom: 0; // Override default setting + overflow: visible !important; + } + + &.in { + overflow-y: visible; + } + + // Undo the collapse side padding for navbars with containers to ensure + // alignment of right-aligned contents. + .navbar-fixed-top &, + .navbar-static-top &, + .navbar-fixed-bottom & { + padding-left: 0; + padding-right: 0; + } + } +} + + +// Both navbar header and collapse +// +// When a container is present, change the behavior of the header and collapse. + +.container, +.container-fluid { + > .navbar-header, + > .navbar-collapse { + margin-right: -$navbar-padding-horizontal; + margin-left: -$navbar-padding-horizontal; + + @media (min-width: $grid-float-breakpoint) { + margin-right: 0; + margin-left: 0; + } + } +} + + +// +// Navbar alignment options +// +// Display the navbar across the entirety of the page or fixed it to the top or +// bottom of the page. + +// Static top (unfixed, but 100% wide) navbar +.navbar-static-top { + z-index: $zindex-navbar; + border-width: 0 0 1px; + + @media (min-width: $grid-float-breakpoint) { + border-radius: 0; + } +} + +// Fix the top/bottom navbars when screen real estate supports it +.navbar-fixed-top, +.navbar-fixed-bottom { + position: fixed; + right: 0; + left: 0; + z-index: $zindex-navbar-fixed; + + // Undo the rounded corners + @media (min-width: $grid-float-breakpoint) { + border-radius: 0; + } +} +.navbar-fixed-top { + top: 0; + border-width: 0 0 1px; +} +.navbar-fixed-bottom { + bottom: 0; + margin-bottom: 0; // override .navbar defaults + border-width: 1px 0 0; +} + + +// Brand/project name + +.navbar-brand { + float: left; + padding: $navbar-padding-vertical $navbar-padding-horizontal; + font-size: $font-size-large; + line-height: $line-height-computed; + height: $navbar-height; + + &:hover, + &:focus { + text-decoration: none; + } + + @media (min-width: $grid-float-breakpoint) { + .navbar > .container &, + .navbar > .container-fluid & { + margin-left: -$navbar-padding-horizontal; + } + } +} + + +// Navbar toggle +// +// Custom button for toggling the `.navbar-collapse`, powered by the collapse +// JavaScript plugin. + +.navbar-toggle { + position: relative; + float: right; + margin-right: $navbar-padding-horizontal; + padding: 9px 10px; + @include navbar-vertical-align(34px); + background-color: transparent; + background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 + border: 1px solid transparent; + border-radius: $border-radius-base; + + // We remove the `outline` here, but later compensate by attaching `:hover` + // styles to `:focus`. + &:focus { + outline: none; + } + + // Bars + .icon-bar { + display: block; + width: 22px; + height: 2px; + border-radius: 1px; + } + .icon-bar + .icon-bar { + margin-top: 4px; + } + + @media (min-width: $grid-float-breakpoint) { + display: none; + } +} + + +// Navbar nav links +// +// Builds on top of the `.nav` components with its own modifier class to make +// the nav the full height of the horizontal nav (above 768px). + +.navbar-nav { + margin: ($navbar-padding-vertical / 2) (-$navbar-padding-horizontal); + + > li > a { + padding-top: 10px; + padding-bottom: 10px; + line-height: $line-height-computed; + } + + @media (max-width: $grid-float-breakpoint-max) { + // Dropdowns get custom display when collapsed + .open .dropdown-menu { + position: static; + float: none; + width: auto; + margin-top: 0; + background-color: transparent; + border: 0; + box-shadow: none; + > li > a, + .dropdown-header { + padding: 5px 15px 5px 25px; + } + > li > a { + line-height: $line-height-computed; + &:hover, + &:focus { + background-image: none; + } + } + } + } + + // Uncollapse the nav + @media (min-width: $grid-float-breakpoint) { + float: left; + margin: 0; + + > li { + float: left; + > a { + padding-top: $navbar-padding-vertical; + padding-bottom: $navbar-padding-vertical; + } + } + + &.navbar-right:last-child { + margin-right: -$navbar-padding-horizontal; + } + } +} + + +// Component alignment +// +// Repurpose the pull utilities as their own navbar utilities to avoid specificity +// issues with parents and chaining. Only do this when the navbar is uncollapsed +// though so that navbar contents properly stack and align in mobile. + +@media (min-width: $grid-float-breakpoint) { + .navbar-left { + float: left !important; + } + .navbar-right { + float: right !important; + } +} + + +// Navbar form +// +// Extension of the `.form-inline` with some extra flavor for optimum display in +// our navbars. + +.navbar-form { + margin-left: -$navbar-padding-horizontal; + margin-right: -$navbar-padding-horizontal; + padding: 10px $navbar-padding-horizontal; + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; + $shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.1); + @include box-shadow($shadow); + + // Mixin behavior for optimum display + @extend .form-inline; + + .form-group { + @media (max-width: $grid-float-breakpoint-max) { + margin-bottom: 5px; + } + } + + // Vertically center in expanded, horizontal navbar + @include navbar-vertical-align($input-height-base); + + // Undo 100% width for pull classes + @media (min-width: $grid-float-breakpoint) { + width: auto; + border: 0; + margin-left: 0; + margin-right: 0; + padding-top: 0; + padding-bottom: 0; + @include box-shadow(none); + + // Outdent the form if last child to line up with content down the page + &.navbar-right:last-child { + margin-right: -$navbar-padding-horizontal; + } + } +} + + +// Dropdown menus + +// Menu position and menu carets +.navbar-nav > li > .dropdown-menu { + margin-top: 0; + @include border-top-radius(0); +} +// Menu position and menu caret support for dropups via extra dropup class +.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu { + @include border-bottom-radius(0); +} + + +// Buttons in navbars +// +// Vertically center a button within a navbar (when *not* in a form). + +.navbar-btn { + @include navbar-vertical-align($input-height-base); + + &.btn-sm { + @include navbar-vertical-align($input-height-small); + } + &.btn-xs { + @include navbar-vertical-align(22); + } +} + + +// Text in navbars +// +// Add a class to make any element properly align itself vertically within the navbars. + +.navbar-text { + @include navbar-vertical-align($line-height-computed); + + @media (min-width: $grid-float-breakpoint) { + float: left; + margin-left: $navbar-padding-horizontal; + margin-right: $navbar-padding-horizontal; + + // Outdent the form if last child to line up with content down the page + &.navbar-right:last-child { + margin-right: 0; + } + } +} + +// Alternate navbars +// -------------------------------------------------- + +// Default navbar +.navbar-default { + background-color: $navbar-default-bg; + border-color: $navbar-default-border; + + .navbar-brand { + color: $navbar-default-brand-color; + &:hover, + &:focus { + color: $navbar-default-brand-hover-color; + background-color: $navbar-default-brand-hover-bg; + } + } + + .navbar-text { + color: $navbar-default-color; + } + + .navbar-nav { + > li > a { + color: $navbar-default-link-color; + + &:hover, + &:focus { + color: $navbar-default-link-hover-color; + background-color: $navbar-default-link-hover-bg; + } + } + > .has-dropdown:not(.active):hover { + >a:first-child { + color: $navbar-default-link-hover-color; + background-color: $navbar-default-link-hover-bg; + } + } + > .active > a { + &, + &:hover, + &:focus { + color: $navbar-default-link-active-color; + background-color: $navbar-default-link-active-bg; + } + } + > .disabled > a { + &, + &:hover, + &:focus { + color: $navbar-default-link-disabled-color; + background-color: $navbar-default-link-disabled-bg; + } + } + } + + .navbar-toggle { + border-color: $navbar-default-toggle-border-color; + &:hover, + &:focus { + background-color: $navbar-default-toggle-hover-bg; + } + .icon-bar { + background-color: $navbar-default-toggle-icon-bar-bg; + } + } + + .navbar-collapse, + .navbar-form { + border-color: $navbar-default-border; + } + + // Dropdown menu items + .navbar-nav { + // Remove background color from open dropdown + > .open > a { + &, + &:hover, + &:focus { + background-color: $navbar-default-link-active-bg; + color: $navbar-default-link-active-color; + } + } + + @media (max-width: $grid-float-breakpoint-max) { + // Dropdowns get custom display when collapsed + .open .dropdown-menu { + > li > a { + color: $navbar-default-link-color; + &:hover, + &:focus { + color: $navbar-default-link-hover-color; + background-color: $navbar-default-link-hover-bg; + } + } + > .active > a { + &, + &:hover, + &:focus { + color: $navbar-default-link-active-color; + background-color: $navbar-default-link-active-bg; + } + } + > .disabled > a { + &, + &:hover, + &:focus { + color: $navbar-default-link-disabled-color; + background-color: $navbar-default-link-disabled-bg; + } + } + } + } + } + + + // Links in navbars + // + // Add a class to ensure links outside the navbar nav are colored correctly. + + .navbar-link { + color: $navbar-default-link-color; + &:hover { + color: $navbar-default-link-hover-color; + } + } + +} + +// Inverse navbar + +.navbar-inverse { + background-color: $navbar-inverse-bg; + border-color: $navbar-inverse-border; + + .navbar-brand { + color: $navbar-inverse-brand-color; + &:hover, + &:focus { + color: $navbar-inverse-brand-hover-color; + background-color: $navbar-inverse-brand-hover-bg; + } + } + + .navbar-text { + color: $navbar-inverse-color; + } + + .navbar-nav { + > li > a { + color: $navbar-inverse-link-color; + + &:hover, + &:focus { + color: $navbar-inverse-link-hover-color; + background-color: $navbar-inverse-link-hover-bg; + } + } + > li.has-dropdown:hover { + >a:first-child { + color: $navbar-inverse-link-hover-color; + background-color: $navbar-inverse-link-hover-bg; + } + } + > .active > a { + &, + &:hover, + &:focus { + color: $navbar-inverse-link-active-color; + background-color: $navbar-inverse-link-active-bg; + } + } + > .disabled > a { + &, + &:hover, + &:focus { + color: $navbar-inverse-link-disabled-color; + background-color: $navbar-inverse-link-disabled-bg; + } + } + } + + // Darken the responsive nav toggle + .navbar-toggle { + border-color: $navbar-inverse-toggle-border-color; + &:hover, + &:focus { + background-color: $navbar-inverse-toggle-hover-bg; + } + .icon-bar { + background-color: $navbar-inverse-toggle-icon-bar-bg; + } + } + + .navbar-collapse, + .navbar-form { + border-color: darken($navbar-inverse-bg, 7%); + } + + // Dropdowns + .navbar-nav { + > .open > a { + &, + &:hover, + &:focus { + background-color: $navbar-inverse-link-active-bg; + color: $navbar-inverse-link-active-color; + } + } + + @media (max-width: $grid-float-breakpoint-max) { + // Dropdowns get custom display + .open .dropdown-menu { + > .dropdown-header { + border-color: $navbar-inverse-border; + } + .divider { + background-color: $navbar-inverse-border; + } + > li > a { + color: $navbar-inverse-link-color; + &:hover, + &:focus { + color: $navbar-inverse-link-hover-color; + background-color: $navbar-inverse-link-hover-bg; + } + } + > .active > a { + &, + &:hover, + &:focus { + color: $navbar-inverse-link-active-color; + background-color: $navbar-inverse-link-active-bg; + } + } + > .disabled > a { + &, + &:hover, + &:focus { + color: $navbar-inverse-link-disabled-color; + background-color: $navbar-inverse-link-disabled-bg; + } + } + } + } + } + + .navbar-link { + color: $navbar-inverse-link-color; + &:hover { + color: $navbar-inverse-link-hover-color; + } + } + +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_navs.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_navs.scss new file mode 100644 index 0000000000000000000000000000000000000000..8ebf4a0e4f4d7de5776c2581cbef9f9790d958c5 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_navs.scss @@ -0,0 +1,246 @@ +// +// Navs +// -------------------------------------------------- + + +// Base class +// -------------------------------------------------- + +.nav { + margin-bottom: 0; + padding-left: 0; // Override default ul/ol + list-style: none; + @include clearfix(); + + > li { + position: relative; + display: block; + + > a { + position: relative; + display: block; + padding: $nav-link-padding; + &:hover, + &:focus { + text-decoration: none; + background-color: $nav-link-hover-bg; + } + } + + // Disabled state sets text to gray and nukes hover/tab effects + &.disabled > a { + color: $nav-disabled-link-color; + + &:hover, + &:focus { + color: $nav-disabled-link-hover-color; + text-decoration: none; + background-color: transparent; + cursor: not-allowed; + } + } + } + + // Open dropdowns + .open > a { + &, + &:hover, + &:focus { + background-color: $nav-link-hover-bg; + border-color: $link-color; + } + } + + // Nav dividers (deprecated with v3.0.1) + // + // This should have been removed in v3 with the dropping of `.nav-list`, but + // we missed it. We don't currently support this anywhere, but in the interest + // of maintaining backward compatibility in case you use it, it's deprecated. + .nav-divider { + @include nav-divider(); + } + + // Prevent IE8 from misplacing imgs + // + // See https://github.com/h5bp/html5-boilerplate/issues/984#issuecomment-3985989 + > li > a > img { + max-width: none; + } +} + + +// Tabs +// ------------------------- + +// Give the tabs something to sit on +.nav-tabs { + border-bottom: 1px solid $nav-tabs-border-color; + > li { + float: left; + // Make the list-items overlay the bottom border + margin-bottom: -1px; + + // Actual tabs (as links) + > a { + margin-right: 2px; + line-height: $line-height-base; + border: 1px solid transparent; + border-radius: $border-radius-base $border-radius-base 0 0; + color: $nav-tabs-link-color; + &:hover, + &:focus{ + background: inherit; + //color: inherit; + border-color: $nav-tabs-link-hover-border-color $nav-tabs-link-hover-border-color $nav-tabs-border-color; + } + } + + // Active state, and its :hover to override normal :hover + &.active > a { + &, + &:hover, + &:focus { + color: $nav-tabs-active-link-hover-color; + background-color: $nav-tabs-active-link-hover-bg; + border: 1px solid $nav-tabs-active-link-hover-border-color; + border-bottom-color: transparent; + cursor: default; + } + } + } + // pulling this in mainly for less shorthand + &.nav-justified { + @extend .nav-justified; + @extend .nav-tabs-justified; + } +} + + +// Pills +// ------------------------- +.nav-pills { + > li { + float: left; + + // Links rendered as pills + > a { + border-radius: $nav-pills-border-radius; + } + + li { + margin-left: 2px; + } + + // Active state + &.active > a { + &, + &:hover, + &:focus { + color: $nav-pills-active-link-hover-color; + background-color: $nav-pills-active-link-hover-bg; + } + } + } +} + + +// Stacked pills +.nav-stacked { + > li { + float: none; + + li { + margin-top: 2px; + margin-left: 0; // no need for this gap between nav items + } + } +} + + +// Nav variations +// -------------------------------------------------- + +// Justified nav links +// ------------------------- + +.nav-justified { + width: 100%; + + > li { + float: none; + > a { + text-align: center; + margin-bottom: 5px; + } + } + + > .dropdown .dropdown-menu { + top: auto; + left: auto; + } + + @media (min-width: $screen-sm-min) { + > li { + display: table-cell; + width: 1%; + > a { + margin-bottom: 0; + } + } + } +} + +// Move borders to anchors instead of bottom of list +// +// Mixin for adding on top the shared `.nav-justified` styles for our tabs +.nav-tabs-justified { + border-bottom: 0; + + > li > a { + // Override margin from .nav-tabs + margin-right: 0; + border-radius: $border-radius-base; + } + + > .active > a, + > .active > a:hover, + > .active > a:focus { + border: 1px solid $nav-tabs-justified-link-border-color; + } + + @media (min-width: $screen-sm-min) { + > li > a { + border-bottom: 1px solid $nav-tabs-justified-link-border-color; + border-radius: $border-radius-base $border-radius-base 0 0; + } + > .active > a, + > .active > a:hover, + > .active > a:focus { + border-bottom-color: $nav-tabs-justified-active-link-border-color; + } + } +} + + +// Tabbable tabs +// ------------------------- + +// Hide tabbable panes to start, show them when `.active` +.tab-content { + > .tab-pane { + display: none; + } + > .active { + display: block; + } +} + + +// Dropdowns +// ------------------------- + +// Specific dropdowns +.nav-tabs .dropdown-menu { + // make dropdown border overlap tab border + margin-top: -1px; + // Remove the top rounded corners here since there is a hard edge above the menu + @include border-top-radius(0); +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_normalize.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_normalize.scss new file mode 100644 index 0000000000000000000000000000000000000000..024e257c1a13532e7d5579b0ea4bb5915d21e4a6 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_normalize.scss @@ -0,0 +1,423 @@ +/*! normalize.css v3.0.0 | MIT License | git.io/normalize */ + +// +// 1. Set default font family to sans-serif. +// 2. Prevent iOS text size adjust after orientation change, without disabling +// user zoom. +// + +html { + font-family: sans-serif; // 1 + -ms-text-size-adjust: 100%; // 2 + -webkit-text-size-adjust: 100%; // 2 +} + +// +// Remove default margin. +// + +body { + margin: 0; +} + +// HTML5 display definitions +// ========================================================================== + +// +// Correct `block` display not defined in IE 8/9. +// + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +nav, +section, +summary { + display: block; +} + +// +// 1. Correct `inline-block` display not defined in IE 8/9. +// 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. +// + +audio, +canvas, +progress, +video { + display: inline-block; // 1 + vertical-align: baseline; // 2 +} + +// +// Prevent modern browsers from displaying `audio` without controls. +// Remove excess height in iOS 5 devices. +// + +audio:not([controls]) { + display: none; + height: 0; +} + +// +// Address `[hidden]` styling not present in IE 8/9. +// Hide the `template` element in IE, Safari, and Firefox < 22. +// + +[hidden], +template { + display: none; +} + +// Links +// ========================================================================== + +// +// Remove the gray background color from active links in IE 10. +// + +a { + background: transparent; +} + +// +// Improve readability when focused and also mouse hovered in all browsers. +// + +a:active, +a:hover { + outline: 0; +} + +// Text-level semantics +// ========================================================================== + +// +// Address styling not present in IE 8/9, Safari 5, and Chrome. +// + +abbr[title] { + border-bottom: 1px dotted; +} + +// +// Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome. +// + +b, +strong { + font-weight: bold; +} + +// +// Address styling not present in Safari 5 and Chrome. +// + +dfn { + font-style: italic; +} + +// +// Address variable `h1` font-size and margin within `section` and `article` +// contexts in Firefox 4+, Safari 5, and Chrome. +// + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +// +// Address styling not present in IE 8/9. +// + +mark { + background: #ff0; + color: #000; +} + +// +// Address inconsistent and variable font size in all browsers. +// + +small { + font-size: 80%; +} + +// +// Prevent `sub` and `sup` affecting `line-height` in all browsers. +// + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +// Embedded content +// ========================================================================== + +// +// Remove border when inside `a` element in IE 8/9. +// + +img { + border: 0; +} + +// +// Correct overflow displayed oddly in IE 9. +// + +svg:not(:root) { + overflow: hidden; +} + +// Grouping content +// ========================================================================== + +// +// Address margin not present in IE 8/9 and Safari 5. +// + +figure { + margin: 1em 40px; +} + +// +// Address differences between Firefox and other browsers. +// + +hr { + -moz-box-sizing: content-box; + box-sizing: content-box; + height: 0; +} + +// +// Contain overflow in all browsers. +// + +pre { + overflow: auto; +} + +// +// Address odd `em`-unit font size rendering in all browsers. +// + +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} + +// Forms +// ========================================================================== + +// +// Known limitation: by default, Chrome and Safari on OS X allow very limited +// styling of `select`, unless a `border` property is set. +// + +// +// 1. Correct color not being inherited. +// Known issue: affects color of disabled elements. +// 2. Correct font properties not being inherited. +// 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome. +// + +button, +input, +optgroup, +select, +textarea { + color: inherit; // 1 + font: inherit; // 2 + margin: 0; // 3 +} + +// +// Address `overflow` set to `hidden` in IE 8/9/10. +// + +button { + overflow: visible; +} + +// +// Address inconsistent `text-transform` inheritance for `button` and `select`. +// All other form control elements do not inherit `text-transform` values. +// Correct `button` style inheritance in Firefox, IE 8+, and Opera +// Correct `select` style inheritance in Firefox. +// + +button, +select { + text-transform: none; +} + +// +// 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` +// and `video` controls. +// 2. Correct inability to style clickable `input` types in iOS. +// 3. Improve usability and consistency of cursor style between image-type +// `input` and others. +// + +button, +html input[type="button"], // 1 +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; // 2 + cursor: pointer; // 3 +} + +// +// Re-set default cursor for disabled elements. +// + +button[disabled], +html input[disabled] { + cursor: default; +} + +// +// Remove inner padding and border in Firefox 4+. +// + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +// +// Address Firefox 4+ setting `line-height` on `input` using `!important` in +// the UA stylesheet. +// + +input { + line-height: normal; +} + +// +// It's recommended that you don't attempt to style these elements. +// Firefox's implementation doesn't respect box-sizing, padding, or width. +// +// 1. Address box sizing set to `content-box` in IE 8/9/10. +// 2. Remove excess padding in IE 8/9/10. +// + +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; // 1 + padding: 0; // 2 +} + +// +// Fix the cursor style for Chrome's increment/decrement buttons. For certain +// `font-size` values of the `input`, it causes the cursor style of the +// decrement button to change from `default` to `text`. +// + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +// +// 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. +// 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome +// (include `-moz` to future-proof). +// + +input[type="search"] { + -webkit-appearance: textfield; // 1 + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; // 2 + box-sizing: content-box; +} + +// +// Remove inner padding and search cancel button in Safari and Chrome on OS X. +// Safari (but not Chrome) clips the cancel button when the search input has +// padding (and `textfield` appearance). +// + +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +// +// Define consistent border, margin, and padding. +// + +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +// +// 1. Correct `color` not being inherited in IE 8/9. +// 2. Remove padding so people aren't caught out if they zero out fieldsets. +// + +legend { + border: 0; // 1 + padding: 0; // 2 +} + +// +// Remove default vertical scrollbar in IE 8/9. +// + +textarea { + overflow: auto; +} + +// +// Don't inherit the `font-weight` (applied by a rule above). +// NOTE: the default cannot safely be changed in Chrome and Safari on OS X. +// + +optgroup { + font-weight: bold; +} + +// Tables +// ========================================================================== + +// +// Remove most spacing between table cells. +// + +table { + border-collapse: collapse; + border-spacing: 0; +} + +td, +th { + padding: 0; +} \ No newline at end of file diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_pager.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_pager.scss new file mode 100644 index 0000000000000000000000000000000000000000..6531fe6f89f4140b159be5fefa334f6f078aac93 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_pager.scss @@ -0,0 +1,55 @@ +// +// Pager pagination +// -------------------------------------------------- + + +.pager { + padding-left: 0; + margin: $line-height-computed 0; + list-style: none; + text-align: center; + @include clearfix(); + li { + display: inline; + > a, + > span { + display: inline-block; + padding: 5px 14px; + background-color: $pager-bg; + border: 1px solid $pager-border; + border-radius: $pager-border-radius; + } + + > a:hover, + > a:focus { + text-decoration: none; + background-color: $pager-hover-bg; + } + } + + .next { + > a, + > span { + float: right; + } + } + + .previous { + > a, + > span { + float: left; + } + } + + .disabled { + > a, + > a:hover, + > a:focus, + > span { + color: $pager-disabled-color; + background-color: $pager-bg; + cursor: not-allowed; + } + } + +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_pagination.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_pagination.scss new file mode 100644 index 0000000000000000000000000000000000000000..44c12226b0da61ccbddde1b12b43aec22df41754 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_pagination.scss @@ -0,0 +1,88 @@ +// +// Pagination (multiple pages) +// -------------------------------------------------- +.pagination { + display: inline-block; + padding-left: 0; + margin: $line-height-computed 0; + border-radius: $border-radius-base; + + > li { + display: inline; // Remove list-style and block-level defaults + > a, + > span { + position: relative; + float: left; // Collapse white-space + padding: $padding-base-vertical $padding-base-horizontal; + line-height: $line-height-base; + text-decoration: none; + color: $pagination-color; + background-color: $pagination-bg; + border: 1px solid $pagination-border; + margin-left: -1px; + } + &:first-child { + > a, + > span { + margin-left: 0; + @include border-left-radius($border-radius-base); + } + } + &:last-child { + > a, + > span { + @include border-right-radius($border-radius-base); + } + } + } + + > li > a, + > li > span { + &:hover, + &:focus { + color: $pagination-hover-color; + background-color: $pagination-hover-bg; + border-color: $pagination-hover-border; + } + } + + > .active > a, + > .active > span { + &, + &:hover, + &:focus { + z-index: 2; + color: $pagination-active-color; + background-color: $pagination-active-bg; + border-color: $pagination-active-border; + cursor: default; + } + } + + > .disabled { + > span, + > span:hover, + > span:focus, + > a, + > a:hover, + > a:focus { + color: $pagination-disabled-color; + background-color: $pagination-disabled-bg; + border-color: $pagination-disabled-border; + cursor: not-allowed; + } + } +} + +// Sizing +// -------------------------------------------------- + +// Large +.pagination-lg { + @include pagination-size($padding-large-vertical, $padding-large-horizontal, $font-size-large, $border-radius-large); +} + +// Small +.pagination-sm { + @include pagination-size($padding-small-vertical, $padding-small-horizontal, $font-size-small, $border-radius-small); +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_panels.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_panels.scss new file mode 100644 index 0000000000000000000000000000000000000000..0ab992541e2860cea3187f96ae5a730845ac3ada --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_panels.scss @@ -0,0 +1,241 @@ +// +// Panels +// -------------------------------------------------- + + +// Base class +.panel { + margin-bottom: $line-height-computed; + background-color: $panel-bg; + border: 1px solid transparent; + border-radius: $panel-border-radius; + @include box-shadow(0 1px 1px rgba(0,0,0,.05)); +} + +// Panel contents +.panel-body { + padding: $panel-body-padding; + @include clearfix(); +} + +// Optional heading +.panel-heading { + padding: 10px 15px; + border-bottom: 1px solid transparent; + @include border-top-radius(($panel-border-radius - 1)); + + > .dropdown .dropdown-toggle { + color: inherit; + } +} + +// Within heading, strip any `h*` tag of its default margins for spacing. +.panel-title { + margin-top: 0; + margin-bottom: 0; + font-size: ceil(($font-size-base * 1.125)); + color: inherit; + + > a { + color: inherit; + } +} + +// Optional footer (stays gray in every modifier class) +.panel-footer { + padding: 10px 15px; + background-color: $panel-footer-bg; + border-top: 1px solid $panel-inner-border; + @include border-bottom-radius(($panel-border-radius - 1)); +} + + +// List groups in panels +// +// By default, space out list group content from panel headings to account for +// any kind of custom content between the two. + +.panel { + > .list-group { + margin-bottom: 0; + + .list-group-item { + border-width: 1px 0; + border-radius: 0; + } + + // Add border top radius for first one + &:first-child { + .list-group-item:first-child { + border-top: 0; + @include border-top-radius(($panel-border-radius - 1)); + } + } + // Add border bottom radius for last one + &:last-child { + .list-group-item:last-child { + border-bottom: 0; + @include border-bottom-radius(($panel-border-radius - 1)); + } + } + } +} +// Collapse space between when there's no additional content. +.panel-heading + .list-group { + .list-group-item:first-child { + border-top-width: 0; + } +} + + +// Tables in panels +// +// Place a non-bordered `.table` within a panel (not within a `.panel-body`) and +// watch it go full width. + +.panel { + > .table, + > .table-responsive > .table { + margin-bottom: 0; + } + // Add border top radius for first one + > .table:first-child, + > .table-responsive:first-child > .table:first-child { + @include border-top-radius(($panel-border-radius - 1)); + + > thead:first-child, + > tbody:first-child { + > tr:first-child { + td:first-child, + th:first-child { + border-top-left-radius: ($panel-border-radius - 1); + } + td:last-child, + th:last-child { + border-top-right-radius: ($panel-border-radius - 1); + } + } + } + } + // Add border bottom radius for last one + > .table:last-child, + > .table-responsive:last-child > .table:last-child { + @include border-bottom-radius(($panel-border-radius - 1)); + + > tbody:last-child, + > tfoot:last-child { + > tr:last-child { + td:first-child, + th:first-child { + border-bottom-left-radius: ($panel-border-radius - 1); + } + td:last-child, + th:last-child { + border-bottom-right-radius: ($panel-border-radius - 1); + } + } + } + } + > .panel-body + .table, + > .panel-body + .table-responsive { + border-top: 1px solid $table-border-color; + } + > .table > tbody:first-child > tr:first-child th, + > .table > tbody:first-child > tr:first-child td { + border-top: 0; + } + > .table-bordered, + > .table-responsive > .table-bordered { + border: 0; + > thead, + > tbody, + > tfoot { + > tr { + > th:first-child, + > td:first-child { + border-left: 0; + } + > th:last-child, + > td:last-child { + border-right: 0; + } + } + } + > thead, + > tbody { + > tr:first-child { + > td, + > th { + border-bottom: 0; + } + } + } + > tbody, + > tfoot { + > tr:last-child { + > td, + > th { + border-bottom: 0; + } + } + } + } + > .table-responsive { + border: 0; + margin-bottom: 0; + } +} + + +// Collapsable panels (aka, accordion) +// +// Wrap a series of panels in `.panel-group` to turn them into an accordion with +// the help of our collapse JavaScript plugin. + +.panel-group { + margin-bottom: $line-height-computed; + + // Tighten up margin so it's only between panels + .panel { + margin-bottom: 0; + border-radius: $panel-border-radius; + overflow: hidden; // crop contents when collapsed + + .panel { + margin-top: 5px; + } + } + + .panel-heading { + border-bottom: 0; + + .panel-collapse .panel-body { + border-top: 1px solid $panel-inner-border; + } + } + .panel-footer { + border-top: 0; + + .panel-collapse .panel-body { + border-bottom: 1px solid $panel-inner-border; + } + } +} + + +// Contextual variations +.panel-default { + @include panel-variant($panel-default-border, $panel-default-text, $panel-default-heading-bg, $panel-default-border); +} +.panel-primary { + @include panel-variant($panel-primary-border, $panel-primary-text, $panel-primary-heading-bg, $panel-primary-border); +} +.panel-success { + @include panel-variant($panel-success-border, $panel-success-text, $panel-success-heading-bg, $panel-success-border); +} +.panel-info { + @include panel-variant($panel-info-border, $panel-info-text, $panel-info-heading-bg, $panel-info-border); +} +.panel-warning { + @include panel-variant($panel-warning-border, $panel-warning-text, $panel-warning-heading-bg, $panel-warning-border); +} +.panel-danger { + @include panel-variant($panel-danger-border, $panel-danger-text, $panel-danger-heading-bg, $panel-danger-border); +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_popovers.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_popovers.scss new file mode 100644 index 0000000000000000000000000000000000000000..abd86d2f2e103fbb1f9928fae3cef4019130998a --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_popovers.scss @@ -0,0 +1,133 @@ +// +// Popovers +// -------------------------------------------------- + + +.popover { + position: absolute; + top: 0; + left: 0; + z-index: $zindex-popover; + display: none; + max-width: $popover-max-width; + padding: 1px; + text-align: left; // Reset given new insertion method + background-color: $popover-bg; + background-clip: padding-box; + border: 1px solid $popover-fallback-border-color; + border: 1px solid $popover-border-color; + border-radius: $border-radius-small; + @include box-shadow(0 5px 10px rgba(0,0,0,.2)); + + // Overrides for proper insertion + white-space: normal; + + // Offset the popover to account for the popover arrow + &.top { margin-top: -$popover-arrow-width; } + &.right { margin-left: $popover-arrow-width; } + &.bottom { margin-top: $popover-arrow-width; } + &.left { margin-left: -$popover-arrow-width; } +} + +.popover-title { + margin: 0; // reset heading margin + padding: 8px 14px; + font-size: $font-size-base; + font-weight: normal; + line-height: 18px; + background-color: $popover-title-bg; + border-bottom: 1px solid darken($popover-title-bg, 5%); + border-radius: $border-radius-small $border-radius-small 0 0; +} + +.popover-content { + padding: 5px; +} + +// Arrows +// +// .arrow is outer, .arrow:after is inner + +.popover > .arrow { + &, + &:after { + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; + } +} +.popover > .arrow { + border-width: $popover-arrow-outer-width; +} +.popover > .arrow:after { + border-width: $popover-arrow-width; + content: ""; +} + +.popover { + &.top > .arrow { + left: 50%; + margin-left: -$popover-arrow-outer-width; + border-bottom-width: 0; + border-top-color: $popover-arrow-outer-fallback-color; // IE8 fallback + border-top-color: $popover-arrow-outer-color; + bottom: -$popover-arrow-outer-width; + &:after { + content: " "; + bottom: 1px; + margin-left: -$popover-arrow-width; + border-bottom-width: 0; + border-top-color: $popover-arrow-color; + } + } + &.right > .arrow { + top: 50%; + left: -$popover-arrow-outer-width; + margin-top: -$popover-arrow-outer-width; + border-left-width: 0; + border-right-color: $popover-arrow-outer-fallback-color; // IE8 fallback + border-right-color: $popover-arrow-outer-color; + &:after { + content: " "; + left: 1px; + bottom: -$popover-arrow-width; + border-left-width: 0; + border-right-color: $popover-arrow-color; + } + } + &.bottom > .arrow { + left: 50%; + margin-left: -$popover-arrow-outer-width; + border-top-width: 0; + border-bottom-color: $popover-arrow-outer-fallback-color; // IE8 fallback + border-bottom-color: $popover-arrow-outer-color; + top: -$popover-arrow-outer-width; + &:after { + content: " "; + top: 1px; + margin-left: -$popover-arrow-width; + border-top-width: 0; + border-bottom-color: $popover-arrow-color; + } + } + + &.left > .arrow { + top: 50%; + right: -$popover-arrow-outer-width; + margin-top: -$popover-arrow-outer-width; + border-right-width: 0; + border-left-color: $popover-arrow-outer-fallback-color; // IE8 fallback + border-left-color: $popover-arrow-outer-color; + &:after { + content: " "; + right: 1px; + border-right-width: 0; + border-left-color: $popover-arrow-color; + bottom: -$popover-arrow-width; + } + } + +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_print.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_print.scss new file mode 100644 index 0000000000000000000000000000000000000000..3655d03953ac830ecd86b55f247ea89b19000996 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_print.scss @@ -0,0 +1,101 @@ +// +// Basic print styles +// -------------------------------------------------- +// Source: https://github.com/h5bp/html5-boilerplate/blob/master/css/main.css + +@media print { + + * { + text-shadow: none !important; + color: #000 !important; // Black prints faster: h5bp.com/s + background: transparent !important; + box-shadow: none !important; + } + + a, + a:visited { + text-decoration: underline; + } + + a[href]:after { + content: " (" attr(href) ")"; + } + + abbr[title]:after { + content: " (" attr(title) ")"; + } + + // Don't show links for images, or javascript/internal links + a[href^="javascript:"]:after, + a[href^="#"]:after { + content: ""; + } + + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; + } + + thead { + display: table-header-group; // h5bp.com/t + } + + tr, + img { + page-break-inside: avoid; + } + + img { + max-width: 100% !important; + } + + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + + h2, + h3 { + page-break-after: avoid; + } + + // Chrome (OSX) fix for https://github.com/twbs/bootstrap/issues/11245 + // Once fixed, we can just straight up remove this. + select { + background: #fff !important; + } + + // Bootstrap components + .navbar { + display: none; + } + .table { + td, + th { + background-color: #fff !important; + } + } + .btn, + .dropup > .btn { + > .caret { + border-top-color: #000 !important; + } + } + .label { + border: 1px solid #000; + } + + .table { + border-collapse: collapse !important; + } + .table-bordered { + th, + td { + border: 1px solid #ddd !important; + } + } + +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_progress-bars.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_progress-bars.scss new file mode 100644 index 0000000000000000000000000000000000000000..7302b729d5b426d529711c73f6510722c5442e64 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_progress-bars.scss @@ -0,0 +1,80 @@ +// +// Progress bars +// -------------------------------------------------- + + +// Bar animations +// ------------------------- + +// WebKit +@-webkit-keyframes progress-bar-stripes { + from { background-position: 40px 0; } + to { background-position: 0 0; } +} + +// Spec and IE10+ +@keyframes progress-bar-stripes { + from { background-position: 40px 0; } + to { background-position: 0 0; } +} + + + +// Bar itself +// ------------------------- + +// Outer container +.progress { + overflow: hidden; + height: $line-height-computed; + margin-bottom: $line-height-computed; + background-color: $progress-bg; + border-radius: $border-radius-base; + @include box-shadow(inset 0 1px 2px rgba(0,0,0,.1)); +} + +// Bar of progress +.progress-bar { + float: left; + width: 0%; + height: 100%; + font-size: $font-size-small; + line-height: $line-height-computed; + color: $progress-bar-color; + text-align: center; + background-color: $progress-bar-bg; + @include box-shadow(inset 0 -1px 0 rgba(0,0,0,.15)); + @include transition(width .6s ease); +} + +// Striped bars +.progress-striped .progress-bar { + @include gradient-striped(); + background-size: 40px 40px; +} + +// Call animation for the active one +.progress.active .progress-bar { + @include animation(progress-bar-stripes 2s linear infinite); +} + + + +// Variations +// ------------------------- + +.progress-bar-success { + @include progress-bar-variant($progress-bar-success-bg); +} + +.progress-bar-info { + @include progress-bar-variant($progress-bar-info-bg); +} + +.progress-bar-warning { + @include progress-bar-variant($progress-bar-warning-bg); +} + +.progress-bar-danger { + @include progress-bar-variant($progress-bar-danger-bg); +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_responsive-utilities.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_responsive-utilities.scss new file mode 100644 index 0000000000000000000000000000000000000000..cd9348c6e429a5e5ebf5dfa41d5943bf31ae905e --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_responsive-utilities.scss @@ -0,0 +1,74 @@ +// +// Responsive: Utility classes +// -------------------------------------------------- + + +// IE10 in Windows (Phone) 8 +// +// Support for responsive views via media queries is kind of borked in IE10, for +// Surface/desktop in split view and for Windows Phone 8. This particular fix +// must be accompanied by a snippet of JavaScript to sniff the user agent and +// apply some conditional CSS to *only* the Surface/desktop Windows 8. Look at +// our Getting Started page for more information on this bug. +// +// For more information, see the following: +// +// Issue: https://github.com/twbs/bootstrap/issues/10497 +// Docs: http://getbootstrap.com/getting-started/#browsers +// Source: http://timkadlec.com/2012/10/ie10-snap-mode-and-responsive-design/ + +@-ms-viewport { + width: device-width; +} + + +// Visibility utilities + +@include responsive-invisibility('.visible-xs, .visible-sm, .visible-md, .visible-lg'); + +@media (max-width: $screen-xs-max) { + @include responsive-visibility('.visible-xs'); +} + +@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { + @include responsive-visibility('.visible-sm'); +} + +@media (min-width: $screen-md-min) and (max-width: $screen-md-max) { + @include responsive-visibility('.visible-md'); +} + +@media (min-width: $screen-lg-min) { + @include responsive-visibility('.visible-lg'); +} + +@media (max-width: $screen-xs-max) { + @include responsive-invisibility('.hidden-xs'); +} + +@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { + @include responsive-invisibility('.hidden-sm'); +} + +@media (min-width: $screen-md-min) and (max-width: $screen-md-max) { + @include responsive-invisibility('.hidden-md'); +} + +@media (min-width: $screen-lg-min) { + @include responsive-invisibility('.hidden-lg'); +} + + +// Print utilities +// +// Media queries are placed on the inside to be mixin-friendly. + +@include responsive-invisibility('.visible-print'); + +@media print { + @include responsive-visibility('.visible-print'); +} + +@media print { + @include responsive-invisibility('.hidden-print'); +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_scaffolding.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_scaffolding.scss new file mode 100644 index 0000000000000000000000000000000000000000..406f0b4a9ff051441dc97545a8c300d4be76becc --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_scaffolding.scss @@ -0,0 +1,134 @@ +// +// Scaffolding +// -------------------------------------------------- + + +// Reset the box-sizing +// +// Heads up! This reset may cause conflicts with some third-party widgets. +// For recommendations on resolving such conflicts, see +// http://getbootstrap.com/getting-started/#third-box-sizing +* { + @include box-sizing(border-box); +} +*:before, +*:after { + @include box-sizing(border-box); +} + + +// Body reset + +html { + font-size: 62.5%; + -webkit-tap-highlight-color: rgba(0,0,0,0); +} + +body { + font-family: $font-family-base; + font-size: $font-size-base; + line-height: $line-height-base; + color: $text-color; + background-color: $body-bg; +} + +// Reset fonts for relevant elements +input, +button, +select, +textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; + color: $text-color; +} + + +// Links + +a { + color: $link-color; + text-decoration: none; + + &:hover, + &:focus { + color: $link-hover-color; + } + + &:focus { + @include tab-focus(); + } +} + + +// Figures +// +// We reset this here because previously Normalize had no `figure` margins. This +// ensures we don't break anyone's use of the element. + +figure { + margin: 0; +} + + +// Images + +img { + vertical-align: middle; +} + +// Responsive images (ensure images don't scale beyond their parents) +.img-responsive { + @include img-responsive(); +} + +// Rounded corners +.img-rounded { + border-radius: $border-radius-large; +} + +// Image thumbnails +// +// Heads up! This is mixin-ed into thumbnails.less for `.thumbnail`. +.img-thumbnail { + padding: $thumbnail-padding; + line-height: $line-height-base; + background-color: $thumbnail-bg; + border: 1px solid $thumbnail-border; + border-radius: $thumbnail-border-radius; + @include transition(all .2s ease-in-out); + + // Keep them at most 100% wide + @include img-responsive(inline-block); +} + +// Perfect circle +.img-circle { + border-radius: 50%; // set radius in percents +} + + +// Horizontal rules + +hr { + margin-top: $line-height-computed; + margin-bottom: $line-height-computed; + border: 0; + border-top: 1px solid $hr-border; +} + + +// Only display content to screen readers +// +// See: http://a11yproject.com/posts/how-to-hide-content/ + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip: rect(0,0,0,0); + border: 0; +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_tables.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_tables.scss new file mode 100644 index 0000000000000000000000000000000000000000..1ddfb7ab36fc7db2a511a0dddda470ec5ff52956 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_tables.scss @@ -0,0 +1,233 @@ +// +// Tables +// -------------------------------------------------- + + +table { + max-width: 100%; + background-color: $table-bg; +} +th { + text-align: left; +} + + +// Baseline styles + +.table { + width: 100%; + margin-bottom: $line-height-computed; + // Cells + > thead, + > tbody, + > tfoot { + > tr { + > th, + > td { + padding: $table-cell-padding; + line-height: $line-height-base; + vertical-align: top; + border-top: 1px solid $table-border-color; + } + } + } + // Bottom align for column headings + > thead > tr > th { + vertical-align: bottom; + border-bottom: 2px solid $table-border-color; + } + // Remove top border from thead by default + > caption + thead, + > colgroup + thead, + > thead:first-child { + > tr:first-child { + > th, + > td { + border-top: 0; + } + } + } + // Account for multiple tbody instances + > tbody + tbody { + border-top: 2px solid $table-border-color; + } + + // Nesting + .table { + background-color: $body-bg; + } +} + + +// Condensed table w/ half padding + +.table-condensed { + > thead, + > tbody, + > tfoot { + > tr { + > th, + > td { + padding: $table-condensed-cell-padding; + } + } + } +} + + +// Bordered version +// +// Add borders all around the table and between all the columns. + +.table-bordered { + border: 1px solid $table-border-color; + > thead, + > tbody, + > tfoot { + > tr { + > th, + > td { + border: 1px solid $table-border-color; + } + } + } + > thead > tr { + > th, + > td { + border-bottom-width: 2px; + } + } +} + + +// Zebra-striping +// +// Default zebra-stripe styles (alternating gray and transparent backgrounds) + +.table-striped { + > tbody > tr:nth-child(odd) { + > td, + > th { + background-color: $table-bg-accent; + } + } +} + + +// Hover effect +// +// Placed here since it has to come after the potential zebra striping + +.table-hover { + > tbody > tr:hover { + > td, + > th { + background-color: $table-bg-hover; + } + } +} + + +// Table cell sizing +// +// Reset default table behavior + +table col[class*="col-"] { + position: static; // Prevent border hiding in Firefox and IE9/10 (see https://github.com/twbs/bootstrap/issues/11623) + float: none; + display: table-column; +} +table { + td, + th { + &[class*="col-"] { + position: static; // Prevent border hiding in Firefox and IE9/10 (see https://github.com/twbs/bootstrap/issues/11623) + float: none; + display: table-cell; + } + } +} + + +// Table backgrounds +// +// Exact selectors below required to override `.table-striped` and prevent +// inheritance to nested tables. + +// Generate the contextual variants +@include table-row-variant('active', $table-bg-active); +@include table-row-variant('success', $state-success-bg); +@include table-row-variant('info', $state-info-bg); +@include table-row-variant('warning', $state-warning-bg); +@include table-row-variant('danger', $state-danger-bg); + + +// Responsive tables +// +// Wrap your tables in `.table-responsive` and we'll make them mobile friendly +// by enabling horizontal scrolling. Only applies <768px. Everything above that +// will display normally. + +@media (max-width: $screen-xs-max) { + .table-responsive { + width: 100%; + margin-bottom: ($line-height-computed * 0.75); + overflow-y: hidden; + overflow-x: scroll; + -ms-overflow-style: -ms-autohiding-scrollbar; + border: 1px solid $table-border-color; + -webkit-overflow-scrolling: touch; + + // Tighten up spacing + > .table { + margin-bottom: 0; + + // Ensure the content doesn't wrap + > thead, + > tbody, + > tfoot { + > tr { + > th, + > td { + white-space: nowrap; + } + } + } + } + + // Special overrides for the bordered tables + > .table-bordered { + border: 0; + + // Nuke the appropriate borders so that the parent can handle them + > thead, + > tbody, + > tfoot { + > tr { + > th:first-child, + > td:first-child { + border-left: 0; + } + > th:last-child, + > td:last-child { + border-right: 0; + } + } + } + + // Only nuke the last row's bottom-border in `tbody` and `tfoot` since + // chances are there will be only one `tr` in a `thead` and that would + // remove the border altogether. + > tbody, + > tfoot { + > tr:last-child { + > th, + > td { + border-bottom: 0; + } + } + } + + } + } +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_theme.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_theme.scss new file mode 100644 index 0000000000000000000000000000000000000000..d8f7bc2fb3cc571cea5c1fa4ac85253b3a0da5e4 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_theme.scss @@ -0,0 +1,247 @@ + +// +// Load core variables and mixins +// -------------------------------------------------- + +@import "variables"; +@import "mixins"; + + + +// +// Buttons +// -------------------------------------------------- + +// Common styles +.btn-default, +.btn-primary, +.btn-success, +.btn-info, +.btn-warning, +.btn-danger { + text-shadow: 0 -1px 0 rgba(0,0,0,.2); + $shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 1px rgba(0,0,0,.075); + @include box-shadow($shadow); + + // Reset the shadow + &:active, + &.active { + @include box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); + } +} + +// Mixin for generating new styles +@mixin btn-styles($btn-color: #555) { + @include gradient-vertical($start-color: $btn-color, $end-color: darken($btn-color, 12%)); + @include reset-filter(); // Disable gradients for IE9 because filter bleeds through rounded corners + background-repeat: repeat-x; + border-color: darken($btn-color, 14%); + + &:hover, + &:focus { + background-color: darken($btn-color, 12%); + background-position: 0 -15px; + } + + &:active, + &.active { + background-color: darken($btn-color, 12%); + border-color: darken($btn-color, 14%); + } +} + +// Common styles +.btn { + // Remove the gradient for the pressed/active state + &:active, + &.active { + background-image: none; + } +} + +// Apply the mixin to the buttons +.btn-default { @include btn-styles($btn-default-bg); text-shadow: 0 1px 0 #fff; border-color: #ccc; } +.btn-primary { @include btn-styles($btn-primary-bg); } +.btn-success { @include btn-styles($btn-success-bg); } +.btn-info { @include btn-styles($btn-info-bg); } +.btn-warning { @include btn-styles($btn-warning-bg); } +.btn-danger { @include btn-styles($btn-danger-bg); } + + + +// +// Images +// -------------------------------------------------- + +.thumbnail, +.img-thumbnail { + @include box-shadow(0 1px 2px rgba(0,0,0,.075)); +} + + + +// +// Dropdowns +// -------------------------------------------------- + +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus { + @include gradient-vertical($start-color: $dropdown-link-hover-bg, $end-color: darken($dropdown-link-hover-bg, 5%)); + background-color: darken($dropdown-link-hover-bg, 5%); +} +.dropdown-menu > .active > a, +.dropdown-menu > .active > a:hover, +.dropdown-menu > .active > a:focus { + @include gradient-vertical($start-color: $dropdown-link-active-bg, $end-color: darken($dropdown-link-active-bg, 5%)); + background-color: darken($dropdown-link-active-bg, 5%); +} + + + +// +// Navbar +// -------------------------------------------------- + +// Default navbar +.navbar-default { + @include gradient-vertical($start-color: lighten($navbar-default-bg, 10%), $end-color: $navbar-default-bg); + @include reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered + border-radius: $navbar-border-radius; + $shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 5px rgba(0,0,0,.075); + @include box-shadow($shadow); + + .navbar-nav > .active > a { + @include gradient-vertical($start-color: darken($navbar-default-bg, 5%), $end-color: darken($navbar-default-bg, 2%)); + @include box-shadow(inset 0 3px 9px rgba(0,0,0,.075)); + } +} +.navbar-brand, +.navbar-nav > li > a { + text-shadow: 0 1px 0 rgba(255,255,255,.25); +} + +// Inverted navbar +.navbar-inverse { + @include gradient-vertical($start-color: lighten($navbar-inverse-bg, 10%), $end-color: $navbar-inverse-bg); + @include reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered + + .navbar-nav > .active > a { + @include gradient-vertical($start-color: $navbar-inverse-bg, $end-color: lighten($navbar-inverse-bg, 2.5%)); + @include box-shadow(inset 0 3px 9px rgba(0,0,0,.25)); + } + + .navbar-brand, + .navbar-nav > li > a { + text-shadow: 0 -1px 0 rgba(0,0,0,.25); + } +} + +// Undo rounded corners in static and fixed navbars +.navbar-static-top, +.navbar-fixed-top, +.navbar-fixed-bottom { + border-radius: 0; +} + + + +// +// Alerts +// -------------------------------------------------- + +// Common styles +.alert { + text-shadow: 0 1px 0 rgba(255,255,255,.2); + $shadow: inset 0 1px 0 rgba(255,255,255,.25), 0 1px 2px rgba(0,0,0,.05); + @include box-shadow($shadow); +} + +// Mixin for generating new styles +@mixin alert-styles($color) { + @include gradient-vertical($start-color: $color, $end-color: darken($color, 7.5%)); + border-color: darken($color, 15%); +} + +// Apply the mixin to the alerts +.alert-success { @include alert-styles($alert-success-bg); } +.alert-info { @include alert-styles($alert-info-bg); } +.alert-warning { @include alert-styles($alert-warning-bg); } +.alert-danger { @include alert-styles($alert-danger-bg); } + + + +// +// Progress bars +// -------------------------------------------------- + +// Give the progress background some depth +.progress { + @include gradient-vertical($start-color: darken($progress-bg, 4%), $end-color: $progress-bg) +} + +// Mixin for generating new styles +@mixin progress-bar-styles($color) { + @include gradient-vertical($start-color: $color, $end-color: darken($color, 10%)); +} + +// Apply the mixin to the progress bars +.progress-bar { @include progress-bar-styles($progress-bar-bg); } +.progress-bar-success { @include progress-bar-styles($progress-bar-success-bg); } +.progress-bar-info { @include progress-bar-styles($progress-bar-info-bg); } +.progress-bar-warning { @include progress-bar-styles($progress-bar-warning-bg); } +.progress-bar-danger { @include progress-bar-styles($progress-bar-danger-bg); } + + + +// +// List groups +// -------------------------------------------------- + +.list-group { + border-radius: $border-radius-base; + @include box-shadow(0 1px 2px rgba(0,0,0,.075)); +} +.list-group-item.active, +.list-group-item.active:hover, +.list-group-item.active:focus { + text-shadow: 0 -1px 0 darken($list-group-active-bg, 10%); + @include gradient-vertical($start-color: $list-group-active-bg, $end-color: darken($list-group-active-bg, 7.5%)); + border-color: darken($list-group-active-border, 7.5%); +} + + + +// +// Panels +// -------------------------------------------------- + +// Common styles +.panel { + @include box-shadow(0 1px 2px rgba(0,0,0,.05)); +} + +// Mixin for generating new styles +@mixin panel-heading-styles($color) { + @include gradient-vertical($start-color: $color, $end-color: darken($color, 5%)); +} + +// Apply the mixin to the panel headings only +.panel-default > .panel-heading { @include panel-heading-styles($panel-default-heading-bg); } +.panel-primary > .panel-heading { @include panel-heading-styles($panel-primary-heading-bg); } +.panel-success > .panel-heading { @include panel-heading-styles($panel-success-heading-bg); } +.panel-info > .panel-heading { @include panel-heading-styles($panel-info-heading-bg); } +.panel-warning > .panel-heading { @include panel-heading-styles($panel-warning-heading-bg); } +.panel-danger > .panel-heading { @include panel-heading-styles($panel-danger-heading-bg); } + + + +// +// Wells +// -------------------------------------------------- + +.well { + @include gradient-vertical($start-color: darken($well-bg, 5%), $end-color: $well-bg); + border-color: darken($well-bg, 10%); + $shadow: inset 0 1px 3px rgba(0,0,0,.05), 0 1px 0 rgba(255,255,255,.1); + @include box-shadow($shadow); +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_thumbnails.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_thumbnails.scss new file mode 100644 index 0000000000000000000000000000000000000000..3d5ed86d05fd5bdeba1d61f3b1faa2c6055d0868 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_thumbnails.scss @@ -0,0 +1,38 @@ +// +// Thumbnails +// -------------------------------------------------- + + +// Mixin and adjust the regular image class +.thumbnail { + display: block; + padding: $thumbnail-padding; + margin-bottom: $line-height-computed; + line-height: $line-height-base; + background-color: $thumbnail-bg; + border: 1px solid $thumbnail-border; + border-radius: $thumbnail-border-radius; + @include transition(all .2s ease-in-out); + + > img, + a > img { + @include img-responsive(); + margin-left: auto; + margin-right: auto; + } + + // [converter] extracted a&:hover, a&:focus, a&.active to a.thumbnail:hover, a.thumbnail:focus, a.thumbnail.active + + // Image captions + .caption { + padding: $thumbnail-caption-padding; + color: $thumbnail-caption-color; + } +} + +// Add a hover state for linked versions only +a.thumbnail:hover, +a.thumbnail:focus, +a.thumbnail.active { + border-color: $link-color; +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_tooltip.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_tooltip.scss new file mode 100644 index 0000000000000000000000000000000000000000..dec674cb40871ec975cd629951a8783d4141ddcd --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_tooltip.scss @@ -0,0 +1,95 @@ +// +// Tooltips +// -------------------------------------------------- + + +// Base class +.tooltip { + position: absolute; + z-index: $zindex-tooltip; + display: block; + visibility: visible; + font-size: $font-size-small; + line-height: 1.4; + @include opacity(0); + + &.in { @include opacity($tooltip-opacity); } + &.top { margin-top: -3px; padding: $tooltip-arrow-width 0; } + &.right { margin-left: 3px; padding: 0 $tooltip-arrow-width; } + &.bottom { margin-top: 3px; padding: $tooltip-arrow-width 0; } + &.left { margin-left: -3px; padding: 0 $tooltip-arrow-width; } +} + +// Wrapper for the tooltip content +.tooltip-inner { + max-width: $tooltip-max-width; + padding: 3px 8px; + color: $tooltip-color; + text-align: center; + text-decoration: none; + background-color: $tooltip-bg; + border-radius: $border-radius-base; +} + +// Arrows +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} +.tooltip { + &.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -$tooltip-arrow-width; + border-width: $tooltip-arrow-width $tooltip-arrow-width 0; + border-top-color: $tooltip-arrow-color; + } + &.top-left .tooltip-arrow { + bottom: 0; + left: $tooltip-arrow-width; + border-width: $tooltip-arrow-width $tooltip-arrow-width 0; + border-top-color: $tooltip-arrow-color; + } + &.top-right .tooltip-arrow { + bottom: 0; + right: $tooltip-arrow-width; + border-width: $tooltip-arrow-width $tooltip-arrow-width 0; + border-top-color: $tooltip-arrow-color; + } + &.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -$tooltip-arrow-width; + border-width: $tooltip-arrow-width $tooltip-arrow-width $tooltip-arrow-width 0; + border-right-color: $tooltip-arrow-color; + } + &.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -$tooltip-arrow-width; + border-width: $tooltip-arrow-width 0 $tooltip-arrow-width $tooltip-arrow-width; + border-left-color: $tooltip-arrow-color; + } + &.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -$tooltip-arrow-width; + border-width: 0 $tooltip-arrow-width $tooltip-arrow-width; + border-bottom-color: $tooltip-arrow-color; + } + &.bottom-left .tooltip-arrow { + top: 0; + left: $tooltip-arrow-width; + border-width: 0 $tooltip-arrow-width $tooltip-arrow-width; + border-bottom-color: $tooltip-arrow-color; + } + &.bottom-right .tooltip-arrow { + top: 0; + right: $tooltip-arrow-width; + border-width: 0 $tooltip-arrow-width $tooltip-arrow-width; + border-bottom-color: $tooltip-arrow-color; + } +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_type.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_type.scss new file mode 100644 index 0000000000000000000000000000000000000000..45b0ccd472569e086029dcbad8035d71359adfbc --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_type.scss @@ -0,0 +1,284 @@ +// +// Typography +// -------------------------------------------------- + + +// Headings +// ------------------------- + +h1, h2, h3, h4, h5, h6, +.h1, .h2, .h3, .h4, .h5, .h6 { + font-family: $headings-font-family; + font-weight: $headings-font-weight; + line-height: $headings-line-height; + color: $headings-color; + + small, + .small { + font-weight: normal; + line-height: 1; + color: $headings-small-color; + } +} + +h1, .h1, +h2, .h2, +h3, .h3 { + margin-top: $line-height-computed; + margin-bottom: ($line-height-computed / 2); + + small, + .small { + font-size: 65%; + } +} +h4, .h4, +h5, .h5, +h6, .h6 { + margin-top: ($line-height-computed / 2); + margin-bottom: ($line-height-computed / 2); + + small, + .small { + font-size: 75%; + } +} + +h1, .h1 { font-size: $font-size-h1; } +h2, .h2 { font-size: $font-size-h2; } +h3, .h3 { font-size: $font-size-h3; } +h4, .h4 { font-size: $font-size-h4; } +h5, .h5 { font-size: $font-size-h5; } +h6, .h6 { font-size: $font-size-h6; } + + +// Body text +// ------------------------- + +p { + margin: 0 0 ($line-height-computed / 2); +} + +.lead { + margin-bottom: $line-height-computed; + font-size: floor(($font-size-base * 1.15)); + font-weight: 200; + line-height: 1.4; + + @media (min-width: $screen-sm-min) { + font-size: ($font-size-base * 1.5); + } +} + + +// Emphasis & misc +// ------------------------- + +// Ex: 14px base font * 85% = about 12px +small, +.small { font-size: 85%; } + +// Undo browser default styling +cite { font-style: normal; } + +// Alignment +.text-left { text-align: left; } +.text-right { text-align: right; } +.text-center { text-align: center; } +.text-justify { text-align: justify; } + +// Contextual colors +.text-muted { + color: $text-muted; +} + +@include text-emphasis-variant('.text-primary', $brand-primary); + +@include text-emphasis-variant('.text-success', $state-success-text); + +@include text-emphasis-variant('.text-info', $state-info-text); + +@include text-emphasis-variant('.text-warning', $state-warning-text); + +@include text-emphasis-variant('.text-danger', $state-danger-text); + +// Contextual backgrounds +// For now we'll leave these alongside the text classes until v4 when we can +// safely shift things around (per SemVer rules). +.bg-primary { + // Given the contrast here, this is the only class to have its color inverted + // automatically. + color: #fff; +} +@include bg-variant('.bg-primary', $brand-primary); + +@include bg-variant('.bg-success', $state-success-bg); + +@include bg-variant('.bg-info', $state-info-bg); + +@include bg-variant('.bg-warning', $state-warning-bg); + +@include bg-variant('.bg-danger', $state-danger-bg); + + +// Page header +// ------------------------- + +.page-header { + padding-bottom: (($line-height-computed / 2) - 1); + margin: ($line-height-computed * 2) 0 $line-height-computed; + border-bottom: 1px solid $page-header-border-color; +} + + +// Lists +// -------------------------------------------------- + +// Unordered and Ordered lists +ul, +ol { + margin-top: 0; + margin-bottom: ($line-height-computed / 2); + ul, + ol { + margin-bottom: 0; + } +} + +// List options + +// Unstyled keeps list items block level, just removes default browser padding and list-style +.list-unstyled { + padding-left: 0; + list-style: none; +} + +// Inline turns list items into inline-block +.list-inline { + @extend .list-unstyled; + margin-left: -5px; + + > li { + display: inline-block; + padding-left: 5px; + padding-right: 5px; + } +} + +// Description Lists +dl { + margin-top: 0; // Remove browser default + margin-bottom: 0; +} +dt, +dd { + line-height: $line-height-base; +} +dt { + font-weight: bold; +} +dd { + margin-left: 0; // Undo browser default +} + +// Horizontal description lists +// +// Defaults to being stacked without any of the below styles applied, until the +// grid breakpoint is reached (default of ~768px). + +@media (min-width: $grid-float-breakpoint) { + .dl-horizontal { + dt { + float: left; + width: ($component-offset-horizontal - 20); + clear: left; + text-align: right; + @include text-overflow(); + } + dd { + margin-left: $component-offset-horizontal; + @include clearfix(); // Clear the floated `dt` if an empty `dd` is present + } + } +} + +// MISC +// ---- + +// Abbreviations and acronyms +abbr[title], +// Add data-* attribute to help out our tooltip plugin, per https://github.com/twbs/bootstrap/issues/5257 +abbr[data-original-title] { + cursor: help; + border-bottom: 1px dotted $abbr-border-color; +} +.initialism { + font-size: 90%; + text-transform: uppercase; +} + +// Blockquotes +blockquote { + padding: ($line-height-computed / 2) $line-height-computed; + margin: 0 0 $line-height-computed; + font-size: $blockquote-font-size; + border-left: 5px solid $blockquote-border-color; + + p, + ul, + ol { + &:last-child { + margin-bottom: 0; + } + } + + // Note: Deprecated small and .small as of v3.1.0 + // Context: https://github.com/twbs/bootstrap/issues/11660 + footer, + small, + .small { + display: block; + font-size: 80%; // back to default font-size + line-height: $line-height-base; + color: $blockquote-small-color; + + &:before { + content: '\2014 \00A0'; // em dash, nbsp + } + } +} + +// Opposite alignment of blockquote +// +// Heads up: `blockquote.pull-right` has been deprecated as of v3.1.0. +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + border-right: 5px solid $blockquote-border-color; + border-left: 0; + text-align: right; + + // Account for citation + footer, + small, + .small { + &:before { content: ''; } + &:after { + content: '\00A0 \2014'; // nbsp, em dash + } + } +} + +// Quotes +blockquote:before, +blockquote:after { + content: ""; +} + +// Addresses +address { + margin-bottom: $line-height-computed; + font-style: normal; + line-height: $line-height-base; +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_utilities.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_utilities.scss new file mode 100644 index 0000000000000000000000000000000000000000..85cb62ea7d6632e67916a63ab744b103070b8a12 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_utilities.scss @@ -0,0 +1,56 @@ +// +// Utility classes +// -------------------------------------------------- + + +// Floats +// ------------------------- + +.clearfix { + @include clearfix(); +} +.center-block { + @include center-block(); +} +.pull-right { + float: right !important; +} +.pull-left { + float: left !important; +} + + +// Toggling content +// ------------------------- + +// Note: Deprecated .hide in favor of .hidden or .sr-only (as appropriate) in v3.0.1 +.hide { + display: none !important; +} +.show { + display: block !important; +} +.invisible { + visibility: hidden; +} +.text-hide { + @include text-hide(); +} + + +// Hide from screenreaders and browsers +// +// Credit: HTML5 Boilerplate + +.hidden { + display: none !important; + visibility: hidden !important; +} + + +// For Affix plugin +// ------------------------- + +.affix { + position: fixed; +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_variables.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_variables.scss new file mode 100644 index 0000000000000000000000000000000000000000..c219c67a11092b8067d97c4d6ee685bf6b29b5d0 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_variables.scss @@ -0,0 +1,905 @@ +// a flag to toggle asset pipeline / compass integration +// defaults to true if twbs-font-path function is present (no function => twbs-font-path('') parsed as string == right side) +// in Sass 3.3 this can be improved with: function-exists(twbs-font-path) +$bootstrap-sass-asset-helper: (twbs-font-path("") != unquote('twbs-font-path("")')) !default; +// +// Variables +// -------------------------------------------------- + + +//== Colors +// +//## Gray and brand colors for use across Bootstrap. +$synnefo-green: #00a551; +$gray-darker: lighten(#000, 13.5%) !default; // #222 +$gray-dark: $snf_gray-dark; +$gray: lighten(#000, 33.5%) !default; // #555 +$gray-light: $snf_gray-light; +$gray-lighter: lighten(#000, 85%) !default; // #eee + +$brand-primary: #fff; +$brand-success: #5cb85c !default; +$brand-info: #5bc0de !default; +$brand-warning: #f0ad4e !default; +$brand-danger: #d9534f !default; + + +$primary-color: $total-black; +$secondary-color: $snf_gray-dark; +//== Scaffolding +// +// ## Settings for some of the most global styles. + +//** Background color for `<body>`. +$body-bg: $secondary-color; +//** Global text color on `<body>`. +$text-color: #fff; +$reverse-text-color: $secondary-color; + +//** Global textual link color. +$link-color: $ciel; +//** Link hover color set via `darken()` function. +$link-hover-color: lighten($link-color, 13%); + +// ----- EXTRA COLOR-RELATED SETTINGS + +$hover-nav-color: #333; + + +$secondary-link-color: white; + +// Î’utton colors +$default-btn-color: $blue-intense; +$bad-karma-color: $red-intense; +$neutral-karma-color: $orange-intense; +$good-karma-color: $green-intense; + +$btn-outline-color: #fff; +$btn-line-bg: $total-black; +$btn-line-border: #fff; + +// Tables +$table-selected-row-bg: $almost-white; +$table-selected-row-color: $reverse-text-color; +$table-zebra-row-bg: lighten($gray-dark,5%); +$table-row-hover: lighten($table-zebra-row-bg, 7%); +$table-datatable-border-color: white; +$table-processing-bg: $gray-light; +$table-processing-color: $text-color; +$table-paginate-current-bg: #fff; +$table-paginate-current-color: $reverse-text-color; +$table-selected-border: gray; + + +// Filters +$filter-bg: $almost-white; +$filter-font-color: $reverse-text-color; +$filter-active-bg: $filter-bg; +$filter-hover-bg: $gray-lighter; +$filter-hover-color: inherit; +$filter-border-color: transparent; +$filter-border-color-alt: white; + +// Tabs +$tab-content-bg: $gray-light; +$tab-content-color: #fff; +$tab-border: inherit; + +$object-details-bg: lighten($body-bg,3%); +$object-details-border: $gray-light-extra-1; + +$object-details-row-hover-bg: lighten($object-details-bg,2%); + +// Popover dismiss icon +$popover-dismiss-bg: $snf_gray-light; +$popover-dismiss-color: lighten($popover-dismiss-bg,20%); +$popover-dismiss-bg-hover: lighten($popover-dismiss-bg,10%); +$popover-dismiss-color-hover: $almost-white; + +$popover-color: $reverse-text-color; +$modal-text-color: $reverse-text-color; + +// Notification area +$notify-bg: #fff; +$notify-color: $reverse-text-color; +$notify-close-color: $gray-dark; + +$parts-separator-color: $snf_gray-light; +// ----- END OF EXTRA COLOR-RELATED SETIINGS + + +//== Typography +// +//## Font, line-height, and color for body text, headings, and more. + +$font-family-sans-serif: 'Open Sans', sans-serif; +$font-family-serif: Georgia, "Times New Roman", Times, serif !default; +//** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`. +$font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace !default; +$font-family-base: $font-family-sans-serif !default; + +$font-size-base: 14px !default; +$font-size-large: ceil(($font-size-base * 1.25)) !default; // ~18px +$font-size-small: ceil(($font-size-base * 0.85)) !default; // ~12px + +$font-size-h1: floor(($font-size-base * 2.6)) !default; // ~36px +$font-size-h2: floor(($font-size-base * 2.15)) !default; // ~30px +$font-size-h3: ceil(($font-size-base * 1.7)) !default; // ~24px +$font-size-h4: ceil(($font-size-base * 1.25)) !default; // ~18px +$font-size-h5: $font-size-base !default; +$font-size-h6: ceil(($font-size-base * 0.85)) !default; // ~12px + +//** Unit-less `line-height` for use in components like buttons. +$line-height-base: 1.428571429 !default; // 20/14 +//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc. +$line-height-computed: floor(($font-size-base * $line-height-base)) !default; // ~20px + +//** By default, this inherits from the `<body>`. +$headings-font-family: inherit !default; +$headings-font-weight: 500 !default; +$headings-line-height: 1.1 !default; +$headings-color: inherit !default; + + +//-- Iconography +// +//## Specify custom locations of the include Glyphicons icon font. Useful for those including Bootstrap via Bower. + +$icon-font-path: "bootstrap/" !default; +$icon-font-name: "glyphicons-halflings-regular" !default; +$icon-font-svg-id: "glyphicons_halflingsregular" !default; + +//== Components +// +//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start). + +$padding-base-vertical: 6px !default; +$padding-base-horizontal: 12px !default; + +$padding-large-vertical: 10px !default; +$padding-large-horizontal: 16px !default; + +$padding-small-vertical: 5px !default; +$padding-small-horizontal: 10px !default; + +$padding-xs-vertical: 1px !default; +$padding-xs-horizontal: 5px !default; + +$line-height-large: 1.33 !default; +$line-height-small: 1.5 !default; + +$border-radius-base: 0; +$border-radius-large: 6px !default; +$border-radius-small: 3px !default; + +//** Global color for active items (e.g., navs or dropdowns). +$component-active-color: #fff !default; +//** Global background color for active items (e.g., navs or dropdowns). +$component-active-bg: $theme-red; + +//** Width of the `border` for generating carets that indicator dropdowns. +$caret-width-base: 4px !default; +//** Carets increase slightly in size for larger components. +$caret-width-large: 5px !default; + + +//== Tables +// +//## Customizes the `.table` component with basic values, each used across all table variations. + +//** Padding for `<th>`s and `<td>`s. +$table-cell-padding: 10px; +//** Padding for cells in `.table-condensed`. +$table-condensed-cell-padding: 5px !default; + +//** Default background color used for all tables. +$table-bg: transparent !default; +//** Background color used for `.table-striped`. +$table-bg-accent: #f9f9f9 !default; +//** Background color used for `.table-hover`. +$table-bg-hover: #f5f5f5 !default; +$table-bg-active: $table-bg-hover !default; + +//** Border color for table and cell borders. +$table-border-color: #ddd; + + +//== Buttons +// +//## For each of Bootstrap's buttons, define text, background and border color. + +$btn-font-weight: normal !default; + +$btn-default-color: #333 !default; +$btn-default-bg: #fff !default; +$btn-default-border: #ccc !default; + +$btn-primary-color: #fff !default; +$btn-primary-bg: $brand-primary !default; +$btn-primary-border: darken($btn-primary-bg, 5%) !default; + +$btn-success-color: #fff !default; +$btn-success-bg: $brand-success !default; +$btn-success-border: darken($btn-success-bg, 5%) !default; + +$btn-info-color: #fff !default; +$btn-info-bg: $brand-info !default; +$btn-info-border: darken($btn-info-bg, 5%) !default; + +$btn-warning-color: #fff !default; +$btn-warning-bg: $brand-warning !default; +$btn-warning-border: darken($btn-warning-bg, 5%) !default; + +$btn-danger-color: #fff !default; +$btn-danger-bg: $brand-danger !default; +$btn-danger-border: darken($btn-danger-bg, 5%) !default; + +$btn-link-disabled-color: lighten($snf_gray-light,20%); + + + +//== Forms +// +//## + +//** `<input>` background color +$input-bg: #fff !default; +//** `<input disabled>` background color +$input-bg-disabled: $gray-lighter !default; + +//** Text color for `<input>`s +$input-color: $gray !default; +//** `<input>` border color +$input-border: #ccc !default; +//** `<input>` border radius +$input-border-radius: $border-radius-base !default; +//** Border color for inputs on focus +$input-border-focus: #66afe9 !default; + +//** Placeholder text color +$input-color-placeholder: $gray-light !default; + +//** Default `.form-control` height +$input-height-base: ($line-height-computed + ($padding-base-vertical * 2) + 2) !default; +//** Large `.form-control` height +$input-height-large: (ceil($font-size-large * $line-height-large) + ($padding-large-vertical * 2) + 2) !default; +//** Small `.form-control` height +$input-height-small: (floor($font-size-small * $line-height-small) + ($padding-small-vertical * 2) + 2) !default; + +$legend-color: $gray-dark !default; +$legend-border-color: #e5e5e5 !default; + +//** Background color for textual input addons +$input-group-addon-bg: $gray-lighter !default; +//** Border color for textual input addons +$input-group-addon-border-color: $input-border !default; + + +//== Dropdowns +// +//## Dropdown menu container and contents. + +//** Background for the dropdown menu. +$dropdown-bg: #fff !default; +//** Dropdown menu `border-color`. +$dropdown-border: rgba(0,0,0,.15) !default; +//** Dropdown menu `border-color` **for IE8**. +$dropdown-fallback-border: #ccc !default; +//** Divider color for between dropdown items. +$dropdown-divider-bg: #e5e5e5 !default; + +//** Dropdown link text color. +$dropdown-link-color: $gray-dark !default; +//** Hover color for dropdown links. +$dropdown-link-hover-color: $gray-dark; +//** Hover background for dropdown links. +$dropdown-link-hover-bg: $gray-lighter; + +//** Active dropdown menu item text color. +$dropdown-link-active-color: $component-active-color !default; +//** Active dropdown menu item background color. +$dropdown-link-active-bg: $component-active-bg !default; + +//** Disabled dropdown menu item background color. +$dropdown-link-disabled-color: $gray-light !default; + +//** Text color for headers within dropdown menus. +$dropdown-header-color: $gray-light !default; + +// Note: Deprecated $dropdown-caret-color as of v3.1.0 +$dropdown-caret-color: #000 !default; + + +//-- Z-index master list +// +// Warning: Avoid customizing these values. They're used for a bird's eye view +// of components dependent on the z-axis and are designed to all work together. +// +// Note: These variables are not generated into the Customizer. + +$zindex-navbar: 1000 !default; +$zindex-dropdown: 1000 !default; +$zindex-popover: 1010 !default; +$zindex-tooltip: 1030 !default; +$zindex-navbar-fixed: 1030 !default; +$zindex-modal-background: 1040 !default; +$zindex-modal: 1050 !default; + + +//== Media queries breakpoints +// +//## Define the breakpoints at which your layout will change, adapting to different screen sizes. + +// Extra small screen / phone +// Note: Deprecated $screen-xs and $screen-phone as of v3.0.1 +$screen-xs: 480px !default; +$screen-xs-min: $screen-xs !default; +$screen-phone: $screen-xs-min !default; + +// Small screen / tablet +// Note: Deprecated $screen-sm and $screen-tablet as of v3.0.1 +$screen-sm: 768px !default; +$screen-sm-min: $screen-sm !default; +$screen-tablet: $screen-sm-min !default; + +// Medium screen / desktop +// Note: Deprecated $screen-md and $screen-desktop as of v3.0.1 +$screen-md: 992px !default; +$screen-md-min: $screen-md !default; +$screen-desktop: $screen-md-min !default; + +// Large screen / wide desktop +// Note: Deprecated $screen-lg and $screen-lg-desktop as of v3.0.1 +$screen-lg: 1200px !default; +$screen-lg-min: $screen-lg !default; +$screen-lg-desktop: $screen-lg-min !default; + +// So media queries don't overlap when required, provide a maximum +$screen-xs-max: ($screen-sm-min - 1) !default; +$screen-sm-max: ($screen-md-min - 1) !default; +$screen-md-max: ($screen-lg-min - 1) !default; + + +//== Grid system +// +//## Define your custom responsive grid. + +//** Number of columns in the grid. +$grid-columns: 12 !default; +//** Padding between columns. Gets divided in half for the left and right. +$grid-gutter-width: 30px !default; +// Navbar collapse +//** Point at which the navbar becomes uncollapsed. +$grid-float-breakpoint: $screen-sm-min !default; +//** Point at which the navbar begins collapsing. +$grid-float-breakpoint-max: ($grid-float-breakpoint - 1) !default; + + +//== Container sizes +// +//## Define the maximum width of `.container` for different screen sizes. + +// Small screen / tablet +$container-tablet: ((780px + $grid-gutter-width)) !default; +//** For `$screen-sm-min` and up. +$container-sm: $container-tablet !default; + +// Medium screen / desktop +$container-desktop: ((980px + $grid-gutter-width)) !default; +//** For `$screen-md-min` and up. +$container-md: $container-desktop !default; + +// Large screen / wide desktop +$container-large-desktop: ((1140px + $grid-gutter-width)) !default; +//** For `$screen-lg-min` and up. +$container-lg: $container-large-desktop !default; + + +//== Navbar +// +//## + +// Basics of a navbar +$navbar-height: 50px; +$navbar-margin-bottom: $line-height-computed !default; +$navbar-border-radius: $border-radius-base !default; +$navbar-padding-horizontal: 0; +$navbar-padding-vertical: (($navbar-height - $line-height-computed) / 2) !default; +$navbar-collapse-max-height: 340px !default; + +$navbar-default-color: #777 !default; +$navbar-default-bg: $primary-color; +$navbar-default-border: transparent; + +// Navbar links +$navbar-default-link-color: $text-color; +$navbar-default-link-hover-color: $text-color; +$navbar-default-link-hover-bg: $hover-nav-color; +$navbar-default-link-active-color: #fff; +$navbar-default-link-active-bg: $theme-red; +$navbar-default-link-disabled-color: #ccc !default; +$navbar-default-link-disabled-bg: transparent !default; + +// Navbar brand label +$navbar-default-brand-color: $navbar-default-link-color !default; +$navbar-default-brand-hover-color: darken($navbar-default-brand-color, 10%) !default; +$navbar-default-brand-hover-bg: darken($synnefo-green,5%); +$navbar-default-brand-bg: $synnefo-green; + +// Navbar toggle +$navbar-default-toggle-hover-bg: #ddd !default; +$navbar-default-toggle-icon-bar-bg: #888 !default; +$navbar-default-toggle-border-color: #ddd !default; + + +// Inverted navbar +// Reset inverted navbar basics +$navbar-inverse-color: $text-color; +$navbar-inverse-bg: $gray-light; +$navbar-inverse-border: transparent; + +// Inverted navbar links +$navbar-inverse-link-color: $text-color; +$navbar-inverse-link-hover-color: $reverse-text-color; +$navbar-inverse-link-hover-bg: $gray-lighter; +$navbar-inverse-link-active-color: white; +$navbar-inverse-link-active-bg: darken($navbar-inverse-bg, 10%) !default; +$navbar-inverse-link-disabled-color: #444 !default; +$navbar-inverse-link-disabled-bg: transparent !default; + +// Inverted navbar brand label +$navbar-inverse-brand-color: $navbar-inverse-link-color !default; +$navbar-inverse-brand-hover-color: #fff !default; +$navbar-inverse-brand-hover-bg: transparent !default; + +// Inverted navbar toggle +$navbar-inverse-toggle-hover-bg: #333 !default; +$navbar-inverse-toggle-icon-bar-bg: #fff !default; +$navbar-inverse-toggle-border-color: #333 !default; + + +//== Navs +// +//## + +//=== Shared nav styles +$nav-link-padding: 10px 15px !default; +$nav-link-hover-bg: $gray-lighter !default; + +$nav-disabled-link-color: $gray-light !default; +$nav-disabled-link-hover-color: $gray-light !default; + +$nav-open-link-hover-color: #fff !default; + +//== Tabs +$nav-tabs-border-color: transparent; + +$nav-tabs-link-hover-border-color: inherit; + +$nav-tabs-active-link-hover-bg: $tab-content-bg; +$nav-tabs-active-link-hover-color: $tab-content-color; +$nav-tabs-active-link-hover-border-color: inherit; +$nav-tabs-link-color: $tab-content-color; + +$nav-tabs-justified-link-border-color: #ddd !default; +$nav-tabs-justified-active-link-border-color: $body-bg !default; + +//== Pills +$nav-pills-border-radius: $border-radius-base !default; +$nav-pills-active-link-hover-bg: $component-active-bg !default; +$nav-pills-active-link-hover-color: $component-active-color !default; + + +//== Pagination +// +//## + +$pagination-color: $link-color !default; +$pagination-bg: #fff !default; +$pagination-border: #ddd !default; + +$pagination-hover-color: $link-hover-color !default; +$pagination-hover-bg: $gray-lighter !default; +$pagination-hover-border: #ddd !default; + +$pagination-active-color: #fff !default; +$pagination-active-bg: $brand-primary !default; +$pagination-active-border: $brand-primary !default; + +$pagination-disabled-color: $gray-light !default; +$pagination-disabled-bg: #fff !default; +$pagination-disabled-border: #ddd !default; + + +//== Pager +// +//## + +$pager-bg: $pagination-bg !default; +$pager-border: $pagination-border !default; +$pager-border-radius: 15px !default; + +$pager-hover-bg: $pagination-hover-bg !default; + +$pager-active-bg: $pagination-active-bg !default; +$pager-active-color: $pagination-active-color !default; + +$pager-disabled-color: $pagination-disabled-color !default; + + +//== Jumbotron +// +//## + +$jumbotron-padding: 30px !default; +$jumbotron-color: inherit !default; +$jumbotron-bg: $gray-lighter !default; +$jumbotron-heading-color: inherit !default; +$jumbotron-font-size: ceil(($font-size-base * 1.5)) !default; + + +//== Form states and alerts +// +//## Define colors for form feedback states and, by default, alerts. + +$state-success-text: #3c763d !default; +$state-success-bg: #dff0d8 !default; +$state-success-border: darken(adjust-hue($state-success-bg, -10), 5%) !default; + +$state-info-text: #31708f !default; +$state-info-bg: #d9edf7 !default; +$state-info-border: darken(adjust-hue($state-info-bg, -10), 7%) !default; + +$state-warning-text: #8a6d3b !default; +$state-warning-bg: #fcf8e3 !default; +$state-warning-border: darken(adjust-hue($state-warning-bg, -10), 5%) !default; + +$state-danger-text: #a94442 !default; +$state-danger-bg: #f2dede !default; +$state-danger-border: darken(adjust-hue($state-danger-bg, -10), 5%) !default; + + +//== Tooltips +// +//## + +//** Tooltip max width +$tooltip-max-width: 200px !default; +//** Tooltip text color +$tooltip-color: #fff !default; +//** Tooltip background color +$tooltip-bg: #000 !default; +$tooltip-opacity: .9 !default; + +//** Tooltip arrow width +$tooltip-arrow-width: 5px !default; +//** Tooltip arrow color +$tooltip-arrow-color: $tooltip-bg !default; + + +//== Popovers +// +//## + +//** Popover body background color +$popover-bg: #fff !default; +//** Popover maximum width +$popover-max-width: 276px !default; +//** Popover border color +$popover-border-color: rgba(0,0,0,.2) !default; +//** Popover fallback border color +$popover-fallback-border-color: #ccc !default; + +//** Popover title background color +$popover-title-bg: darken($popover-bg, 3%) !default; + +//** Popover arrow width +$popover-arrow-width: 10px !default; +//** Popover arrow color +$popover-arrow-color: #fff !default; + +//** Popover outer arrow width +$popover-arrow-outer-width: ($popover-arrow-width + 1) !default; +//** Popover outer arrow color +$popover-arrow-outer-color: fadein($popover-border-color, 5%) !default; +//** Popover outer arrow fallback color +$popover-arrow-outer-fallback-color: darken($popover-fallback-border-color, 20%) !default; + + +//== Labels +// +//## + +//** Default label background color +$label-default-bg: $almost-white; +//** Primary label background color +$label-primary-bg: $brand-primary !default; +//** Success label background color +$label-success-bg: $brand-success !default; +//** Info label background color +$label-info-bg: $brand-info !default; +//** Warning label background color +$label-warning-bg: $brand-warning !default; +//** Danger label background color +$label-danger-bg: $brand-danger !default; + +//** Default label text color +$label-color: $gray-dark !default; +//** Default text color of a linked label +$label-link-hover-color: #fff; + + +//== Modals +// +//## + +//** Padding applied to the modal body +$modal-inner-padding: 20px !default; + +//** Padding applied to the modal title +$modal-title-padding: 15px !default; +//** Modal title line-height +$modal-title-line-height: $line-height-base !default; + +//** Background color of modal content area +$modal-content-bg: #fff !default; +//** Modal content border color +$modal-content-border-color: rgba(0,0,0,.2) !default; +//** Modal content border color **for IE8** +$modal-content-fallback-border-color: #999 !default; + +//** Modal backdrop background color +$modal-backdrop-bg: #000 !default; +//** Modal backdrop opacity +$modal-backdrop-opacity: .5 !default; +//** Modal header border color +$modal-header-border-color: transparent; +//** Modal footer border color +$modal-footer-border-color: $modal-header-border-color !default; + +$modal-lg: 900px !default; +$modal-md: 760px !default; +$modal-sm: 300px !default; + + +//== Alerts +// +//## Define alert colors, border radius, and padding. + +$alert-padding: 15px !default; +$alert-border-radius: $border-radius-base !default; +$alert-link-font-weight: bold !default; + +$alert-success-bg: $state-success-bg !default; +$alert-success-text: $state-success-text !default; +$alert-success-border: $state-success-border !default; + +$alert-info-bg: $state-info-bg !default; +$alert-info-text: $state-info-text !default; +$alert-info-border: $state-info-border !default; + +$alert-warning-bg: $state-warning-bg !default; +$alert-warning-text: $state-warning-text !default; +$alert-warning-border: $state-warning-border !default; + +$alert-danger-bg: $state-danger-bg !default; +$alert-danger-text: $state-danger-text !default; +$alert-danger-border: $state-danger-border !default; + + +//== Progress bars +// +//## + +//** Background color of the whole progress component +$progress-bg: #f5f5f5 !default; +//** Progress bar text color +$progress-bar-color: #fff !default; + +//** Default progress bar color +$progress-bar-bg: $brand-primary !default; +//** Success progress bar color +$progress-bar-success-bg: $brand-success !default; +//** Warning progress bar color +$progress-bar-warning-bg: $brand-warning !default; +//** Danger progress bar color +$progress-bar-danger-bg: $brand-danger !default; +//** Info progress bar color +$progress-bar-info-bg: $brand-info !default; + + +//== List group +// +//## + +//** Background color on `.list-group-item` +$list-group-bg: #fff !default; +//** `.list-group-item` border color +$list-group-border: #ddd !default; +//** List group border radius +$list-group-border-radius: $border-radius-base !default; + +//** Background color of single list elements on hover +$list-group-hover-bg: #f5f5f5 !default; +//** Text color of active list elements +$list-group-active-color: $component-active-color !default; +//** Background color of active list elements +$list-group-active-bg: $component-active-bg !default; +//** Border color of active list elements +$list-group-active-border: $list-group-active-bg !default; +$list-group-active-text-color: lighten($list-group-active-bg, 40%) !default; + +$list-group-link-color: #555 !default; +$list-group-link-heading-color: #333 !default; + + +//== Panels +// +//## + +$panel-bg: #fff !default; +$panel-body-padding: 15px !default; +$panel-border-radius: $border-radius-base !default; + +//** Border color for elements within panels +$panel-inner-border: #ddd !default; +$panel-footer-bg: #f5f5f5 !default; + +$panel-default-text: $gray-dark !default; +$panel-default-border: #ddd !default; +$panel-default-heading-bg: #f5f5f5 !default; + +$panel-primary-text: #fff !default; +$panel-primary-border: $brand-primary !default; +$panel-primary-heading-bg: $brand-primary !default; + +$panel-success-text: $state-success-text !default; +$panel-success-border: $state-success-border !default; +$panel-success-heading-bg: $state-success-bg !default; + +$panel-info-text: $state-info-text !default; +$panel-info-border: $state-info-border !default; +$panel-info-heading-bg: $state-info-bg !default; + +$panel-warning-text: $state-warning-text !default; +$panel-warning-border: $state-warning-border !default; +$panel-warning-heading-bg: $state-warning-bg !default; + +$panel-danger-text: $state-danger-text !default; +$panel-danger-border: $state-danger-border !default; +$panel-danger-heading-bg: $state-danger-bg !default; + + +//== Thumbnails +// +//## + +//** Padding around the thumbnail image +$thumbnail-padding: 4px !default; +//** Thumbnail background color +$thumbnail-bg: $body-bg !default; +//** Thumbnail border color +$thumbnail-border: #ddd !default; +//** Thumbnail border radius +$thumbnail-border-radius: $border-radius-base !default; + +//** Custom text color for thumbnail captions +$thumbnail-caption-color: $text-color !default; +//** Padding around the thumbnail caption +$thumbnail-caption-padding: 9px !default; + + +//== Wells +// +//## + +$well-bg: inherit; +$well-border: inherit; + + +//== Badges +// +//## + +$badge-color: inherit; +//** Linked badge text color on hover +$badge-link-hover-color: inherit; +$badge-bg: $gray-light !default; + +//** Badge text color in active nav link +$badge-active-color: $link-color !default; +//** Badge background color in active nav link +$badge-active-bg: #fff !default; + +$badge-font-weight: bold !default; +$badge-line-height: 1 !default; +$badge-border-radius: 0; + + +//== Breadcrumbs +// +//## + +$breadcrumb-padding-vertical: 8px !default; +$breadcrumb-padding-horizontal: 15px !default; +//** Breadcrumb background color +$breadcrumb-bg: #f5f5f5 !default; +//** Breadcrumb text color +$breadcrumb-color: #ccc !default; +//** Text color of current page in the breadcrumb +$breadcrumb-active-color: $gray-light !default; +//** Textual separator for between breadcrumb elements +$breadcrumb-separator: "/" !default; + + +//== Carousel +// +//## + +$carousel-text-shadow: 0 1px 2px rgba(0,0,0,.6) !default; + +$carousel-control-color: #fff !default; +$carousel-control-width: 15% !default; +$carousel-control-opacity: .5 !default; +$carousel-control-font-size: 20px !default; + +$carousel-indicator-active-bg: #fff !default; +$carousel-indicator-border-color: #fff !default; + +$carousel-caption-color: #fff !default; + + +//== Close +// +//## + +$close-font-weight: bold !default; +$close-color: #000 !default; +$close-text-shadow: 0 1px 0 #fff !default; + + +//== Code +// +//## + +$code-color: #c7254e !default; +$code-bg: #f9f2f4 !default; + +$kbd-color: #fff !default; +$kbd-bg: #333 !default; + +$pre-bg: #f5f5f5 !default; +$pre-color: $gray-dark !default; +$pre-border-color: #ccc !default; +$pre-scrollable-max-height: 340px !default; + + +//== Type +// +//## + +//** Text muted color +$text-muted: $gray-light !default; +//** Abbreviations and acronyms border color +$abbr-border-color: $gray-light !default; +//** Headings small color +$headings-small-color: $gray-light !default; +//** Blockquote small color +$blockquote-small-color: $gray-light !default; +//** Blockquote font size +$blockquote-font-size: ($font-size-base * 1.25) !default; +//** Blockquote border color +$blockquote-border-color: $gray-lighter !default; +//** Page header border color +$page-header-border-color: $gray-lighter !default; + + +//== Miscellaneous +// +//## + +//** Horizontal line color. +$hr-border: $gray-lighter !default; + +//** Horizontal offset for forms and lists. +$component-offset-horizontal: 180px !default; diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_wells.scss b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_wells.scss new file mode 100644 index 0000000000000000000000000000000000000000..6b8d85f233c374d09f2a41653c46bc28462973e2 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/bootstrap/_wells.scss @@ -0,0 +1,28 @@ +// +// Wells +// -------------------------------------------------- + + +// Base class +.well { + min-height: 20px; + padding: 0; + margin-bottom: 20px; + background-color: $well-bg; + border: 1px solid $well-border; + border-radius: $border-radius-base; + blockquote { + border-color: #ddd; + border-color: rgba(0,0,0,.15); + } +} + +// Sizes +.well-lg { + padding: 24px; + border-radius: $border-radius-large; +} +.well-sm { + padding: 9px; + border-radius: $border-radius-small; +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/icon-fonts.scss b/snf-admin-app/synnefo_admin/admin/static/sass/icon-fonts.scss new file mode 100644 index 0000000000000000000000000000000000000000..6ef97c0ef5d2ba9d0a6b0a4de276fa38daa6ec19 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/icon-fonts.scss @@ -0,0 +1,377 @@ +@font-face { + font-family: 'font-icons'; + src:url('../fonts/font-icons.eot?hm0cup'); + src:url('../fonts/font-icons.eot?#iefixhm0cup') format('embedded-opentype'), + url('../fonts/font-icons.woff?hm0cup') format('woff'), + url('../fonts/font-icons.ttf?hm0cup') format('truetype'), + url('../fonts/font-icons.svg?hm0cup#font-icons') format('svg'); + font-weight: normal; + font-style: normal; +} + +/* Font with kpal icons */ + +@font-face { + font-family: "snf-font"; + src:url("../fonts/snf-font.eot"); + src:url("../fonts/snf-font.eot?#iefix") format("embedded-opentype"), + url("../fonts/snf-font.woff") format("woff"), + url("../fonts/snf-font.ttf") format("truetype"), + url("../fonts/snf-font.svg#snf-font") format("svg"); + font-weight: normal; + font-style: normal; +} + +@mixin font-base($font-family){ + font-family: $font-family; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +@mixin font-icon($font-family, $char) { + @include font-base($font-family); + &:before { + content: $char; + } +} + + +.snf-ok{ + @include font-icon('font-icons',"\61"); +} +.snf-remove { + @include font-icon('font-icons', "\62"); +} +.snf-envelope { + @include font-icon('font-icons', "\63"); +} +.snf-envelope-alt { + @include font-icon('font-icons', "\64"); +} +.snf-angle-up { + @include font-icon('font-icons', "\65"); +} +.snf-angle-down { + @include font-icon('font-icons', "\66"); +} +.snf-exclamation-sign { + @include font-icon('font-icons', "\67"); +} +.snf-clipboard-h { + @include font-icon('font-icons', "\68"); +} +.snf-clipboard-i { + @include font-icon('font-icons', "\69"); +} +.snf-copy { + @include font-icon('font-icons', "\6c"); +} +.snf-search { + @include font-icon('font-icons', "\6d"); +} +.snf-sign-out { + @include font-icon('font-icons', "\6e"); +} +.snf-archive { + @include font-icon('font-icons', "\6b"); +} +.snf-checkbox-checked { + @include font-icon('font-icons', "\6f"); +} +.snf-checkbox-unchecked { + @include font-icon('font-icons', "\70"); +} +.snf-radio-checked { + @include font-icon('font-icons', "\71"); +} +.snf-radio-unchecked { + @include font-icon('font-icons', "\72"); +} +.snf-info { + @include font-icon('font-icons', "\73"); +} +.snf-user-outline { + @include font-icon('font-icons', "\75"); +} +.snf-user-full { + @include font-icon('font-icons', "\74"); +} +.snf-wallet-full { + @include font-icon('font-icons', "\78"); +} +.snf-wallet-outline { + @include font-icon('font-icons', "\79"); +} +.snf-keyboard { + @include font-icon('font-icons', "\7a"); +} +.snf-book-2 { + @include font-icon('font-icons', "\42"); +} +.snf-bell-1 { + @include font-icon('font-icons', "\43"); +} +.snf-bulb { + @include font-icon('font-icons', "\46"); +} +.snf-sun-1 { + @include font-icon('font-icons', "\47"); +} +.snf-moon-1 { + @include font-icon('font-icons', "\76"); +} +.snf-sun-2-full { + @include font-icon('font-icons', "\77"); +} +.snf-sun-2-outline { + @include font-icon('font-icons', "\6a"); +} +.snf-moon-2-full:before { + @include font-icon('font-icons', "\44"); +} +.snf-moon-2-outline { + @include font-icon('font-icons', "\45"); +} +.snf-sun-3 { + @include font-icon('font-icons', "\41"); +} +.snf-filter { + @include font-icon('font-icons', "\7b"); +} +.snf-eye { + @include font-icon('snf-font', "\41"); +} +.snf-radio-checked { + @include font-icon('snf-font', "\42"); +} +.snf-radio-unchecked { + @include font-icon('snf-font', "\43"); +} +.snf-close { + @include font-icon('snf-font', "\44"); +} +.snf-www { + @include font-icon('snf-font', "\49"); +} +.snf-arrow-up { + @include font-icon('snf-font', "\4c"); +} +.snf-arrow-down { + @include font-icon('snf-font', "\4d"); +} +.snf-checkbox-unchecked { + @include font-icon('snf-font', "\61"); +} +.snf-checkbox-checked { + @include font-icon('snf-font', "\62"); +} +.snf-cancel-circled { + @include font-icon('snf-font', "\63"); +} +.snf-search { + @include font-icon('snf-font', "\64"); +} +.snf-twitter-logo { + @include font-icon('snf-font', "\67"); +} +.snf-ok { + @include font-icon('snf-font', "\68"); +} +.snf-switch { + @include font-icon('snf-font', "\69"); +} +.snf-ban-circle { + @include font-icon('snf-font', "\6a"); +} +.snf-ok-sign { + @include font-icon('snf-font', "\6c"); +} +.snf-minus-sign { + @include font-icon('snf-font', "\6e"); +} +.snf-edit { + @include font-icon('snf-font', "\71"); +} +.snf-listview { + @include font-icon('snf-font', "\73"); +} +.snf-gridview { + @include font-icon('snf-font', "\74"); +} +.snf-dashboard-outline { + @include font-icon('snf-font', "\7a"); +} +.snf-pithos-outline { + @include font-icon('snf-font', "\79"); +} +.snf-info-full { + @include font-icon('snf-font', "\70"); +} +.snf-volume-create-full { + @include font-icon('snf-font', "\36"); +} +.snf-image-full { + @include font-icon('snf-font', "\51"); +} +.snf-pc-create-full { + @include font-icon('snf-font', "\53"); +} +.snf-network-create-outline { + @include font-icon('snf-font', "\54"); +} +.snf-network-create-full { + @include font-icon('snf-font', "\55"); +} +.snf-ram-outline { + @include font-icon('snf-font', "\4a"); +} +.snf-nic-outline { + @include font-icon('snf-font', "\50"); +} +.snf-ram-full { + @include font-icon('snf-font', "\52"); +} +.snf-nic-full { + @include font-icon('snf-font', "\72"); +} +.snf-network-broken-1-full { + @include font-icon('snf-font', "\56"); +} +.snf-network-broken-2-full { + @include font-icon('snf-font', "\57"); +} +.snf-pc-broken-full { + @include font-icon('snf-font', "\58"); +} +.snf-pc-reboot-full { + @include font-icon('snf-font', "\59"); +} +.snf-pc-switch-full { + @include font-icon('snf-font', "\5a"); +} +.snf-key-full { + @include font-icon('snf-font', "\31"); +} +.snf-router-full { + @include font-icon('snf-font', "\32"); +} +.snf-chip-full { + @include font-icon('snf-font', "\33"); +} +.snf-plus-full { + @include font-icon('snf-font', "\34"); +} +.snf-snapshot-full { + @include font-icon('snf-font', "\4e"); +} +.snf-pithos-full { + @include font-icon('snf-font', "\35"); +} +.snf-volume-full { + @include font-icon('snf-font', "\4f"); +} +.snf-network-full { + @include font-icon('snf-font', "\4b"); +} +.snf-pc-full { + @include font-icon('snf-font', "\78"); +} +.snf-network-broken-1-outline { + @include font-icon('snf-font', "\37"); +} +.snf-network-broken-2-outline { + @include font-icon('snf-font', "\38"); +} +.snf-pc-broken-outline { + @include font-icon('snf-font', "\39"); +} +.snf-volume-broken-outline { + @include font-icon('snf-font', "\30"); +} +.snf-pc-reboot-outline { + @include font-icon('snf-font', "\21"); +} +.snf-pc-switch-outline { + @include font-icon('snf-font', "\40"); +} +.snf-key-outline { + @include font-icon('snf-font', "\23"); +} +.snf-router-outline { + @include font-icon('snf-font', "\48"); +} +.snf-chip-outline { + @include font-icon('snf-font', "\45"); +} +.snf-image-outline { + @include font-icon('snf-font', "\66"); +} +.snf-plus-outline { + @include font-icon('snf-font', "\6d"); +} +.snf-snapshot-outline { + @include font-icon('snf-font', "\65"); +} +.snf-volume-outline { + @include font-icon('snf-font', "\75"); +} +.snf-network-outline { + @include font-icon('snf-font', "\76"); +} +.snf-pc-outline { + @include font-icon('snf-font', "\77"); +} +.snf-info-outline { + @include font-icon('snf-font', "\6f"); +} +.snf-thunder-full { + @include font-icon('snf-font', "\6b"); +} +.snf-lock-closed-full { + @include font-icon('snf-font', "\46"); +} +.snf-lock-open-full { + @include font-icon('snf-font', "\47"); +} + +.snf-link-outline { + @include font-icon('snf-font', "\26"); +} +.snf-refresh-outline { + @include font-icon('snf-font', "\29"); +} +.snf-download-full { + @include font-icon('snf-font', "\25"); +} +.snf-person-outline { + @include font-icon('snf-font', "\2a"); +} +.snf-upload-full { + @include font-icon('snf-font', "\28"); +} +.snf-arrow-right-small-full { + @include font-icon('snf-font', "\2d"); +} +.snf-copy-outline { + @include font-icon('snf-font', "\3f"); +} +.snf-copy-full { + @include font-icon('snf-font', "\22"); +} +.snf-arrow-left-small-full { + @include font-icon('snf-font', "\5f"); +} +.snf-trash-full { + @include font-icon('snf-font', "\3d"); +} +.snf-trash-outline { + @include font-icon('snf-font', "\24"); +} diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/main-light.scss b/snf-admin-app/synnefo_admin/admin/static/sass/main-light.scss new file mode 100644 index 0000000000000000000000000000000000000000..cb4ec9cd7475844b2aa7a65341a8d63acf865335 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/main-light.scss @@ -0,0 +1,6 @@ +@import "settings"; + +@import "theme-light"; + +@import "bootstrap"; +@import "global"; diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/main.scss b/snf-admin-app/synnefo_admin/admin/static/sass/main.scss new file mode 100644 index 0000000000000000000000000000000000000000..6bfbdb1519562963c3c75cc326a1643c3db2532e --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/main.scss @@ -0,0 +1,6 @@ +@import "settings"; + +@import "bootstrap/variables"; + +@import "bootstrap"; +@import "global"; diff --git a/snf-admin-app/synnefo_admin/admin/static/sass/screen.scss b/snf-admin-app/synnefo_admin/admin/static/sass/screen.scss new file mode 100644 index 0000000000000000000000000000000000000000..81de8470349d8ff8a47687d84cc3bdae1a035174 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/static/sass/screen.scss @@ -0,0 +1,6 @@ +/* Welcome to Compass. + * In this file you should write your main styles. (or centralize your imports) + * Import this file using the following HTML or equivalent: + * <link href="/stylesheets/screen.css" media="screen, projection" rel="stylesheet" type="text/css" /> */ + +@import "compass/reset"; diff --git a/snf-admin-app/synnefo_admin/admin/tables.py b/snf-admin-app/synnefo_admin/admin/tables.py new file mode 100644 index 0000000000000000000000000000000000000000..321d4de30f22fd9d35a35777d62a0b0178b7c45b --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/tables.py @@ -0,0 +1,39 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from eztables.views import DatatablesView +from django.utils.html import escape + + +def escape_row(row): + """Escape a whole row using Django's escape function.""" + return [escape(cell) for cell in row] + + +class AdminJSONView(DatatablesView): + + """Class-based Django view for admin purposes. + + It is based on the DataTablesView class of django-eztables plugin and aims + to provide some common functionality for all the views that are derived + from it. + """ + + def format_data_rows(self, rows): + if hasattr(self, 'format_data_row'): + rows = [escape_row(self.format_data_row(row)) for row in rows] + else: + rows = [escape_row(row) for row in rows] + return rows diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/_action_modal.html b/snf-admin-app/synnefo_admin/admin/templates/admin/_action_modal.html new file mode 100644 index 0000000000000000000000000000000000000000..c63aca335516fda4883c5636a5698aea25b27996 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/_action_modal.html @@ -0,0 +1,65 @@ +{% load admin_tags %} +<!--<div class="modal fade" id="{{type}}-{{ op }}" data-type="{{ op }}" data-backdrop="static" data-keyboard="false">--> +<div class="modal fade" data-item ={{ action.target }} id="{{action.target}}-{{ op }}" data-backdrop="static" data-keyboard="true" tabindex="-1" data-karma={{ action.karma }} data-caution={{ action.caution_level }} data-type={{ op }}> + <div class="modal-dialog modal-md"> + <div class="modal-content area"> + <div class="modal-header"> + <a class="close cancel" data-dismiss="modal">×</a> + <h3 class="elem">{{ action.name }}</h3> + </div> + <div class="modal-body"> + {% if op == "contact" %} + <div class="form-sender form-area"> + <label>From:</label> + <input type="text" name="sender" class="sender" + form="contactForm" value="{{mail.sender}}" /> + <a data-error="empty-sender" data-toggle="popover" data-trigger="hover" class="error-sign snf-exclamation-sign" href="#" rel="tooltip" data-content="Missing the sender address of the e‑mail."></a> + <a data-error="invalid-email" data-toggle="popover" data-trigger="hover" class="error-sign snf-exclamation-sign" href="#" rel="tooltip" data-content="Invalid e‑mail address."></a> + </div> + </br> + <div class="form-subject form-area"> + <label>Subject:</label> + <input type="text" name="subject" class="subject" + form="contactForm" value="{{mail.subject}}" /> + <a data-error="empty-subject" data-toggle="popover" data-trigger="hover" class="error-sign snf-exclamation-sign" href="#" rel="tooltip" data-content="Missing the subject of the e‑mail."></a> + </div> + <div class="form-body form-area"> + <label>Body:</label> + <textarea name="text" form="contactForm" class="email-content body">{{ mail.body }}</textarea> + <a data-error="empty-body" data-toggle="popover" data-trigger="hover" class="error-sign snf-exclamation-sign" href="#" rel="tooltip" data-content="Missing the body of the e-mail."></a> + <div class="extra-info"> + <a href="" class="snf-info instructions-icon" data-container="#{{action.target}}-{{ op }}" data-toggle="popover" data-html="true" data-placement="right" data-content=" + <h2>Legend</h2> + <dl class='dl-horizontal'> + {% for name, attr in mail.legend.items %} + <dt>{{ name }}: </dt><dd>{{ attr }}</dt> + {% endfor %} + </dl>"></a> + <!-- place here the msg for duplicates --> + </div> + </div> + {# If we are in a details view, then the number of items is just one, so we can get rid of the plural #} + <div class="summary"> + <p>You have chosen to <em class="elem lowercase">{{ action.name }}</em> to the following <em class="num elem"></em> user{% if view_type == 'list' %}s{% endif %}: + {% else %} + <div class="summary"> + <p>You have chosen to <em class="elem lowercase">{{ action.name }}</em> the following <em class="num elem"></em> {{action.target}}{% if view_type == 'list' %}s{% endif %}: + {% endif %} + <a data-error="no-selected" data-toggle="popover" data-trigger="hover" class="error-sign snf-exclamation-sign" href="#" data-content="There are no selected items to complete this action." rel="tooltip"></a> + </p> + {% if view_type == "list" %} + <table class="table-selected table table-bordered"> + <tbody> + </tbody> + </table> + <button class= "custom-btn toggle-more closed" data-karma="dark"><span>Show All</span></button> + {% endif %} + </div> + </div> + <div class="modal-footer"> + <a href="#" class="custom-btn cancel" data-dismiss="modal"><span>Cancel</span></a> + <a href="#" data-url={% url admin-actions %} data-op={{ op }} data-ids="" data-target={{ action.target }} class="custom-btn apply-action" data-karma={{ action.karma }} data-caution={{ action.caution_level }} data-dismiss="modal"><span>{{ action.name }}</span></a> + </div> + </div> + </div> +</div> diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/_base_list_head.html b/snf-admin-app/synnefo_admin/admin/templates/admin/_base_list_head.html new file mode 100644 index 0000000000000000000000000000000000000000..9acb61130286c0e202f059fd0cf65b0ec2604ae8 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/_base_list_head.html @@ -0,0 +1,41 @@ +{% load admin_tags %} + +<div class="object-anchor" id="{{ type }}-area"></div> +<div class="object-details info-block well {{ rowcls }}"> +{% if association.items|length > 0 and type != 'ip_log' %} +<a href="#" class="show-hide-all line-btn"> + <span class="snf-font-arrow-up"></span> + <span class="snf-font-arrow-down"></span> + <span class="txt">Collapse All </span> + <em>({{ association.items|length }})</em> + +</a> +{% endif %} +<h3> + <span class="snf-details-{{type}}"></span> + {{ type|display_list_type }} + <em> of '{{ main_item|repr }}'</em> + {% if association.excluded or association.excluded|add:association.showing < association.total %} + <span class="popover-dismiss" data-html="true" data-trigger="hover" + data-toggle="popover" data-content='{% include "admin/tooltip_associations.html" %}'> + ! + </span> + {% endif %} +</h3> + +{% if type == 'ip_log' and association.items|length > 0 %} +<table class="table ip_log"> + <thead> + <tr> + <th>IP</th> + <th>VM</th> + <th>Network</th> + <th>User</th> + <th>Attached date</th> + <th>Detached date</th> + <th>State</th> + </tr> + </thead> + <tbody> +{% endif %} + diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/_base_list_tail.html b/snf-admin-app/synnefo_admin/admin/templates/admin/_base_list_tail.html new file mode 100644 index 0000000000000000000000000000000000000000..f57e45dccc36d4078912003c26d5fd2f8f6f19b7 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/_base_list_tail.html @@ -0,0 +1,6 @@ +{% if type == 'ip_log' %} + </tbody> +</table> +{% endif %} +</div> + diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/_base_main_item_head.html b/snf-admin-app/synnefo_admin/admin/templates/admin/_base_main_item_head.html new file mode 100644 index 0000000000000000000000000000000000000000..dcf76f5d4f9a7393daee477668dc206ff33fb8d5 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/_base_main_item_head.html @@ -0,0 +1 @@ +<div class="info-block main"> diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/_base_main_item_tail.html b/snf-admin-app/synnefo_admin/admin/templates/admin/_base_main_item_tail.html new file mode 100644 index 0000000000000000000000000000000000000000..04f5b84499777605571b7c1980fe23d176b69e1f --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/_base_main_item_tail.html @@ -0,0 +1 @@ +</div> diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/_details_h4_lt.html b/snf-admin-app/synnefo_admin/admin/templates/admin/_details_h4_lt.html new file mode 100644 index 0000000000000000000000000000000000000000..5296c0dc26ab47749e9199e706f0d19fe7acc72e --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/_details_h4_lt.html @@ -0,0 +1,11 @@ +{% load admin_tags %} +<span class="lt"> + {% if type != main_type %} + <a href="{% url admin-details type item|id %}" class="snf-search icon-link" title="Details for {{ item|repr }}"></a> + {% else %} + <span class="snf-details-{{type}}"></span> + {% endif %} + <span class="title">{{ item|repr }}</span> + <span class="arrow snf-angle-down" role="button" tabindex="0"></span> +</span> + diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/_ip_details.html b/snf-admin-app/synnefo_admin/admin/templates/admin/_ip_details.html new file mode 100644 index 0000000000000000000000000000000000000000..e49674bcfa508094de8e6cc1d5d4e0810dec7fa9 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/_ip_details.html @@ -0,0 +1,41 @@ +{% load admin_tags %} + +{% with ip=item %} +<div class="object-anchor" id="ip-{{ip.pk}}"></div> +<div class="object-details {{ rowcls }}" data-id="{{ip.pk}}" + data-type="{{type}}"> + <h4 class="clearfix"> + {% include "admin/_details_h4_lt.html" %} + <span class="rt"> + <span class="label">ID: {{ip.pk}}</span> + {% if ip.in_use %} + <span class="label">VM ID: {{ ip.nic.machine.pk }}</span> + {% endif %} + <span class="label">IPv{{ ip.ipversion }}</span> + </span> + </h4> + <div class="tags"> + </div> + <div class="object-details-content"> + <ul class="nav nav-tabs"> + <li class="active"><a href="#details{{ type }}{{ ip.pk }}" data-toggle="tab">Details</a></li> + </ul> + <div class="tab-content"> + <div class="tab-pane active" id="details{{ type }}{{ ip.pk }}"> + <dl class="dl-horizontal well"> + <dt>ID</dt><dd>{{ ip.pk }}</dd> + <dt>Address</dt><dd>{{ ip.address }}</dd> + <dt>Floating</dt><dd>{{ ip.floating_ip}}</dd> + <dt>Created</dt><dd>{{ ip.created }} ({{ ip.created|timesince }} ago)</dd> + <dt>Updated</dt><dd>{{ ip.updated }} ({{ ip.created|timesince }} ago)</dd> + <dt>Floating</dt><dd>{{ ip.floating_ip}}</dd> + <dt>Deleted</dt><dd>{{ ip.deleted}}</dd> + + </dl> + </div> + </div> <!-- <div class="tab-content"> --> + </div> <!-- <div class="object-details-content"> --> + {% include "admin/action_list_horizontal.html" %} +</div> <!-- <div class="object-details {{ rowcls }}"> --> +{% endwith %} + diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/_ip_log_details.html b/snf-admin-app/synnefo_admin/admin/templates/admin/_ip_log_details.html new file mode 100644 index 0000000000000000000000000000000000000000..4da55cdf9324dbc002ea6bd0de9c7cb2aeeb1796 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/_ip_log_details.html @@ -0,0 +1,19 @@ +{% load admin_tags %} + +{% with ip_log=item %} +<tr> + <td>{{ ip_log|details_url:"ip"|safe }}</td> + <td>{{ ip_log|details_url:"vm"|safe }}</td> + <td>{{ ip_log|details_url:"network"|safe }}</td> + <td>{{ ip_log|details_url:"user"|safe }}</td> + <td>{{ ip_log.allocated_at }}</td> + <td>{{ ip_log.released_at|default:"-" }}</td> + <td> + {% if ip_log.active %} + <span class="label label-success">In Use</span> + {% else %} + Free + {% endif %} + </td> +</tr> +{% endwith %} diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/_network_details.html b/snf-admin-app/synnefo_admin/admin/templates/admin/_network_details.html new file mode 100644 index 0000000000000000000000000000000000000000..e1d178b3d3b18a765b950db90d401f80e48471b9 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/_network_details.html @@ -0,0 +1,37 @@ +{% load admin_tags %} + +{% with network=item %} +<div class="object-anchor" id="network-{{network.pk}}"></div> +<div class="network-details object-details {{ rowcls }}" data-id="{{network.pk}}" + data-type="{{type}}"> + <h4 class="clearfix"> + {% include "admin/_details_h4_lt.html" %} + <span class="rt"> + {{ network|status_label|safe }} + <span class="label">ID: {{ network.pk }}</span> + {% if network.public %} + <span class="label">PUBLIC</span> + {% endif %} + </span> + </h4> + <div class="network-details-content object-details-content"> + <ul class="nav nav-tabs"> + <li class="active"><a href="#network-details{{ network.pk }}" data-toggle="tab">Details</a></li> + </ul> + <div class="tab-content"> + <div class="tab-pane active" id="network-details{{ network.pk }}"> + <dl class="dl-horizontal well"> + <dt>ID</dt><dd>{{ network.pk }}</dd> + <dt>Name</dt><dd>{{ network.name|default:"-" }}</dd> + <dt>Public</dt><dd>{{ network.public }}</dd> + <dt>User ID</dt><dd>{{ network.userid }}</dd> + <dt>Created</dt><dd>{{ network.created }} ({{ network.created|timesince }} ago)</dd> + <dt>Updated</dt><dd>{{ network.updated }} ({{ network.created|timesince }} ago)</dd> + <dt>State</dt><dd>{{ network.get_state_display }} ({{ network.state }})</dd> + </dl> + </div> + </div> + </div> + {% include "admin/action_list_horizontal.html" %} +</div> +{% endwith %} diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/_nic_details.html b/snf-admin-app/synnefo_admin/admin/templates/admin/_nic_details.html new file mode 100644 index 0000000000000000000000000000000000000000..79465824f1c641f78f9f30f10d33894fb768667f --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/_nic_details.html @@ -0,0 +1,56 @@ +{% load admin_tags %} + +{% with nic=item %} +<div class="object-anchor" id="nic-{{nic.pk}}"></div> +<div class="object-details {{ rowcls }}"> + <h4 class="clearfix"> + <span class="lt"> + <span class="title">{{ nic|repr }}</span> + <span class="arrow snf-angle-down" role="button" tabindex="0"></span> + </span> + <span class="rt"> + <span class="label">ID: {{nic.pk}}</span> + <span class="label">MAC: {{ nic.mac }}</span> + <span class="label">{{ nic.state|upper }}</span> + </span> + </h4> + <div class="tags"> + </div> + <div class="object-details-content"> + <ul class="nav nav-tabs"> + <li class="active"><a href="#details{{ type }}{{ nic.pk }}" data-toggle="tab">Details</a></li> + <li><a href="#security{{ type }}{{ nic.pk }}" data-toggle="tab">Security</a></li> + </ul> + <div class="tab-content"> + <div class="tab-pane active" id="details{{ type }}{{ nic.pk }}"> + <dl class="dl-horizontal well"> + <dt>ID</dt><dd>{{ nic.pk }}</dd> + <dt>MAC</dt><dd>{{ nic.mac }}</dd> + <dt>Name</dt><dd>{{ nic.name|default:"-" }}</dd> + <dt>Public</dt><dd>{{ nic.public }}</dd> + <dt>User ID</dt><dd>{{ nic.userid }}</dd> + <dt>Created</dt><dd>{{ nic.created }} ({{ nic.created|timesince }} ago)</dd> + <dt>Updated</dt><dd>{{ nic.updated }} ({{ nic.created|timesince }} ago)</dd> + <dt>State</dt><dd>{{ nic.state }}</dd> + </dl> + </div> + <div class="tab-pane" id="security{{ type }}{{ nic.pk }}"> + <dl class="dl-horizontal well"> + <dt>Firewall profile</dt><dd>{{ nic.firewall_profile }}</dd> + <dt>Security groups</dt><dd> + {% for group in nic.security_groups.all %} + {{ group.name|upper }} + {% empty %} + None + {% endfor %} + </dd> + </dl> + </div> + </div> <!-- <div class="tab-content"> --> + </div> <!-- <div class="object-details-content"> --> + <div class="todo vm-actions clearfix"> + {% include "admin/action_list_horizontal.html" %} + </div> +</div> <!-- <div class="object-details {{ rowcls }}"> --> +{% endwith %} + diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/_project_details.html b/snf-admin-app/synnefo_admin/admin/templates/admin/_project_details.html new file mode 100644 index 0000000000000000000000000000000000000000..6cbf47832c09064dc225bbdd8cd18ef4a3b56008 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/_project_details.html @@ -0,0 +1,118 @@ +{% load admin_tags %} + +{% with project=item page='user' %} +<div class="object-anchor" id="project-{{project.uuid}}"></div> +<div class="object-details {{ rowcls }}" data-id="{{project.uuid}}" + data-type="{{type}}"> + <h4 class="clearfix"> + {% include "admin/_details_h4_lt.html" %} + <span class="rt"> + {{ project|status_label|safe }} + {% if project.last_application %} + {{ project.last_application|status_label|safe }} + {% endif %} + <span class="label">UUID: {{ project.uuid }}</span> + {% if project.is_base %} + <span class="label label-info"> + SYSTEM + {% else %} + <span class="label"> + Members: {{ project.members_count }} + {% endif %} + </span> + </span> + </h4> + <div class="object-details-content"> + <ul class="nav nav-tabs"> + <li class="active"><a href="#prj-details{{ project.uuid }}" data-toggle="tab">Details</a></li> + <li><a href="#prj-members{{ project.uuid }}" data-toggle="tab">Members</a></li> + <li><a href="#prj-policies{{ project.uuid }}" data-toggle="tab">Member Policies</a></li> + <li><a href="#resources{{ project.uuid }}" data-toggle="tab">Resources</a></li> + </ul> + <div class="tab-content"> + <div class="tab-pane active" id="prj-details{{ project.uuid }}"> + <dl class="dl-horizontal well"> + <dt>ID</dt><dd>{{ project.id }}</dd> + <dt>UUID</dt><dd>{{ project.uuid }}</dd> + <dt>Name</dt><dd>{{ project.realname|default:"(not set)" }}</dd> + {% if not project.is_base %} + <dt>Owner</dt><dd>{{ project.owner.realname|default:"(not set)" }}</dd> + <dt>Homepage url</dt><dd>{{ project.homepage|default:"(not set)" }}</dd> + {% endif %} + <dt>Description</dt><dd>{{ project.description }}</dd> + <dt>Creation date</dt><dd>{{ project.creation_date }}</dd> + {% if not project.is_base %} + <dt>End date</dt><dd>{{ project.end_date }}</dd> + {% endif %} + <dt>Project Status</dt><dd>{{ project|get_status_from_instance }}</dd> + {% if project.last_application %} + <dt>Application Status</dt><dd>{{ project.last_application|get_status_from_instance }}</dd> + {% endif %} + </dl> + </div> + <div class="tab-pane" id="prj-members{{ project.uuid }}"> + <table class="table"> + <thead> + <tr> + <td>UUID</td> + <td>Name</td> + <td>Status</td> + </tr> + </thead> + <tbody> + {% with members=project|get_project_members %} + {% for m in members %} + <tr> + {% for val in m %} + <td>{{ val }}</td> + {% endfor %} + </tr> + {% empty %} + <tr> + <td colspan=3>No members available</td> + </tr> + {% endfor %} + {% endwith %} + </tbody> + </table> + </div> + <div class="tab-pane" id="prj-policies{{ project.uuid }}"> + <dl class="dl-horizontal well"> + <dt>Max members</dt><dd>{{ project.limit_on_members_number }}</dd> + <dt>Current members</dt><dd>{{ project.members_count }}</dd> + <dt>Join policy</dt><dd>{{ project.member_join_policy_display }}</dd> + <dt>Leave policy</dt><dd>{{ project.member_leave_policy_display }}</dd> + </dl> + </div> + + <div class="tab-pane" id="resources{{ project.uuid }}"> + <table class="table"> + <thead> + <tr> + <th></th> + {% if not project.is_base %} + <th>Max per Member</th> + {% endif %} + <th>Max per Project</th> + <th>Usage</th> + </tr> + </thead> + <tbody> + {% with stats=project|get_project_stats %} + {% for resource_name, values in stats.items %} + <tr> + <td>{{ resource_name }}</td> + {% for value in values %} + <td>{{ value }}</td> + {% endfor %} + </tr> + {% endfor %} + {% endwith %} + </tbody> + </table> + </div> + </div> + </div> + {% include "admin/action_list_horizontal.html" %} +</div> +{% endwith %} diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/_quota_details.html b/snf-admin-app/synnefo_admin/admin/templates/admin/_quota_details.html new file mode 100644 index 0000000000000000000000000000000000000000..2d63c60394fbd477ac9b5adb7dcc82514958b8b9 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/_quota_details.html @@ -0,0 +1,32 @@ +{% load admin_tags %} +{% with source=item project=item.project %} +<div class="object-anchor" id="quota-{{project|id}}"></div> +<div class="object-details {{ rowcls }}"> + <h4 class="clearfix"> + <span class="lt"> + <span class="title">{{ project|repr }}</span> + <span class="arrow snf-angle-down" role="button" tabindex="0"></span> + </span> + <span class="rt"> + <span class="label">ID: {{ project.id }}</span> + </span> + </h4> + <div class="object-details-content"> + <ul class="nav nav-tabs"> + <li class="active"><a href="#quota-details{{ project.id }}" data-toggle="tab">Details</a></li> + </ul> + <div class="tab-content"> + <div class="tab-pane active" id="quota-details{{ project.id }}"> + <dl class="dl-horizontal well"> + {% for name, usage,limit in source.resources %} + <dt>{{ name }}</dt><dd>{{ usage }} from {{ limit }}</dd> + {% endfor %} + </dl> + </div> + </div> + </div> + <div class="todo vm-actions clearfix"> + {% include "admin/action_list_horizontal.html" %} + </div> +</div> +{% endwith %} diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/_user_details.html b/snf-admin-app/synnefo_admin/admin/templates/admin/_user_details.html new file mode 100644 index 0000000000000000000000000000000000000000..aec6d5e59b3bb3a3de7cfbe88ed4e9bf1ea3db81 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/_user_details.html @@ -0,0 +1,71 @@ +{% load admin_tags %} + +{% with user=item %} +<div class="object-anchor" id="{{ type }}-{{user|id}}"></div> +<div class="object-details {{ rowcls }}" data-id="{{user|id}}" + data-type="{{type}}"> + <h4 class="clearfix"> + {% include "admin/_details_h4_lt.html" %} + <span class="rt"> + {{ user|status_label|safe }} + <span class="label">UUID: {{ user.uuid }}</span> + <span class="label">{{ user.email }}</span> + </span> + </h4> + + <div class="object-details-content"> + <ul class="nav nav-tabs"> + <li class="active"><a href="#details{{ user.uuid }}" data-toggle="tab">Details</a></li> + <li><a href="#log{{ user.uuid }}" data-toggle="tab">Log</a></li> + <li><a href="#auth{{ user.uuid }}" data-toggle="tab">Auth Providers</a></li> + </ul> + <div class="tab-content"> + <div class="tab-pane active" id="details{{ user.uuid }}"> + <dl class="dl-horizontal well"> + <dt>ID</dt><dd>{{ user.id }}</dd> + <dt>UUID</dt><dd>{{ user.uuid }}</dd> + <dt>Name</dt><dd>{{ user.realname }}</dd> + <dt>Email</dt><dd>{{ user.email }}</dd> + <dt>Status</dt><dd>{{ user.status_display|upper }}</dd> + <dt>Groups</dt><dd>{{ user|get_groups }}</dd> + </dl> + </div> + <div class="tab-pane" id="log{{ user.uuid }}"> + <dl class="dl-horizontal well"> + {% if user.date_signed_terms %} + <dt>Signed terms at</dt><dd>{{ user.date_signed_terms }}</dd> + {% endif %} + {% if user.verified_at %} + <dt>Verified at</dt><dd>{{ user.verified_at }}</dd> + {% endif %} + {% if user.activation_sent %} + <dt>Activation sent at</dt><dd>{{ user.activation_sent }}</dd> + {% endif %} + {% if user.moderated_at %} + <dt>Moderated at</dt><dd>{{ user.moderated_at }}</dd> + <dt>Moderation policy</dt><dd>{{ user.accepted_policy }}</dd> + {% endif %} + {% if user.rejected_reason %} + <dt>Rejection reason</dt><dd>{{ user.rejected_reason }}</dd> + {% endif %} + {% if user.deactivated_at %} + <dt>Deactivated at</dt><dd>{{ user.deactivated_at }}</dd> + <dt>Deactivation reason</dt><dd>{{ user.deactivated_reason }}</dd> + {% endif %} + <dt>Last profile update at</dt><dd>{{ user.updated }}</dd> + <dt>Last logged-in at</dt><dd>{{ user.last_login }}</dd> + + </dl> + </div> + <div class="tab-pane" id="auth{{ user.uuid }}"> + <dl class="dl-horizontal well"> + <dt>Unused</dt><dd>{{ user|show_auth_providers:"available" }}</dd> + <dt>Enabled</dt><dd>{{ user|show_auth_providers:"enabled" }}</dd> + <dt>Disabled</dt><dd>{{ user|show_auth_providers:"disabled" }}</dd> + </dl> + </div> + </div> <!-- <div class="tab-content"> --> + </div> <!-- <div class="object-details-content"> --> + {% include "admin/action_list_horizontal.html" %} +</div> <!-- <div class="object-details {{ rowcls }}"> --> +{% endwith %} diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/_vm_details.html b/snf-admin-app/synnefo_admin/admin/templates/admin/_vm_details.html new file mode 100644 index 0000000000000000000000000000000000000000..80601730184b2a30c7b96d2f07ab3a9d2e2f62b5 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/_vm_details.html @@ -0,0 +1,130 @@ +{% load admin_tags %} + +{% with vm=item %} +<div class="object-anchor" id="vm-{{vm.pk}}"></div> +<div class="vm-details object-details {{ rowcls }}" data-id="{{vm.pk}}" + data-type="{{type}}"> + <h4 class="clearfix"> + {% include "admin/_details_h4_lt.html" %} + <span class="rt"> + {% if vm.suspended %} + <span class="label label-warning">SUSPENDED</span> + {% endif %} + {{ vm|status_label|safe }} + <span class="label">ID: {{ vm.pk }}</span> + <span class="label flavor"> + <span class="cpu">{{ vm.flavor.cpu }}x</span> + <span class="ram">{{ vm.flavor.ram}}MB</span> + <span class="disk">{{ vm.flavor.disk }}GB</span> + </span> + <em class="os-info"><img src="{{ UI_MEDIA_URL }}images/icons/os/{{ vm|get_os }}.png" alt="{{ vm|get_os }}" />{{ vm|get_os }}</em> + </span> +</h4> + <div class="vm-details-content object-details-content"> + + <ul class="nav nav-tabs"> + <li class="active"><a href="#details{{ vm.pk }}" data-toggle="tab">Details</a></li> + <li><a href="#metadata{{ vm.pk }}" data-toggle="tab">Metadata</a></li> + <li><a href="#image{{ vm.pk }}" data-toggle="tab">Image info</a></li> + <li><a href="#backend{{ vm.pk }}" data-toggle="tab">Backend info</a></li> + <li><a href="#network{{ vm.pk }}" data-toggle="tab">Network interfaces</a></li> + </ul> + <div class="tab-content"> + <div class="tab-pane active" id="details{{ vm.pk }}"> + <dl class="dl-horizontal well"> + <dt>ID</dt><dd>{{ vm.pk }}</dd> + <dt>Name</dt><dd>{{ vm.name }}</dd> + <dt>User id</dt><dd>{{ vm.userid }}</dd> + <dt>Created</dt><dd>{{ vm.created }} ({{ vm.created|timesince }} ago)</dd> + <dt>Updated</dt><dd>{{ vm.updated }} ({{ vm.updated|timesince }} ago)</dd> + <dt>Suspended</dt><dd>{{ vm.suspended }}</dd> + <dt>Deleted</dt><dd>{{ vm.deleted }}</dd> + <dt>Image id</dt><dd>{{ vm.imageid }}</dd> + <dt>Flavor</dt><dd>{{ vm|flavor_info }}</dl> + </dl> + </div> + <div class="tab-pane" id="metadata{{ vm.pk }}"> + <dl class="dl-horizontal well"> + {% for meta in vm.metadata.all %} + <dt>{{ meta.meta_key }}</dt><dd>{{ meta.meta_value }}</dd> + {% empty %} + <dt>No metadata</dt> + {% endfor %} + </dl> + </div> + <div class="tab-pane" id="image{{ vm.pk }}"> + <dl class="dl-horizontal well"> + {% with image=vm|image_info %} + {# Iterate all image info fields and create dt/dd pairs #} + {% for field, value in image.items %} + <dt>{{ field|title }}</dt><dd> + + {# The 'properties' field is a special case #} + {% if field == "properties" %} + {% for f, v in value.items %} + <b>{{ f|title }}:</b> {{ v }}{% if not forloop.last %}, {% endif %} + {% endfor %} + {% else %} + {{ value|default:"-" }} + {% endif %} + + </dd> + {% endfor %} + {% endwith %} + </dl> + </div> + <div class="tab-pane" id="backend{{ vm.pk }}"> + <dl class="dl-horizontal well"> + <dt>Action</dt><dd>{{ vm.get_action_display }} ({{ vm.action }})</dd> + <dt>Operstate</dt><dd>{{ vm.get_operstate_display }} ({{ vm.operstate }})</dd> + <dt>Backend job id</dt><dd>{{ vm.backendjobid }}</dd> + <dt>Backend op code</dt><dd>{{ vm.get_backendopcode_display }} ({{ vm.backendopcode }})</dd> + <dt>Backend log msg</dt><dd>{{ vm.backendlogmsg }}</dd> + <dt>Build backendjobstatus</dt><dd>{{ vm.backendjobstatus }}</dd> + <dt>Build percentage</dt><dd>{{ vm.buildpercentage }}</dd> + </dl> + <dl class="dl-horizontal well"> + {{ vm|backend_info|safe }} + </dl> + </div> + <div class="tab-pane" id="network{{ vm.pk }}"> + <table class="table well"> + <thead> + <tr> + <th>ID</th> + <th>Network (ID)</th> + <th>Created</th> + <th>Updated</th> + <th>Index</th> + <th>MAC</th> + <th>IPv4</th> + <th>IPv6</th> + <th>Firewall</th> + </tr> + </thead> + <tbody> + {% for nic in vm.nics.all %} + <tr> + <td>{{ nic.pk }}</td> + <td>{{ nic.network }} ({{ nic.network.pk }})</td> + <td>{{ nic.created }}</td> + <td>{{ nic.updated }}</td> + <td>{{ nic.index }}</td> + <td>{{ nic.mac }}</td> + <td>{{ nic.ipv4_address }}</td> + <td>{{ nic.ipv6_address }}</td> + <td>{{ nic.get_firewall_profile_display }} ({{nic.firewall_profile}})</td> + </tr> + {% empty %} + <tr> + <td colspan=9>No network interface available</td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + </div> + </div> + {% include "admin/action_list_horizontal.html" %} +</div> +{% endwith %} diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/_volume_details.html b/snf-admin-app/synnefo_admin/admin/templates/admin/_volume_details.html new file mode 100644 index 0000000000000000000000000000000000000000..08d3f664dfc1c98b576782b418ffc8e182a9487b --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/_volume_details.html @@ -0,0 +1,69 @@ +{% load admin_tags %} + +{% with volume=item %} +<div class="object-anchor" id="volume-{{volume.pk}}"></div> +<div class="object-details {{ rowcls }}" data-id="{{volume.pk}}" + data-type="{{type}}"> + <h4 class="clearfix"> + {% include "admin/_details_h4_lt.html" %} + <span class="rt"> + {{ volume|status_label|safe }} + <span class="label">ID: {{ volume.pk }}</span> + {% if volume.machine %} + <span class="label">VM ID: {{ volume.machine.pk }}</span> + {% endif %} + + <span class="label">{{ volume.size }} GB</span> + <span class="label">{{ volume.type|repr }}</span> + </span> + </h4> + <div class="tags"> + </div> + <div class="object-details-content"> + <ul class="nav nav-tabs"> + <li class="active"><a href="#details{{ type }}{{ volume.pk }}" data-toggle="tab">Details</a></li> + <li><a href="#type{{ type }}{{ volume.pk }}" data-toggle="tab">Type</a></li> + <li><a href="#source{{ type }}{{ volume.pk }}" data-toggle="tab">Source</a></li> + </ul> + <div class="tab-content"> + <div class="tab-pane active" id="details{{ type }}{{ volume.pk }}"> + <dl class="dl-horizontal well"> + <dt>ID</dt><dd>{{ volume.pk }}</dd> + <dt>Name</dt><dd>{{ volume.name|default:"-" }}</dd> + <dt>Description</dt><dd>{{ volume.description|default:"-" }}</dd> + <dt>Size</dt><dd>{{ volume.size }} GB</dd> + <dt>Index</dt><dd>{{ volume.index }}</dd> + <dt>Status</dt><dd>{{ volume.status }}</dd> + <dt>Deleted</dt><dd>{{ volume.deleted }}</dd> + <dt>Created</dt><dd>{{ volume.created }} ({{ volume.created|timesince }} ago)</dd> + <dt>Updated</dt><dd>{{ volume.updated }} ({{ volume.created|timesince }} ago)</dd> + <dt>Snapshot counter</dt><dd>{{ volume.snapshot_counter }}</dd> + <dt>User</dt><dd>{{ volume|details_url:"user"|safe }}</dd> + <dt>Project</dt><dd>{{ volume|details_url:"project"|safe }}</dd> + {% if volume.machine %} + <dt>VM</dt><dd>{{ volume|details_url:"vm"|safe }}</dd> + {% endif %} + + </dl> + </div> + <div class="tab-pane" id="type{{ type }}{{ volume.pk }}"> + <dl class="dl-horizontal well"> + <dt>Type name</dt><dd>{{ volume.volume_type.name|default:"-" }}</dd> + <dt>Disk template</dt><dd>{{ volume.volume_type.disk_template|default:"-" }}</dd> + <dt>Provider</dt><dd>{{ volume.volume_type.provider|default:"-" }}</dd> + <dt>Deleted</dt><dd>{{ volume.volume_type.deleted }}</dd> + </dl> + </div> + <div class="tab-pane" id="source{{ type }}{{ volume.pk }}"> + <dl class="dl-horizontal well"> + <dt>Source image</dt><dd>{{ volume.source }}</dd> + <dt>Source version</dt><dd>{{ volume.source_version|default:"-" }}</dd> + <dt>Origin</dt><dd>{{ volume.origin }}</dd> + </dl> + </div> + </div> <!-- <div class="tab-content"> --> + </div> <!-- <div class="object-details-content"> --> + {% include "admin/action_list_horizontal.html" %} +</div> <!-- <div class="object-details {{ rowcls }}"> --> +{% endwith %} + diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/action_list_horizontal.html b/snf-admin-app/synnefo_admin/admin/templates/admin/action_list_horizontal.html new file mode 100644 index 0000000000000000000000000000000000000000..ac63e1f7b03a6e02527b7abedb8dbaa30d030496 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/action_list_horizontal.html @@ -0,0 +1,10 @@ +{% load admin_tags %} +<div class="actions-per-item actions"> + {% for op, action in action_dict.items %} + {% if action|can_apply:item %} + <a href="" data-target="#{{action.target}}-{{ op }}" data-action="{{ op }}" data-toggle="modal" data-karma="{{ action.karma }}" data-caution="{{ action.caution_level }}" class="custom-btn"> + <span>{{ action.name }}</span> + </a> + {% endif %} + {% endfor %} +</div> diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/action_list_vertical.html b/snf-admin-app/synnefo_admin/admin/templates/admin/action_list_vertical.html new file mode 100644 index 0000000000000000000000000000000000000000..a5b27ec24eaab82baaef97825d9f54eaa784086b --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/action_list_vertical.html @@ -0,0 +1,14 @@ +{% if action_dict %} +<div class="sidebar actionbar"> + <div id="sticker"> + <div class="btn-group-vertical"> + {% for op, action in action_dict.items %} + <a href="" data-target="#{{action.target}}-{{ op }}" data-action="{{ op }}" data-toggle="modal" data-karma={{ action.karma }} data-caution="{{ action.caution_level }}" class="disabled custom-btn"> + <span>{{ action.name }}</span> + </a> + {% endfor %} + </div> + </div> + +</div> +{% endif %} diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/action_modal_list.html b/snf-admin-app/synnefo_admin/admin/templates/admin/action_modal_list.html new file mode 100644 index 0000000000000000000000000000000000000000..5e87b8fd4f8cf090e9e8f183041ce9cc3c033550 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/action_modal_list.html @@ -0,0 +1,17 @@ +{% for op, action in action_dict.items %} + {% include "admin/_action_modal.html" %} +{% endfor %} + +{% if view_type == "list" %} + + {% include "admin/massive_modal.html" %} + +{% elif view_type == "details" %} + + {% for association in associations_list %} + {% for op, action in association.actions.items %} + {% include "admin/_action_modal.html" %} + {% endfor %} + {% endfor %} + +{% endif %} diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/admin_404.html b/snf-admin-app/synnefo_admin/admin/templates/admin/admin_404.html new file mode 100644 index 0000000000000000000000000000000000000000..b5ba0eac995aec5b2157168370f5171f173dc4f9 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/admin_404.html @@ -0,0 +1,6 @@ +{% extends "admin/base.html" %} + +{% block content %} +<h1>4 Oh 4!</h1> +<h3>{{ msg }}</h3> +{% endblock %} diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/admin_405.html b/snf-admin-app/synnefo_admin/admin/templates/admin/admin_405.html new file mode 100644 index 0000000000000000000000000000000000000000..3c51063d655657403506fe98536bada931876fc7 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/admin_405.html @@ -0,0 +1,6 @@ +{% extends "admin/base.html" %} + +{% block content %} +<h1>4 Oh 5!</h1> +<h3>{{ msg }}</h3> +{% endblock %} diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/auth_provider_list.html b/snf-admin-app/synnefo_admin/admin/templates/admin/auth_provider_list.html new file mode 100644 index 0000000000000000000000000000000000000000..2ef0e979e2216988aa25e70f8b986eb1a2362c83 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/auth_provider_list.html @@ -0,0 +1 @@ +{% extends "admin/base_list.html" %} diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/base.html b/snf-admin-app/synnefo_admin/admin/templates/admin/base.html new file mode 100644 index 0000000000000000000000000000000000000000..89c36cc738e73350f5a494b14159c0d4693313cb --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/base.html @@ -0,0 +1,125 @@ +{% load admin_tags %} +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <link rel="shortcut icon" href="{{ BRANDING_FAVICON_URL }}" /> + <title>{{BRANDING_SERVICE_NAME}} admin</title> + <link href="{{ ADMIN_MEDIA_URL }}{% min_prefix %}css/icon-fonts.css" rel="stylesheet"> + <!-- There is no minified version of jquery.dataTables.css in DataTables 1.10.0 --> + <link href="{{ ADMIN_MEDIA_URL }}css/jquery.dataTables.css" rel="stylesheet"> + + {% if request.COOKIES.theme == 'dark' %} + <link href="{{ ADMIN_MEDIA_URL }}{% min_prefix %}css/main.css" rel="stylesheet"> + {% else %} + <link href="{{ ADMIN_MEDIA_URL }}{% min_prefix %}css/main-light.css" rel="stylesheet"> + {% endif %} + <!-- + <link href="{{ ADMIN_MEDIA_URL }}css/ie7.css" rel="stylesheet"> --> + <link href='https://fonts.googleapis.com/css?family=Open+Sans:400italic,600italic,700italic,400,700,300,600' rel='stylesheet' type='text/css'> + </head> + + {% block custom-css %} + {% endblock %} + + + <body> + <div class="wrapper"> + {% block nav-bar %} + <div class="navbar navbar-default navbar-fixed-top"> + <div class="container-fluid"> + <!-- Brand and toggle get grouped for better mobile display --> + <div class="navbar-header"> + <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#navbar-collapse-1"> + <span class="sr-only">Toggle navigation</span> + <span class="icon-bar"></span> + <span class="icon-bar"></span> + <span class="icon-bar"></span> + </button> + <a href="{% url admin-home %}" title="Homepage" class="navbar-brand home-icon"><img src="{{ BRANDING_IMAGE_MEDIA_URL }}cloudbar_home.png" alt="S" /></a> + </div> + + <div class="collapse navbar-collapse" id="navbar-collapse-1"> + <ul class="nav navbar-nav navbar-left"> + {% for view, view_dict in views.items %} + <li + {% if item_type == view or main_type == view %}class="active"{% endif %} + ><a href="{% url admin-list view %}">{{ view_dict.label }}</a></li> + {% endfor %} + <li {% block nav-reports %}class="has-dropdown"{% endblock %}> + <a href="#">Reports + <span class="snf-angle-down arrow"></span></a> + <ul class="dropdown-menu align-left"> + <li {% block nav-stats %}{% endblock %}><a href="{% url admin-stats %}">Stats</a></li> + <li {% block nav-charts %}{% endblock %}><a href="{% url admin-charts %}">Charts</a></li> + </ul> + </li> + </ul> + + <ul class="nav navbar-nav navbar-right"> + <li class="has-dropdown"> + <a href="" data-noclick="true"> + {{ user.access.user.name }} + <span class="snf-angle-down arrow"></span> + </a> + <ul class="dropdown-menu align-right"> + <li> + <a href="{% url admin-logout %}" class="sign-out"><span class="snf-sign-out"></span>Sign Out</a> + </li> + </ul> + </li> + </ul> + </div> <!-- /collapse --> + </div> <!-- /container-fluid --> + + </div> <!-- /nav-main --> + {% block subnav-bar %} + {% endblock subnav-bar %} + + + {% endblock nav-bar %} + + {% if messages %} + <div class="messages"> + {% for message in messages %} + <div class="alert {% if message.tags %}alert-{{ message.tags }}{% endif %}"> + <button class="close" data-dismiss="alert">×</button> + {{ message }} + </div> + {% endfor %} + </div> + {% endif %} + + {% block main-area %} + <div class="container container-solid"> + {% block content %} + {% endblock %} + </div> + {% endblock main-area %} + + <div class="themes"> + <a href="" id="toggle-theme" class="line-btn" title="Change theme. This will refresh the page."><span class="snf-moon-1"></span></a> + </div><!-- /themes --> + + + <div class="notify"> + <a href="" class="close-notify close-notifications" title="Close notification area"><span class="snf-remove"></span></a> + <div class="container"> + <p class="no-notifications">There are no notifications.</p> + </div> + </div><!-- /notify --> + + </div> + <script src="{{ MEDIA_URL }}admin/js/jquery.js"></script> + <script src="{{ MEDIA_URL }}admin/js/jquery.cookie.js"></script> + <script src="{{ MEDIA_URL }}admin/js/bootstrap.js"></script> + <script src="{{ MEDIA_URL }}admin/js/underscore.js"></script> + + <script src="{{ MEDIA_URL }}admin/js/common.js"></script> + + {% block custom-javascript %} + {% endblock %} + + + </body> +</html> diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/base_details.html b/snf-admin-app/synnefo_admin/admin/templates/admin/base_details.html new file mode 100644 index 0000000000000000000000000000000000000000..47aedd21100c5e6f12ed2948d9c8be9f0227068f --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/base_details.html @@ -0,0 +1,99 @@ +{% extends "admin/base.html" %} +{% load admin_tags %} + +{% block subnav-bar %} + {% include "admin/subnav.html" %} +{% endblock subnav-bar %} + +{% block main-area%} + +<div class="container part-one"> + + {% block main-item-details %} + + {# Start of the decoration of main item #} + {% block main-item-head %} + {% include "admin/_base_main_item_head.html" %} + {% endblock %} + + {# This is the section for the item for which we hare requested details #} + {% block main-item %} + {% include main_type|get_details_template with item=main_item type=main_type %} + {% endblock %} + + {# End of the decoration of main item #} + {% block main-item-tail %} + {% include "admin/_base_main_item_tail.html" %} + {% endblock %} + + {% endblock %} + +</div> + +<div class="parts-separator"> + <h2> + <div class="container"> + Associations for <em title="{{ main_item|repr }}" data-toggle="tooltip" data-placement="bottom">'{{ main_item|repr }}'</em> : + </div> + </h2> +</div> + +<div class="container part-two"> + + {# This is a list of associations for this item #} + {% block item-associations-list %} + {% for association in associations_list %} + {% with type=association.type %} + + {# Seperator for the start of the list #} + {% block item-list-head %} + {% include "admin/_base_list_head.html" %} + {% endblock item-list-head %} + + {# This is an item list for each association #} + {% block item-list %} + {% for item in association.items %} + + <!-- {% cycle 'row1' 'row2' as rowcls %} --> + {# Details for an item in the list #} + {% block item-details %} + {% include type|get_details_template with action_dict=association.actions%} + {% endblock item-details %} + {% empty %} + <p>No items in the list</p> + {% endfor %} + {% endblock item-list %} + + {# Seperator for the end of the list #} + {% block item-list-tail %} + {% include "admin/_base_list_tail.html" %} + {% endblock item-list-tail %} + + {% endwith %} + {% empty %} + <p>No associations for this item</p> + {% endfor %} + {% endblock item-associations-list %} + <div class="custom-buttons bottom"> + <a href="" id="toggle-notifications" title="Open/Close notifications" class="select line-btn" data-karma="neutral" data-caution="none"> + <span class="snf-bell-1"></span> + </a> + <a href="" class="line-btn shortcuts-btn"> + <span data-container="body" data-toggle="popover" data-html="true" data-placement="right" data-content='{% include "admin/tips.html" %}'> + <i class="snf-book-2 book-icon"></i> + Tips and Tricks + </span> + </a> + </div> + + <!-- Modals --> + {% block action-modals %} + {% include "admin/action_modal_list.html" %} + {% endblock action-modals %} +</div> +{% endblock main-area%} + +{% block custom-javascript %} +<script src="{{ MEDIA_URL }}admin/js/details.js"></script> +{% endblock %} + diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/base_list.html b/snf-admin-app/synnefo_admin/admin/templates/admin/base_list.html new file mode 100644 index 0000000000000000000000000000000000000000..f12a1e4adb132fbada8674cf5a11371e33fcda46 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/base_list.html @@ -0,0 +1,31 @@ +{% extends "admin/base.html" %} + +{% block content %} + + {# Show the filter list only in views that have filters #} + {% if filter_dict %} + {% block filter-list %} + {% include "admin/filter_list.html" %} + {% endblock filter-list %} + {% endif %} + + {% block action-list %} + {% include "admin/action_list_vertical.html" %} + {% endblock action-list %} + + {% block table-area %} + {% include "admin/table.html" %} + {% endblock %} + + <!-- Modals --> + {% block action-modals %} + {% include "admin/action_modal_list.html" %} + {% endblock action-modals %} + +{% endblock %} + +{% block custom-javascript %} +<script src="{{ MEDIA_URL }}admin/js/jquery.dataTables.js"></script> +<script src="{{ MEDIA_URL }}admin/js/tables.js"></script> +<script src="{{ MEDIA_URL }}admin/js/filters.js"></script> +{% endblock %} diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/charts.html b/snf-admin-app/synnefo_admin/admin/templates/admin/charts.html new file mode 100644 index 0000000000000000000000000000000000000000..ace43db9c07955f2ac4d202a03042a5ac17c1969 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/charts.html @@ -0,0 +1,79 @@ +{% extends "admin/base.html" %} + +{% block custom-css %} +<link href="{{ ADMIN_MEDIA_URL }}css/c3.css" rel="stylesheet" type="text/css"> +{% endblock %} + +{% block nav-reports %} +class="has-dropdown active" +{% endblock %} + +{% block nav-charts %} +class="active" +{% endblock %} + +{% block content %} +<div class="charts" id="charts" + data-astakos-stats={% url admin-stats-component-details 'astakos' %} + data-cyclades-stats={% url admin-stats-component-details 'cyclades' %}> +<div class="sidebar actionbar"> + <div id="sticker"> + <a href="" data-chart="resource-usage" class="active"><span>Resource Usage</span></a> + <a href="" data-chart="temp"><span>User status and providers</span></a> + <a href="" data-chart="vm-status"><span>VM status</span></a> + <a href="" data-chart="ip-status"><span>IP status</span></a> + <a href="" data-chart="disk-templates"><span>Disk templates</span></a> + <a href="" data-chart="images"><span>Images</span></a> + </div> + +</div> +<div class="info well"> + <div id="infra-usage-wrap" class="chart" data-chart="resource-usage"> + <h3>Infra usage</h3> + <div id="infra-usage"></div> + </div> + <div id="resource-usage" class="chart" data-chart="resource-usage"></div> + <div id="provider-status-wrap" class="chart" data-chart="temp"> + <h3>Providers per user status<h3> + <div id="provider-status"></div> + </div> + <div id="provider-status-reversed-wrap" class="chart" data-chart="temp"> + <h3>Status per user provider</h3> + <div id="provider-status-reversed"></div> + </div> + <div id="provider-exclusiveness-wrap" class="chart" data-chart="temp"> + <h3>Provider exclusiveness</h3> + <div id="provider-exclusiveness"></div> + </div> + <div id="server-status-wrap" class="chart" data-chart="vm-status"> + <h3>Server status</h3> + <div id="server-status"></div> + </div> + <div id="ip-pool-status-wrap" class="chart" data-chart="ip-status"> + <h3>IP pool status</h3> + <div id="ip-pool-status"></div> + </div> + <div id="disk-templates-wrap" class="chart" data-chart="disk-templates"> + <h3>Disk templates</h3> + <div id="disk-templates"></div> + </div> + <div id="images-wrap" class="chart" data-chart="images"> + <h3>Images</h3> + <div id="images"></div> + </div> +</div> +</div> +{% endblock %} + +{% block custom-javascript %} +<!-- Load d3.js and c3.js --> +<script src="{{ ADMIN_MEDIA_URL }}js/d3.v3.min.js"></script> +<!--Switch to c3.min at some point--> +<script src="{{ ADMIN_MEDIA_URL }}js/c3.js"></script> + +<script src="{{ ADMIN_MEDIA_URL }}js/charts.js"></script> +<script src="{{ ADMIN_MEDIA_URL }}js/charts_common.js"></script> +{% endblock %} + + + diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/filter_bool.html b/snf-admin-app/synnefo_admin/admin/templates/admin/filter_bool.html new file mode 100644 index 0000000000000000000000000000000000000000..3fed507e4212be836c328192483917be72152955 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/filter_bool.html @@ -0,0 +1,15 @@ +<div class="filter filter-dropdown filter-boolean" data-filter="{{ filter.name }}"> + <div class="dropdown"> + <a class="" href="#" data-toggle="dropdown"> + <span class="category">{{ filter.label }}:</span> + <span class="selected-value">-</span> + <span class="snf-angle-down arrow"></span> + </a> + <ul class="dropdown-menu choices"> + <li class="active reset"><a href="#"><span class="snf-radio-unchecked"></span><span class="snf-radio-checked"></span>-</a></li> + <li class="divider"></li> + <li><a href="#"><span class="snf-radio-unchecked"></span><span class="snf-radio-checked"></span>True</a></li> + <li><a href="#"><span class="snf-radio-unchecked"></span><span class="snf-radio-checked"></span>False</a></li> + </ul> + </div> +</div> diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/filter_char.html b/snf-admin-app/synnefo_admin/admin/templates/admin/filter_char.html new file mode 100644 index 0000000000000000000000000000000000000000..ead660861217faaeb9a949cf294c34fd95376ae9 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/filter_char.html @@ -0,0 +1,11 @@ +{% load admin_tags %} +<div class="filter filter-text filter-input" data-filter="{{ filter.name }}"> + <div class="form-group"> + <label title="{{ filter.label }}"> + {% with label=filter.name|label_to_icon:filter.label %} + {{ label|safe }} + {% endwith %} + <input type="search" data-filter="{{ filter.name }}"> + </label> + </div> +</div> diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/filter_choice.html b/snf-admin-app/synnefo_admin/admin/templates/admin/filter_choice.html new file mode 100644 index 0000000000000000000000000000000000000000..62573c6d7ec901863b48ec8b50bb38472d6e7507 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/filter_choice.html @@ -0,0 +1,16 @@ +<div class=" filter filter-dropdown"> + <div class="dropdown" data-filter="{{ filter.name }}"> + <a class="" href="#" data-toggle="dropdown"> + <span class="category">{{ filter.label }}:</span> + <span class="selected-value">All</span> + <span class="snf-angle-down arrow"></span> + </a> + <ul class="dropdown-menu choices"> + <li class="active reset"><a href="#"><span class="selection-indicator snf-checkbox-checked">All</a></li> + <li class="divider"></li> + {% for choice,_ in filter.field.choices %} + <li><a href="#"><span class="selection-indicator snf-checkbox-unchecked"></span>{{ choice }}</a></li> + {% endfor %} + </ul> + </div> +</div> diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/filter_compact.html b/snf-admin-app/synnefo_admin/admin/templates/admin/filter_compact.html new file mode 100644 index 0000000000000000000000000000000000000000..fb0fe7194b0fb28b2b6a1c5aab9a40a5a659d252 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/filter_compact.html @@ -0,0 +1,14 @@ +{% load admin_tags %} +<div class="filter compact-filter input-with-btn"> + <div class="form-group"> + <label title="Compact View"> + <input type="search" data-filter="compact"> + </label> + </div> + <a class="exec-search search-btn"><span>Search</span></a> + <div class="filter-error"> + <span class="error-sign snf-exclamation-sign"></span> + <span class="error-description"></span> + </div> + {% include "admin/filter_compact_instructions.html" %} +</div> diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/filter_compact_instructions.html b/snf-admin-app/synnefo_admin/admin/templates/admin/filter_compact_instructions.html new file mode 100644 index 0000000000000000000000000000000000000000..826a95e480114b03e151507306d0cd78cd0908a3 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/filter_compact_instructions.html @@ -0,0 +1,28 @@ +{% load admin_tags %} +<div class="instructions"> + <a href="" class="toggle-instructions line-btn"> + <span>Instructions</span> + <span class="snf-angle-down arrow"></span> + <span class="snf-angle-up arrow"></span> + </a> + <div class="content content-area"> + The list below presents the identifiers that correspond to the filters + of <b>Standard View</b>, as well as their appropriate values: + <dl class="dl-horizontal"> + {% for filter in filter_dict %} + <dt>{{ filter.name }}</dt> + <dd> + {% if filter|get_filter_type == 'choice' or filter|get_filter_type == 'multichoice' %} + {{ filter|default_value }}, {{ filter|choices|join:", "|title }} + {% elif filter|get_filter_type == 'bool' %} + -, True, False + {% else %} + Free text + {% endif %} + </dd> + {% endfor %} + </dl> + </br> + <p class="clarifications">For detailed instructions please consult "Tips and Tricks".</p> + </div> +</div> diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/filter_list.html b/snf-admin-app/synnefo_admin/admin/templates/admin/filter_list.html new file mode 100644 index 0000000000000000000000000000000000000000..7e88ae0cf522cd6fb6481a16e165b3b1e3be5838 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/filter_list.html @@ -0,0 +1,19 @@ +{% load admin_tags %} +<div class="filters filters-area"> + <a class="onoffswitch search-mode"> + <input type="checkbox" name="onoffswitch" class="onoffswitch-checkbox" id="myonoffswitch" checked> + <label class="onoffswitch-label" for="myonoffswitch"> + <span class="onoffswitch-inner"></span> + <span class="onoffswitch-switch"></span> + </label> +</a> + <!-- <a class="search-mode search-mode-btn">Compact View</a> --> + {% include "admin/filter_select_list.html" %} + <!-- use it to center it (sort of) --> + {% for filter in filter_dict %} + {% include filter|get_filter_template %} + {% endfor %} + + {% include "admin/filter_compact.html" %} +</div> + diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/filter_multichoice.html b/snf-admin-app/synnefo_admin/admin/templates/admin/filter_multichoice.html new file mode 100644 index 0000000000000000000000000000000000000000..8bac531e1af2772d19032bc0353f2e4f4d97514f --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/filter_multichoice.html @@ -0,0 +1,17 @@ +{% load admin_tags %} +<div class="filter filter-dropdown" data-filter="{{ filter.name }}"> + <div class="dropdown"> + <a class="" href="#" data-toggle="dropdown"> + <span class="category">{{ filter.label }}:</span> + <span class="selected-value">{{ filter|default_value }}</span> + <span class="snf-angle-down arrow"></span> + </a> + <ul class="dropdown-menu choices"> + <li class="active reset"><a href="#"><span class="snf-checkbox-unchecked"></span><span class="snf-checkbox-checked"></span>{{ filter|default_value }}</a></li> + <li class="divider"></li> + {% for choice,_ in filter.field.choices %} + <li><a href="#"><span class="snf-checkbox-unchecked"></span><span class="snf-checkbox-checked"></span>{{ choice }}</a></li> + {% endfor %} + </ul> + </div> +</div> diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/filter_number.html b/snf-admin-app/synnefo_admin/admin/templates/admin/filter_number.html new file mode 100644 index 0000000000000000000000000000000000000000..3d901dc72b3d605ed3a380b9a704f97c9586de98 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/filter_number.html @@ -0,0 +1,7 @@ +<div class="filter filter-text filter-input"> + <div class="form-group"> + <label>{{ filter.label }}: + <input type="search" data-filter="{{ filter.name }}"> + </label> + </div> +</div> \ No newline at end of file diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/filter_select_list.html b/snf-admin-app/synnefo_admin/admin/templates/admin/filter_select_list.html new file mode 100644 index 0000000000000000000000000000000000000000..dde4c2b3059596d8037ea5c716b919809b599c5a --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/filter_select_list.html @@ -0,0 +1,16 @@ +{% load admin_tags %} +<div class="filter filters-list visible-filter"> + <a class="" id="select-filters" href="#" link-title="Available Filters" popover-content= + '<ul class="choices dropdown-list"> + <li class="reset" data-filter-name="all-filters"><a href="#"><span class="snf-checkbox-unchecked"></span><span class="snf-checkbox-checked"></span>All</a></li> + <li class="divider"></li> + {% for filter in filter_dict %} + <li data-filter-name="{{ filter.name }}"><a href="#"><span class="snf-checkbox-unchecked"></span><span class="snf-checkbox-checked"></span>{{ filter.label }}</a></li> + {% endfor %} + </ul>'> + <span class="snf-filter"></span> + </a> +</div> + + + \ No newline at end of file diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/group_list.html b/snf-admin-app/synnefo_admin/admin/templates/admin/group_list.html new file mode 100644 index 0000000000000000000000000000000000000000..2ef0e979e2216988aa25e70f8b986eb1a2362c83 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/group_list.html @@ -0,0 +1 @@ +{% extends "admin/base_list.html" %} diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/home.html b/snf-admin-app/synnefo_admin/admin/templates/admin/home.html new file mode 100644 index 0000000000000000000000000000000000000000..d184ffb444cb2fe5699dac8755fd9fe04d4246e0 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/home.html @@ -0,0 +1,36 @@ +{% extends "admin/base.html" %} + +{% block nav-bar %} +<div class="nav-simple navbar-fixed-top"> + <span class="header"> + {% if BRANDING_ADMIN_LOGO_URL %} + <img src="{{ BRANDING_ADMIN_LOGO_URL }}" alt="{{ BRANDING_SERVICE_NAME }} Admin Panel" /> + {% else %} + Synnefo Admin Panel + {% endif %} + </span> + <div class="login-info">Welcome, + <div class="has-dropdown" > + <a href="" data-noclick="true">{{ user.access.user.name }} + <span class="snf-angle-down arrow"></span> + </a> + <ul class="dropdown-menu align-right"> + <li> + <a href="{% url admin-logout %}" class="sign-out"> + <span class="snf-sign-out"></span> + Sign Out + </a> + </li> + </ul> + </div> + </div> +</div> +{% endblock %} + +{% block main-area %} +<div class="app-list"> + <a href="" data-toggle="tooltip" data-placement="bottom" title="Under construction" class="disabled"><span>Infrastructure</span></a> + <a href="{% url admin-default %}"><span>Service</span></a> +</div> + +{% endblock main-area%} diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/ip_details.html b/snf-admin-app/synnefo_admin/admin/templates/admin/ip_details.html new file mode 100644 index 0000000000000000000000000000000000000000..8f9e1b3694587404ce97c79668bbb406170628eb --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/ip_details.html @@ -0,0 +1 @@ +{% extends "admin/base_details.html" %} diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/ip_list.html b/snf-admin-app/synnefo_admin/admin/templates/admin/ip_list.html new file mode 100644 index 0000000000000000000000000000000000000000..2ef0e979e2216988aa25e70f8b986eb1a2362c83 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/ip_list.html @@ -0,0 +1 @@ +{% extends "admin/base_list.html" %} diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/ip_log_list.html b/snf-admin-app/synnefo_admin/admin/templates/admin/ip_log_list.html new file mode 100644 index 0000000000000000000000000000000000000000..2ef0e979e2216988aa25e70f8b986eb1a2362c83 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/ip_log_list.html @@ -0,0 +1 @@ +{% extends "admin/base_list.html" %} diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/massive_modal.html b/snf-admin-app/synnefo_admin/admin/templates/admin/massive_modal.html new file mode 100644 index 0000000000000000000000000000000000000000..7af77793ed8a37d226f6e2d3b08b1fe3cda7112b --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/massive_modal.html @@ -0,0 +1,40 @@ +<div class="modal fade" id="massive-actions-warning" data-backdrop="static" data-keyboard="false" data-karma="dark"> + <div class="modal-dialog modal-md"> + <div class="modal-content area"> + <div class="modal-header"> + <a class="close" data-dismiss="modal">×</a> + <h3 class="elem">Select All Items</h3> + </div> + <div class="modal-body"> + <!-- <div class="progress-area"><img src="{{ ADMIN_MEDIA_URL }}/img/ajax-loader.gif"></div> --> + <p>You have chosen to select all items that meet the criteria of the filters.</p> + <p>This might take some time.</p> + <p class="progress-area">Please wait...</p> + </div> + <div class="modal-footer"> + <a href="#" class="custom-btn cancel" data-dismiss="modal"><span>Cancel</span></a> + <a href="#" class="custom-btn select-all-confirm" data-karma="dark"><span>Select all</span></a> + </div> + </div> + </div> +</div> + +<div class="modal fade" id="clear-all-warning" data-backdrop="static" data-keyboard="false" data-karma="dark"> + <div class="modal-dialog modal-md"> + <div class="modal-content area"> + <div class="modal-header"> + <a class="close" data-dismiss="modal">×</a> + <h3 class="elem">Clear All Items</h3> + </div> + <div class="modal-body"> + <!-- <div class="progress-area"><img src="{{ ADMIN_MEDIA_URL }}/img/ajax-loader.gif"></div> --> + <p>You have chosen to clear all your selections.</p> + <p class="progress-area">Please wait...</p> + </div> + <div class="modal-footer"> + <a href="#" class="custom-btn cancel" data-dismiss="modal"><span>Cancel</span></a> + <a href="#" class="custom-btn clear-all-confirm" data-dismiss="modal" data-karma="dark"><span>Clear all</span></a> + </div> + </div> + </div> +</div> diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/network_details.html b/snf-admin-app/synnefo_admin/admin/templates/admin/network_details.html new file mode 100644 index 0000000000000000000000000000000000000000..8f9e1b3694587404ce97c79668bbb406170628eb --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/network_details.html @@ -0,0 +1 @@ +{% extends "admin/base_details.html" %} diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/network_list.html b/snf-admin-app/synnefo_admin/admin/templates/admin/network_list.html new file mode 100644 index 0000000000000000000000000000000000000000..2ef0e979e2216988aa25e70f8b986eb1a2362c83 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/network_list.html @@ -0,0 +1 @@ +{% extends "admin/base_list.html" %} diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/project_details.html b/snf-admin-app/synnefo_admin/admin/templates/admin/project_details.html new file mode 100644 index 0000000000000000000000000000000000000000..8f9e1b3694587404ce97c79668bbb406170628eb --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/project_details.html @@ -0,0 +1 @@ +{% extends "admin/base_details.html" %} diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/project_list.html b/snf-admin-app/synnefo_admin/admin/templates/admin/project_list.html new file mode 100644 index 0000000000000000000000000000000000000000..2ef0e979e2216988aa25e70f8b986eb1a2362c83 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/project_list.html @@ -0,0 +1 @@ +{% extends "admin/base_list.html" %} diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/stats.html b/snf-admin-app/synnefo_admin/admin/templates/admin/stats.html new file mode 100644 index 0000000000000000000000000000000000000000..baa46fc3b7e27bc1570b075f6ee6d76acb19d73a --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/stats.html @@ -0,0 +1,56 @@ +{% extends "admin/base.html" %} + +{% block nav-reports %} +class="has-dropdown active" +{% endblock %} + +{% block nav-stats %} +class="active" +{% endblock %} + +{% block content %} +<div class="container stats"> +<section class="clearfix"> + <h3><span class="snf-user-full"></span>Astakos</h3> + <a href="{% url admin-stats-component 'astakos' %}" class="custom-btn" download="astakos-terse" type="application/json" title="Download terse Astakos statistics in JSON format" data-toggle="tooltip" data-placement="bottom"> + <span class="snf-download-full"></span> + <span>Terse stats</span> + </a> + <a href="{% url admin-stats-component-details 'astakos' %}" class="custom-btn" download="astakos-details" type="application/json" title="Download detailed Astakos statistics in JSON format" data-toggle="tooltip" data-placement="bottom"> + <span class="snf-download-full"></span> + <span>Detailed stats</span> + </a> + <div class="spinner"> + <div class="bounce1"></div> + <div class="bounce2"></div> + <div class="bounce3"></div> + <em>Retrieving data</em> + </div> + </section> + <section class="clearfix"> + <h3><span class="snf-pc-full"></span>Cyclades</h3> + <a href="{% url admin-stats-component 'cyclades' %}" class="custom-btn" download="cyclades-terse" type="application/json" title="Download terse Cyclades statistics in JSON format" data-toggle="tooltip" data-placement="bottom"> + <span class="snf-download-full"></span> + <span>Terse stats</span> + </a> + <a href="{% url admin-stats-component-details 'cyclades' %}" class="custom-btn" download="cyclades-details" type="application/json" title="Download detailed Cyclades statistics in JSON format" data-toggle="tooltip" data-placement="bottom"> + <span class="snf-download-full"></span> + <span>Detailed stats</span> + </a> + <div class="spinner"> + <div class="bounce1"></div> + <div class="bounce2"></div> + <div class="bounce3"></div> + <em>Retrieving data</em> + </div> + + </section> +</div> +{% endblock %} + +{% block custom-javascript %} +<script src="{{ MEDIA_URL }}admin/js/stats.js"></script> +{% endblock %} + + + diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/subnav.html b/snf-admin-app/synnefo_admin/admin/templates/admin/subnav.html new file mode 100644 index 0000000000000000000000000000000000000000..98106214c5e071d51c27ebe7e2208d8bd9234d72 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/subnav.html @@ -0,0 +1,29 @@ +{% load admin_tags %} +<div class="navbar sub-nav navbar-inverse navbar-fixed-top"> + <ul class="nav navbar-nav navbar-left"> + <!-- For main item --> + <li><a href="#{{ main_type }}-{{ main_item|id }}" class="link-to-anchor"> + {{ main_type|title }}: <i>{{ main_item|repr }}</i></a> + </li> + {% for association in associations_list %} + {% if association.items %} + <li class="has-dropdown"> + <a href="#"> + {{ association.type|display_list_type }} + <span class="snf-angle-down arrow"></span> + </a> + <ul class="dropdown-menu"> + <li><a href="#{{ association.type }}-area" class="link-to-anchor">All</a></li> + <li class="divider"></li> + + {% if association.type != 'ip_log' %} + {% for item in association.items %} + <li><a href="#{{ association.type }}-{{ item|id }}" class="link-to-anchor">{{ item|repr }}</a></li> + {% endfor %} + {% endif %} + </ul> + </li> + {% endif %} + {% endfor %} + </ul> +</div> <!-- /sub-nav --> diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/table.html b/snf-admin-app/synnefo_admin/admin/templates/admin/table.html new file mode 100644 index 0000000000000000000000000000000000000000..745834c072f9e40407c268e58c08620aabf2af4e --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/table.html @@ -0,0 +1,37 @@ +<div class="info-block well info"> + <table class="table-selected-main" id="table-items-selected"> + <thead> + <tr> + {% for column in columns %} + <th>{{ column }}</th> + {% endfor %} + </tr> + </thead> + </table> + + <table class="table-items" id="table-items-total" + data-url="{% url admin-json item_type %}" data-server-side="true" + data-content="{{ item_type }}"> + <thead> + <tr> + {% for column in columns %} + <th>{{ column }}</th> + {% endfor %} + </tr> + </thead> + </table> + {% block table-footer %} + {% include "admin/table_footer.html" %} + {% endblock %} +</div> +<div class="hidden massive-pull"> + <table class="table total-list" id="total-list"> + <thead> + <tr> + {% for column in columns %} + <th>{{ column }}</th> + {% endfor %} + </tr> + </thead> + </table> +</div> diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/table_footer.html b/snf-admin-app/synnefo_admin/admin/templates/admin/table_footer.html new file mode 100644 index 0000000000000000000000000000000000000000..25bb88b858853050d932411437803a951343f392 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/table_footer.html @@ -0,0 +1,12 @@ +<div class="custom-buttons bottom"> + <a href="" id="toggle-notifications" title="Open/Close notifications" class="select line-btn" data-karma="neutral" data-caution="none"> + <span class="snf-bell-1"></span> + </a> + <a href="" class="line-btn shortcuts-btn"> + <span data-container="body" data-toggle="popover" data-html="true" data-placement="right" data-content='{% include "admin/tips.html" %}'> + <i class="snf-book-2 book-icon"></i> + Tips and Tricks + </span> + </a> +</div> + diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/tips.html b/snf-admin-app/synnefo_admin/admin/templates/admin/tips.html new file mode 100644 index 0000000000000000000000000000000000000000..662d4467b7e28fcee9b588df63336f8235f8bdc2 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/tips.html @@ -0,0 +1,54 @@ +{% load admin_tags %} +<div class="panel panel-default"> + <h2 class="panel-heading">Shortcuts</h2> + <dl class="dl-horizontal shortcuts panel-body"> + <dt><span class="key">i</span></dt> + <dd>Open/close the windowthat contains information regarding the state of the actions.</dd> + <dt><span class="key">esc</span></dt><dd>Close confirmation window.</dd> + <dt><span class="key">shift</span> + <span class="key">click</span></dt> + <dd>Order the contents of the table by selecting multiple columns.</dd> + </dl> + + {% comment %} + Show filter tips only in pages that actually have filters + {% endcomment %} + {% if filter_dict %} + <h2 class="panel-heading">Compact Filters View</h2> + <div class="panel-body"> + <p>Each filter has an identifier that can be used in <b>Compact Filters View</b>.</br> + The identifiers for the filters at the top of this page are:</br> + <b> + {% for filter in filter_dict %} + {{ filter.name }}{% if forloop.last %}.{% else %}, {% endif %} + {% endfor %} + </b> + </p> + <h3>Examples</h3> + <dl class="filters-examples"> + <dt>Search for the keyword "John" in the most common text fields of the <b>User model</b>.</dt> + <dd><span class="highlight">user:John</span></dd> + <dd class="divider"></dd> + <dt>Search for the keywords "John" and "Doe" in the most common text fields of the <b>User model</b> and return the conjuction <b>(logical AND)</b> of their results.</dt> + <dd><span class="highlight">user:John Doe</span></dd> + <dd><span class="highlight">user: John Doe</span></dd> + <dd class="divider"></dd> + <dt>Search for the keyword "John" in the <b>first_name</b> field of <b>User</b> and "Doe" in the <b>last_name</b> field, and return the conjuction of their results.</dt> + <dd><span class="highlight">user:first_name=John last_name=Doe</span></dd> + <dd class="divider"></dd> + <dt>Search for the keywords "John" and "Doe" but also limit the results by filtering the users that:</br> + - have status "Pending Moderation" <b>OR</b> "Active"</br> + - have VMs whose status is "Started"</br> + - have IPs that contain the number ".27"</br> + </dt> + <dd><span class="highlight">user:John Doe status:pending moderation,active vm:operstate=started ip:.27</span></dd> + <dd class="divider"></dd> + </dl> + <dl class="notes dl-horizontal"> + <dt>Note 1:</dt> + <dd>You can also experiment with Compact View by switching to and from Standard View.</dd> + <dt>Note 2:</dt> + <dd>For the time being, you can find more model field names by reading the <b>models.py files of Cyclades/Astakos</b>.</dd> + </dl> + </div> + {% endif %} +</div> diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/tooltip_associations.html b/snf-admin-app/synnefo_admin/admin/templates/admin/tooltip_associations.html new file mode 100644 index 0000000000000000000000000000000000000000..01a1ea000cfa6b498b7372d711cbf73726b17eeb --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/tooltip_associations.html @@ -0,0 +1,12 @@ +{% load admin_tags %} +{% if association.excluded %} + Excluding <b>{{ association.excluded }} {{ association|present_excluded}}</b> + from the results.<br> +{% endif %} + +{% if association.excluded|add:association.showing < association.total %} + Showing <b>{{ association.showing }} out of {{ association.total }}</b> total entries.<br> +{% endif %} + +To view all entries, visit the {{ type|display_list_type }} tab in navigation. +{{ association|show_more_exception_message }} diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/user_details.html b/snf-admin-app/synnefo_admin/admin/templates/admin/user_details.html new file mode 100644 index 0000000000000000000000000000000000000000..8f9e1b3694587404ce97c79668bbb406170628eb --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/user_details.html @@ -0,0 +1 @@ +{% extends "admin/base_details.html" %} diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/user_list.html b/snf-admin-app/synnefo_admin/admin/templates/admin/user_list.html new file mode 100644 index 0000000000000000000000000000000000000000..2ef0e979e2216988aa25e70f8b986eb1a2362c83 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/user_list.html @@ -0,0 +1 @@ +{% extends "admin/base_list.html" %} diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/vm_details.html b/snf-admin-app/synnefo_admin/admin/templates/admin/vm_details.html new file mode 100644 index 0000000000000000000000000000000000000000..8f9e1b3694587404ce97c79668bbb406170628eb --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/vm_details.html @@ -0,0 +1 @@ +{% extends "admin/base_details.html" %} diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/vm_list.html b/snf-admin-app/synnefo_admin/admin/templates/admin/vm_list.html new file mode 100644 index 0000000000000000000000000000000000000000..2ef0e979e2216988aa25e70f8b986eb1a2362c83 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/vm_list.html @@ -0,0 +1 @@ +{% extends "admin/base_list.html" %} diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/volume_details.html b/snf-admin-app/synnefo_admin/admin/templates/admin/volume_details.html new file mode 100644 index 0000000000000000000000000000000000000000..8f9e1b3694587404ce97c79668bbb406170628eb --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/volume_details.html @@ -0,0 +1 @@ +{% extends "admin/base_details.html" %} diff --git a/snf-admin-app/synnefo_admin/admin/templates/admin/volume_list.html b/snf-admin-app/synnefo_admin/admin/templates/admin/volume_list.html new file mode 100644 index 0000000000000000000000000000000000000000..2ef0e979e2216988aa25e70f8b986eb1a2362c83 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templates/admin/volume_list.html @@ -0,0 +1 @@ +{% extends "admin/base_list.html" %} diff --git a/snf-admin-app/synnefo_admin/admin/templatetags/__init__.py b/snf-admin-app/synnefo_admin/admin/templatetags/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/snf-admin-app/synnefo_admin/admin/templatetags/admin_tags.py b/snf-admin-app/synnefo_admin/admin/templatetags/admin_tags.py new file mode 100644 index 0000000000000000000000000000000000000000..a0e0425e4722488e0533f0eed51fad16ce47c46e --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/templatetags/admin_tags.py @@ -0,0 +1,482 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +from importlib import import_module +from collections import OrderedDict +from django import template +from django.conf import settings +from django.core.exceptions import ObjectDoesNotExist +import logging + +import django_filters + +from snf_django.lib.api import faults +from synnefo.api.util import get_image +from synnefo.lib.dict import SnfOrderedDict +from synnefo.db.models import Image + +import synnefo_admin.admin.resources.projects.utils as project_utils +import synnefo_admin.admin.resources.users.utils as user_utils +import synnefo_admin.admin.resources.vms.utils as vm_utils +from synnefo_admin.admin import utils +mod = import_module('astakos.im.management.commands.project-show') + +register = template.Library() + +status_map = {} +status_map['vm'] = { + 'BUILD': 'warning', + 'PENDING': 'warning', + 'ERROR': 'danger', + 'STOPPED': 'info', + 'STARTED': 'success', + 'ACTIVE': 'success', + 'DESTROYED': 'inverse' +} +status_map['volume'] = { + 'AVAILABLE': 'success', + 'IN_USE': 'success', + 'DELETING': 'warning', + 'ERROR': 'danger', + 'ERROR_DELETING': 'danger', + 'ERROR_RESTORING': 'danger', +} +status_map['network'] = { + 'ACTIVE': 'success', + 'ERROR': 'danger', +} +status_map['project'] = { + 'PENDING': 'warning', + 'ACTIVE': 'success', + 'DENIED': 'danger', + 'DELETED': 'danger', + 'SUSPENDED': 'warning', +} +status_map['application'] = status_map['project'] +status_map['application']['APPROVED'] = 'success' + + +@register.filter() +def get_status_from_instance(inst): + """Generic function to get the status of any instance.""" + try: + return inst.state_display() + except AttributeError: + pass + + try: + return inst.status_display + except AttributeError: + pass + + try: + return inst.operstate + except AttributeError: + pass + + try: + return inst.state + except AttributeError: + pass + + return inst.status + + +@register.filter(is_safe=True) +def status_label(inst): + """Return a span label styled based on the instance's current status""" + inst_type = utils.get_type_from_instance(inst) + state = get_status_from_instance(inst).upper() + state_cls = 'info' + + if inst_type == 'user': + if not inst.email_verified or not inst.moderated: + state_cls = 'warning' + elif inst.is_rejected: + state_cls = 'danger' + elif inst.is_active: + state_cls = 'success' + elif not inst.is_active: + state_cls = 'inverse' + else: + state_cls = status_map[inst_type].get(state, 'info') + + label_cls = "label label-%s" % state_cls + + if inst_type in ["project", "application"]: + name = inst_type.capitalize() + " Status: " + else: + name = "" + + deleted_label = "" + if getattr(inst, 'deleted', False): + deleted_label = '<span class="label label-danger">Deleted</span>' + return '%s\n<span class="%s">%s%s</span>' % (deleted_label, label_cls, + name, state) + + +@register.filter(name="get_os", is_safe=True) +def get_os(vm): + try: + return vm.metadata.filter(meta_key="OS").get().meta_value + except: + return "unknown" + + +@register.filter +def image_info(vm): + """Retrieve Image details. + + If the Image is deleted or no longer visible by the user, then provide + whatever information is stored by Cyclades. + """ + # Use the same order of appearance for Plankton and Cyclades image info + order = ['name', 'id', 'owner', 'version', 'size', 'created_at', + 'updated_at', 'deleted_at', 'is_public', 'is_snapshot', + 'is_system', 'location', 'os', 'osfamily', 'properties'] + + # TODO: Add this code when deferred loading works. + # Try to retrieve Image info using Plankton. + #try: + # image_info = get_image(vm.imageid, vm.userid) + #except faults.ItemNotFound: + + # Check if Cyclades DB has any info about this Image. + try: + image_info = Image.objects.get(uuid=vm.imageid) + except ObjectDoesNotExist: + # If all else fails, gather whatever info are available from the + # VirtualMachine instance. + image_info = { + 'id': vm.imageid, + 'version': vm.image_version, + 'os': get_os(vm), + } + + return SnfOrderedDict(image_info, order, strict=False) + + +@register.filter +def flavor_info(vm): + return vm_utils.get_flavor_info(vm) + + +@register.filter(name="network_vms", is_safe=True) +def network_vms(network, account, show_deleted=False): + vms = [] + nics = network.nics.filter(machine__userid=account) + if not show_deleted: + nics = nics.filter(machine__deleted=False).distinct() + for nic in nics: + vms.append(nic.machine) + return vms + + +@register.filter(name="network_nics") +def network_nics(network, account, show_deleted=False): + nics = network.nics.filter(machine__userid=account) + if not show_deleted: + nics = nics.filter(machine__deleted=False).distinct() + return nics + + +@register.filter(name="backend_info", is_safe=True) +def backend_info(vm): + content = "" + backend = vm.backend + excluded = ['password_hash', 'hash', 'username'] + if not vm.backend: + content = "No backend" + return content + + for field in vm.backend._meta.fields: + if field.name in excluded: + continue + content += '<dt>Backend ' + field.name + '</dt><dd>' + \ + str(getattr(backend, field.name)) + '</dd>' + return content + + +@register.filter +def display_list_type(type): + """Display the type of an item list in a human readable way.""" + if type == "user": + return "Users" + elif type == "project": + return "Projects" + elif type == "quota": + return "Quotas" + elif type == "vm": + return "Virtual Machines" + elif type == "network": + return "Networks" + elif type == "nic": + return "Network Interfaces" + elif type == "ip": + return "IP Addresses" + elif type == "volume": + return "Volumes" + elif type == "ip_log": + return "IP History" + else: + return "Unknown type" + + +@register.filter +def admin_debug(var): + """Print in the log a value.""" + logging.info("Template debugging: %s", var) + return var + + +@register.filter +def get_details_template(type): + """Get the correct template for the provided item.""" + template = 'admin/_' + type + '_details.html' + return template + + +@register.filter +def get_filter_type(filter): + """Get the flter type according to the filter class.""" + if isinstance(filter, django_filters.NumberFilter): + type = "number" + elif isinstance(filter, django_filters.CharFilter): + type = "char" + elif isinstance(filter, django_filters.BooleanFilter): + type = "bool" + elif isinstance(filter, django_filters.ChoiceFilter): + type = "choice" + elif isinstance(filter, django_filters.MultipleChoiceFilter): + type = "multichoice" + else: + raise Exception("Unknown filter type: %s", filter) + return type + + +@register.filter +def get_filter_template(filter): + """Get the correct flter template according to the filter type. + + This only works for filters that are instances of django-filter's Filter. + """ + type = get_filter_type(filter) + template = 'admin/filter_' + type + '.html' + return template + + +@register.filter +def choices(filter): + """Return an iterator with the available choices for a filter.""" + return [choice for choice, _ in filter.field.choices] + + +@register.filter +def id(item): + try: + return item['project'].uuid + except TypeError: + pass + + try: + return item.uuid + except AttributeError: + pass + + try: + return item.id + except AttributeError: + pass + + return item.pk + + +@register.filter() +def details_url(inst, target): + """Get a url for the details of an instance's field.""" + # Get instance type and import the appropriate utilities module. + inst_type = utils.get_type_from_instance(inst) + mod = import_module("synnefo_admin.admin.resources.{}s.utils".format(inst_type)) + + # Call the details_href function for the provided target. + func = getattr(mod, "get_{}_details_href".format(target), None) + if func: + return func(inst) + else: + raise Exception("Wrong target name: {}".format(target)) + + +@register.filter +def repr(item): + """Return the string representation of an item. + + If an item has a "realname" attribute that is not emprty, we return this. + Else, if an item has a "name" attribute that is not empty, we return this. + Finally, if an item has none of the above attributes, or the attributes are + empty, return the result of the __str__ method of the item. + """ + try: + return item.address + except AttributeError: + pass + + try: + return item['project'].realname + except TypeError: + pass + + try: + if item.realname: + return item.realname + except AttributeError: + pass + + try: + if item.name: + return item.name + except AttributeError: + pass + + return item.__str__() + + +@register.filter +def get_groups(user): + return user_utils.get_user_groups(user) + + +@register.filter +def verbify(action): + """Create verb from action name. + + If action has more than one words, then we keep the first one which, by + convention, will be a verb. + """ + return action.split()[0].capitalize() + + +@register.filter +def get_project_members(project): + members, _ = mod.members_fields(project) + return members + + +@register.filter +def get_project_stats(project): + """Create a dictionary with a summary for a project's stats.""" + limit = project_utils.get_project_quota_category(project, "limit") + usage = project_utils.get_project_usage(project) + member = project_utils.get_project_quota_category(project, "member") + if not usage: + usage = [(name, '-',) for name, _ in limit] + + if project.is_base: + all_stats = zip(limit, usage) + else: + all_stats = zip(member, limit, usage) + + new_stats = OrderedDict() + for row in all_stats: + resource_name = row[0][0] + new_stats[resource_name] = [] + for _, value in row: + new_stats[resource_name].append(value) + + return new_stats + + +@register.filter +def show_auth_providers(user, category): + """Show auth providers for a user.""" + func = getattr(user, "get_%s_auth_providers" % category) + providers = [prov.module for prov in func()] + if providers: + return ", ".join(providers) + else: + return "None" + + +@register.filter +def can_apply(action, item): + """Return if action can apply on item.""" + if action.name == "Send e-mail" and action.target != 'user': + return False + return action.can_apply(item) + + +@register.filter +def default_value(f): + """Set default value for filters. + + By default the value is all, except for filters with "NOT" in their label. + """ + if 'NOT' in f.label: + return 'None' + return 'All' + + +@register.filter +def present_excluded(assoc): + """Present what are the excluded entries.""" + if assoc.type == "user": + return "users that are not project members" + else: + return "deleted entries" + + +FILTER_NAME_ICON_MAP = { + 'vm': 'snf-pc-full', + 'user': 'snf-user-full', + 'vol': 'snf-volume-full', + 'volume': 'snf-volume-full', + 'net': 'snf-network-full', + 'network': 'snf-network-full', + 'proj': 'snf-clipboard-h', + 'project': 'snf-clipboard-h', +} + + +@register.filter +def label_to_icon(filter_name, filter_label): + """ + Return a span icon based on the filter name + If no icon is found, return filter label + """ + icon_cls = FILTER_NAME_ICON_MAP.get(filter_name) + if icon_cls: + label = '<span class="%s"></span>' % icon_cls + else: + label = filter_label + ":" + return label + + +@register.filter +def show_more_exception_message(assoc): + """Show an extra message for an instance in the popover for "Show More".""" + if assoc.type == "user": + return """</br>Alternatively, you may consult the "Members" tab of the project.""" + return "" + + +@register.simple_tag +def min_prefix(): + """ + Return minified files folder for production environment + """ + if settings.DEBUG == False: + return 'min-' + else: + return '' diff --git a/snf-admin-app/synnefo_admin/admin/tests/__init__.py b/snf-admin-app/synnefo_admin/admin/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3eda4ae0758b6c5f4140e36c121e466ed02fbfc6 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/tests/__init__.py @@ -0,0 +1,21 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# flake8: noqa +from synnefo_admin.admin.tests.urls import * +from synnefo_admin.admin.tests.views import * +from synnefo_admin.admin.tests.utils import * +from synnefo_admin.admin.tests.users import * +from synnefo_admin.admin.tests.projects import * diff --git a/snf-admin-app/synnefo_admin/admin/tests/common.py b/snf-admin-app/synnefo_admin/admin/tests/common.py new file mode 100644 index 0000000000000000000000000000000000000000..25ccf2f60ece05f534f006e0213db05b23f42f51 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/tests/common.py @@ -0,0 +1,215 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import functools +import django.test +import string +import random +import mock + +from astakos.im.tests.projects import ProjectAPITest +from astakos.im.functions import approve_application +from synnefo.db import models_factory as mf + +from synnefo_admin import admin_settings + + +USER1 = "5edcb5aa-1111-4146-a8ed-2b6287824353" +USER2 = "5edcb5aa-2222-4146-a8ed-2b6287824353" + +USERS_UUIDS = {} +USERS_UUIDS[USER1] = {'displayname': 'testuser@test.com'} +USERS_UUIDS[USER2] = {'displayname': 'testuser2@test.com'} + +USERS_DISPLAYNAMES = dict(map(lambda k: (k[1]['displayname'], {'uuid': k[0]}), + USERS_UUIDS.iteritems())) + + +def gibberish(length=10, like='string'): + """Create a random number, a gibberish string or email.""" + if like not in ['string', 'number', 'email']: + raise Exception("You gave me gibberish: %s" % (like)) + + matrix = string.digits + if not like == 'number': + matrix += string.ascii_letters + + if like == 'email': + if length < 8: + raise Exception("Can't create email with less than 8 characters") + # Remove '@', '.' and TLD (always 2 chars) + length -= 4 + tld = gibberish(2) + dom_len = length / 2 + domain = gibberish(dom_len) + name_len = length - dom_len + name = gibberish(name_len) + return name + '@' + domain + '.' + tld + + gib_list = [random.choice(matrix) for n in xrange(length)] + gib = ''.join(gib_list) + + return gib if not like == 'number' else int(gib) + + +def for_all_views(views=admin_settings.ADMIN_VIEWS.keys()): + """Decorator that runs a test for all the specified views.""" + def decorator(func): + @functools.wraps(func) + def wrapper(self, *args, **kwargs): + for view in views: + self.current_view = view + func(self, *args, **kwargs) + return wrapper + return decorator + + +class AstakosClientMock(): + + """Mock class for astakosclient.""" + + def __init__(*args, **kwargs): + pass + + def get_username(self, uuid): + try: + return USERS_UUIDS.get(uuid)['displayname'] + except TypeError: + return None + + def get_uuid(self, display_name): + try: + return USERS_DISPLAYNAMES.get(display_name)['uuid'] + except TypeError: + return None + + +class AuthClient(django.test.Client): + + """Mock class for Django AuthClient.""" + + def request(self, **request): + """Fill the HTTP_X_AUTH_TOKEN parameter with user token.""" + token = request.pop('user_token', '0000') + if token: + request['HTTP_X_AUTH_TOKEN'] = token + return super(AuthClient, self).request(**request) + + +def get_user_mock(request, *args, **kwargs): + """Mock function that fills an HTTP request. + + Return a different request based on the provided token. The '0000' token + will return a request for an unauthorized user, while the '0001' token will + return a request for an admin user. + """ + request.user_uniq = None + request.user = None + if request.META.get('HTTP_X_AUTH_TOKEN', None) == '0000': + request.user_uniq = 'test' + request.user = {"access": { + "token": { + "expires": "2013-06-19T15:23:59.975572+00:00", + "id": "0000", + "tenant": { + "id": "test", + "name": "Firstname Lastname" + } + }, + "serviceCatalog": [], + "user": { + "roles_links": [], + "id": "test", + "roles": [{"id": 1, "name": "default"}], + "name": "Firstname Lastname"}} + } + + if request.META.get('HTTP_X_AUTH_TOKEN', None) == '0001': + request.user_uniq = 'test' + request.user = {"access": { + "token": { + "expires": "2013-06-19T15:23:59.975572+00:00", + "id": "0001", + "tenant": { + "id": "test", + "name": "Firstname Lastname" + } + }, + "serviceCatalog": [], + "user": { + "roles_links": [], + "id": "test", + "roles": [{"id": 1, "name": "default"}, + {"id": 2, "name": "admin"}], + "name": "Firstname Lastname"}} + } + + +@mock.patch("astakosclient.AstakosClient", new=AstakosClientMock) +@mock.patch("snf_django.lib.astakos.get_user", new=get_user_mock) +class AdminTestCase(ProjectAPITest): + + """Generic TestCase for admin tests. + + This TestCase class is based on the ProjectAPITest class, which has some + useful functions and Astakos models already created. We enhance the above + by adding model instances through the model_factory of cyclades. + """ + + def setUp(self): + """Common setUp for all AdminTestCases. + + This setUp method will create and approve a project as well as add + several Cyclades model instances using Cyclades' model_factory. + """ + ProjectAPITest.setUp(self) + # Create a simple project. + h_owner = {"HTTP_X_AUTH_TOKEN": self.user1.auth_token} + + app1 = {"name": "test.pr", + "description": u"δεσκÏίπτιον", + "end_date": "2113-5-5T20:20:20Z", + "join_policy": "auto", + "max_members": 5, + "resources": {u"σÎÏβις1.ÏίσοÏÏ‚11": { + "project_capacity": 1024, + "member_capacity": 512}} + } + + status, body = self.create(app1, h_owner) + + # Ensure that the project application has been created. + self.assertEqual(status, 201) + project_id = body["id"] + app_id = body["application"] + + # Approve the application. + self.project = approve_application(app_id, project_id) + self.assertIsNotNone(self.project) + + # Alias owner user with a generic name + self.user = self.user1 + + # Create cyclades models. + self.vm = mf.VirtualMachineFactory() + self.volume = mf.VolumeFactory() + self.network = mf.NetworkFactory() + self.ipv4 = mf.IPv4AddressFactory(address="1.2.3.4") + self.ipv6 = mf.IPv6AddressFactory(address="::1") + + # Override the test_projects function of ProjectAPITest class so that it + # doesn't get called. + test_projects = None diff --git a/snf-admin-app/synnefo_admin/admin/tests/projects.py b/snf-admin-app/synnefo_admin/admin/tests/projects.py new file mode 100644 index 0000000000000000000000000000000000000000..25b6a81bb437e22a2916d9bdf044327ae4844f8a --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/tests/projects.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +#import logging +from astakos.im.models import Resource + +from synnefo_admin.admin.resources.projects.utils import get_project_quota_category +from .common import AdminTestCase + + +class TestAdminProjects(AdminTestCase): + + """Test suite for project-related tests.""" + + def test_quota(self): + """Test if project quota are measured properly.""" + # Get the reported description of the resource. + resource = Resource.objects.get(name=u"σÎÏβις1.ÏίσοÏÏ‚11") + desc = resource.report_desc + + # Get the member and project quota. + member_quota = get_project_quota_category(self.project, "member") + project_quota = get_project_quota_category(self.project, "limit") + + # Compare them to the ones in the application. + self.assertEqual(member_quota, [(desc, '512')]) + self.assertEqual(project_quota, [(desc, '1024')]) diff --git a/snf-admin-app/synnefo_admin/admin/tests/urls.py b/snf-admin-app/synnefo_admin/admin/tests/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..a80089b33dee5a9530468850b8709048237b8b88 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/tests/urls.py @@ -0,0 +1,135 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys + +import django.test +from django.conf import settings +from django.core.urlresolvers import resolve, set_urlconf, Resolver404 + +from snf_django.utils.testing import override_settings + +URL1 = "https://example.synnefo.org/rand0m" +URL2 = "https://admin.example.synnefo.org" + + +def reload_urlconf(urlconf=None): + """Reload Synnefo's URLconf after ADMIN_BASE_URL is changed. + + This function should be called everytime ADMIN_BASE_URL is altered. It + tries to propagate this change to the RegexURLResolver Django class by: + a) Reloading the Admin settings and recalculating the BASE_PATH setting. + b) Reloading the Admin urls, and thus recreating the Admin URLpatterns, + which depend on the BASE_PATH setting. + c) Reloading the Synnefo webproject URLs in order to integrate the new + Admin URLs with the URLs of Synnefo. + + The cached Django RegexURLResolver has a pointer to the + 'synnefo.webproject.urls' module, so the changes should be instantly + visible. + + The caller can optionally set a new URLconf to use. + + Before the end of the test, this function should always be called with no + arguments once the ADMIN_BASE_URL is restored, in order to bring the test + suite in its initial state. + """ + reload(sys.modules['synnefo_admin.admin_settings']) + reload(sys.modules['synnefo_admin.urls']) + reload(sys.modules['synnefo.webproject.urls']) + # Using this function with no urlconf will reset the ROOT_URLCONF to its + # initial value. + set_urlconf(urlconf) + + +class TestAdminUrls(django.test.TestCase): + + """Unit tests for Admin urls.""" + + def tearDown(self): + """Bring the URLpatterns to their initial state.""" + reload_urlconf() + + def test_url_resolving(self): + """Test if URL resolving works properly. + + Check if the ADMIN_BASE_URL setting is used by Admin and if we have any + issues with slashes and redirections. + """ + ## + # Test 1 - Default Admin URL. + # + # By default, the BASE_PATH for Admin is '/admin/'. + r = resolve('/admin/') + self.assertEqual(r.url_name, 'admin-default') + + # If we try to resolve the BASE_PATH without the slash, then we should + # get redirected to the correct ('/admin/') URL. + r = resolve('/admin') + self.assertEqual(r.url_name, 'django.views.generic.simple.redirect_to') + self.assertEqual(r.args, ()) + self.assertEqual(r.kwargs, {'url': 'admin/'}) + + # Any URL that starts with the '/admin' string but has extra characters + # should return 404. + with self.assertRaises(Resolver404): + r = resolve('/adminandoopstoomanychars') + + ## + # Test 2 - Custom Admin URL with suffix. This tests if URL resolving + # works properly when using a custom ADMIN_BASE_URL with a unique + # suffix. + + # Change the ADMIN_BASE_URL and update the URLpatterns of Synnefo. + with override_settings(settings, ADMIN_BASE_URL=URL1): + reload_urlconf() + + # Check that the '/admin/' URL no longer works. + with self.assertRaises(Resolver404): + r = resolve('/admin/') + + # The new BASE_PATH should be 'rand0m'. Check if the resolved view + # is the expected. + r = resolve('/rand0m/') + self.assertEqual(r.url_name, 'admin-default') + + # Check if resolving the BASE_PATH without a slash redirects us + # properly. + r = resolve('/rand0m') + self.assertEqual(r.url_name, + 'django.views.generic.simple.redirect_to') + self.assertEqual(r.args, ()) + self.assertEqual(r.kwargs, {'url': 'rand0m/'}) + + # Check if extra characters return 404. + with self.assertRaises(Resolver404): + r = resolve('/rand0mandoopstoomanychars') + + ## + # Test 3 - Custom Admin URL without suffix. This tests if URL resolving + # works properly when using a custom ADMIN_BASE_URL that points to a + # node only, with no extra suffix. + + # Although Admin can be installed in a separate node from the + # Cyclades/Astakos nodes, their packages - and by extension their + # URLPatterns - will be installed in the Admin node. In order to "hide" + # their urls, we will use the Admin urls as our ROOT_URLCONF for this + # part of the test. + with override_settings(settings, ADMIN_BASE_URL=URL2): + reload_urlconf('synnefo_admin.urls') + + # Check that hitting the node URL sends us to Admin. + r = resolve('/') + self.assertEqual(r.url_name, 'admin-default') diff --git a/snf-admin-app/synnefo_admin/admin/tests/users.py b/snf-admin-app/synnefo_admin/admin/tests/users.py new file mode 100644 index 0000000000000000000000000000000000000000..c9338dab7c505fc9c5bef91d564d34897f9226d9 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/tests/users.py @@ -0,0 +1,162 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +#import logging +from django.core.urlresolvers import reverse + +from astakos.im.models import ProjectMembership, Resource +from astakos.im.functions import remove_membership, enroll_member +from synnefo.db import models_factory as mf + +from synnefo_admin import admin_settings +from synnefo_admin.admin.resources.users.utils import get_suspended_vms, get_quotas +from .common import AdminTestCase + + +class TestAdminUsers(AdminTestCase): + + """Test suite for user-related tests.""" + + def expected_href(self, pk): + """Create an href string for a vm with the provided pk.""" + vm_details = reverse('admin-details', args=['vm', pk]) + return '<a href={0}>Name{1} (id:{1})</a>'.format(vm_details, pk) + + def test_suspended_vms(self): + """Test if suspended VMs for a user are displayed properly.""" + # The VMs that will be shown at any time to the user are limited by the + # setting below. + start = 1134 + end = start + admin_settings.ADMIN_LIMIT_SUSPENDED_VMS_IN_SUMMARY + + # Test 1 - Assert that 'None' is displayed when there are no suspended + # VMs. + vms = get_suspended_vms(self.user) + self.assertEqual(vms, 'None') + + # Test 2 - Assert that only one href is printed when there is only one + # suspended VM. + mf.VirtualMachineFactory(userid=self.user.uuid, pk=start, + name='Name{}'.format(start), + suspended=True) + vms = get_suspended_vms(self.user) + self.assertEqual(vms, self.expected_href(start)) + + # Test 3 - Assert that the hrefs are comma-separated when there are + # more than one suspended VMs, in descending ID order. + # + # Create one more VM, get the list of suspended VMs and split it. + mf.VirtualMachineFactory(userid=self.user.uuid, pk=(start + 1), + name='Name{}'.format(start + 1), + suspended=True) + vms = get_suspended_vms(self.user).split(', ') + + # Asssert that each element of the list (1135, 1134) is displayed + # properly. + for pk in reversed(xrange(start, start + 2)): + i = start + 1 - pk + self.assertEqual(vms[i], self.expected_href(pk)) + + # Test 4 - Assert that dots ('...') are printed when there are too + # many suspended VMs and that the older VMs are ommited. + # + # Create more VMs than the current limit and split them like before. + for pk in range(start + 2, end + 1): + mf.VirtualMachineFactory(userid=self.user.uuid, pk=pk, + name='Name{}'.format(pk), suspended=True) + vms = get_suspended_vms(self.user).split(', ') + + # Asssert that each element of the list (1144...1135) is displayed + # properly and that the VM that was created first is ommited and + # replaced with dots ('...'). + for pk in reversed(xrange(start, end + 1)): + i = end - pk + if pk == start: + self.assertEqual(vms[i], '...') + else: + self.assertEqual(vms[i], self.expected_href(pk)) + + def test_quota(self): + """Test if quotas are displayed properly for a user.""" + def get_project_id(obj): + """Sort by project id.""" + return obj['project'].uuid + + def assertQuotaEqual(quota1, quota2): + """Custom assert function fro quota lists.""" + quota1 = sorted(quota1, key=get_project_id) + quota2 = sorted(quota2, key=get_project_id) + self.assertEqual(len(quota1), len(quota2)) + + for q1, q2 in zip(quota1, quota2): + p1 = q1['project'] + p2 = q2['project'] + r1 = q1['resources'] + r2 = q2['resources'] + self.assertEqual(p1.uuid, p2.uuid) + self.assertItemsEqual(r1, r2) + + # Get the reported description of the resources. + resource = Resource.objects.get(name=u"σÎÏβις1.ÏίσοÏÏ‚11") + desc11 = resource.report_desc + resource = Resource.objects.get(name=u"σÎÏβις1.resource12") + desc12 = resource.report_desc + resource = Resource.objects.get(name=u"astakos.pending_app") + desc13 = resource.report_desc + + # These should be the base quota of the user + base_quota = [{'project': self.user.base_project, + 'resources': [(desc11, '0', '100'), + (desc13, '0', '3'), + (desc12, '0 bytes', '1.00 kB')] + }] + + # Test 1 - Check if get_quotas works properly for base quotas + quota = get_quotas(self.user) + assertQuotaEqual(quota, base_quota) + + # Test 2 - Check if get_quotas works properly for projects too. + # + # Add member to a project + enroll_member(self.project.uuid, self.user) + + # These should be the additional quota from the project. + new_quota = [{'project': self.project, + 'resources': [(desc11, '0', '512')] + }] + + # Assert that get_quotas works as intented + quota = get_quotas(self.user) + assertQuotaEqual(new_quota + base_quota, quota) + + # Test 3 - Check if get_quotas shows zero values for removed member + # from a project. + # + # Remove member from project + membership = ProjectMembership.objects.get(project=self.project, + person=self.user) + remove_membership(membership.id) + + # These should be the new quota from the project. They are zero since + # the user has not used any of the resources, but they are still + # displayed as the project limit is > 0. + new_quota = [{'project': self.project, + 'resources': [(desc11, '0', '0')] + }] + + # Assert that get_quotas works as intented + quota = get_quotas(self.user) + assertQuotaEqual(new_quota + base_quota, quota) diff --git a/snf-admin-app/synnefo_admin/admin/tests/utils.py b/snf-admin-app/synnefo_admin/admin/tests/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..d1286f0b3966815e33b09e39b9b75f81236b8d22 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/tests/utils.py @@ -0,0 +1,236 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import unittest +import sys + +from django.core import mail +from django.conf import settings + +from synnefo.db import models_factory as mf +from astakos.im import settings as astakos_settings +from snf_django.lib.api import faults +from snf_django.utils.testing import override_settings + +from synnefo_admin import admin_settings +from synnefo_admin.admin import views +from synnefo_admin.admin import utils +from synnefo_admin.admin.exceptions import AdminHttp404 +from .common import for_all_views, AdminTestCase, gibberish + +model_views = admin_settings.ADMIN_VIEWS.copy() +# Remove the model views that do not have details +model_views.pop('ip_log', None) +model_views.pop('group', None) + + +class MockUser(object): + realname = log_display = "Spider Jerusalem" + first_name = "Spider" + last_name = "Jerusalem" + email = "thetruth@thehole.com" + + +class MockRequest(object): + + """Mock a Django request.""" + + def __init__(self, content): + self.POST = content + + def update(self, content): + self.POST.update(content) + + +def reload_settings(): + """Reload admin settings after a Django setting has changed.""" + reload(sys.modules['synnefo_admin.admin_settings']) + + +class TestAdminUtilsUnit(unittest.TestCase): + + """Unit test suite for admin utility functions.""" + + def test_render_email(self): + """Test if emails are rendered properly.""" + user = MockUser() + + # Test 1 - Check if reqular emails are returned properly + request = { + "subject": "Very confidential", + "text": "This is a very confidential mail", + } + + subject, body = utils.render_email(user, request) + self.assertEqual(request["subject"], subject) + self.assertEqual(request["text"], body) + + # Test 2 - Check if emails with parameters are formatted properly + subject = """Very confidential for {{ first_name }} + {{ last_name }} or {{ full_name }} ({{ email }})""" + body = """This is a very confidential mail for {{ first_name }} + {{ last_name }} or {{ full_name }} ({{ email }})""" + + expected_subject = """Very confidential for Spider + Jerusalem or Spider Jerusalem (thetruth@thehole.com)""" + expected_body = """This is a very confidential mail for Spider + Jerusalem or Spider Jerusalem (thetruth@thehole.com)""" + + request = { + "subject": subject, + "text": body, + } + + subject, body = utils.render_email(user, request) + self.assertEqual(expected_subject, subject) + self.assertEqual(expected_body, body) + + def test_send_admin_email(self): + """Test if send_admin_email works properly.""" + def verify_sent_email(request, mail): + self.assertEqual(request.POST['subject'], mail.subject) + self.assertEqual(request.POST['text'], mail.body) + self.assertEqual(request.POST['sender'], mail.from_email) + + user = MockUser() + default_sender = astakos_settings.SERVER_EMAIL + + # Test 1 - Check if malformed contact request raises BadRequest: + # + # a) Request with no POST dictionary. + bad_request = {} + + with self.assertRaises(faults.BadRequest) as cm: + utils.send_admin_email(user, bad_request) + self.assertEqual("Contact request does not have a POST dictionary.", + cm.exception.message) + + # b) Request with required fields missing. + bad_request = MockRequest({ + "subject": 'Subject', + "sender": astakos_settings.SERVER_EMAIL, + }) + + with self.assertRaises(faults.BadRequest) as cm: + utils.send_admin_email(user, bad_request) + self.assertEqual( + "Contact request does not have the following fields: text", + cm.exception.message) + + # Test 2 - Check if email from default sender is sent properly and that + # the default sender remains the same. + request = MockRequest({ + "sender": astakos_settings.SERVER_EMAIL, + "subject": 'Subject', + "text": 'Body', + }) + + utils.send_admin_email(user, request) + self.assertEqual(len(mail.outbox), 1) + verify_sent_email(request, mail.outbox[0]) + self.assertEqual(default_sender, astakos_settings.SERVER_EMAIL) + + # Test 3 - Check if email from custom sender is sent properly and that + # the default sender remains the same. + request.update({"sender": 'admin@lemonparty.org'}) + + utils.send_admin_email(user, request) + self.assertEqual(len(mail.outbox), 2) + verify_sent_email(request, mail.outbox[1]) + self.assertEqual(default_sender, astakos_settings.SERVER_EMAIL) + + def test_default_view(self): + """Test if the default_view() function works as expected.""" + self.assertEqual(utils.default_view(), 'user') + with override_settings(settings, ADMIN_VIEWS_ORDER=[]): + reload_settings() + self.assertEqual(utils.default_view(), None) + + with override_settings(settings, ADMIN_VIEWS_ORDER=['1', '2']): + reload_settings() + self.assertEqual(utils.default_view(), None) + + with override_settings(settings, ADMIN_VIEWS_ORDER=['1', 'user', '3']): + reload_settings() + self.assertEqual(utils.default_view(), 'user') + + reload_settings() + + +class TestAdminUtilsIntegration(AdminTestCase): + + """Integration test suite for admin utility functions.""" + + def get_ip_or_404_helper(self, get_model_or_404): + for ip_version in ['ipv4', 'ipv6']: + model = getattr(self, ip_version, None) + + returned_model = get_model_or_404(model.id) + self.assertEqual(model, returned_model) + with self.assertRaises(AdminHttp404): + get_model_or_404(gibberish(like='number')) + + returned_model = get_model_or_404(model.address) + self.assertEqual(model, returned_model) + with self.assertRaises(AdminHttp404): + get_model_or_404(gibberish()) + + self.ipv4_2 = mf.IPv4AddressFactory(address="1.2.3.4") + self.ipv6_2 = mf.IPv6AddressFactory(address="::1") + + for ip_version in ['ipv4', 'ipv6']: + model = getattr(self, ip_version, None) + + returned_model = get_model_or_404(model.id) + self.assertEqual(model, returned_model) + + with self.assertRaises(AdminHttp404): + returned_model = get_model_or_404(model.address) + + @for_all_views(model_views.keys()) + def test_get_or_404(self): + mod = views.get_view_module_or_404(self.current_view) + self.assertIsNotNone(mod) + + get_model_or_404 = getattr(mod, 'get_%s_or_404' % self.current_view, + None) + self.assertIsNotNone(get_model_or_404) + + # Special handling for ips + if self.current_view == 'ip': + self.get_ip_or_404_helper(get_model_or_404) + return + + model = getattr(self, self.current_view, None) + self.assertIsNotNone(model) + + if hasattr(model, 'uuid'): + returned_model = get_model_or_404(model.uuid) + self.assertEqual(model, returned_model) + with self.assertRaises(AdminHttp404): + get_model_or_404(gibberish()) + + if hasattr(model, 'email'): + returned_model = get_model_or_404(model.email) + self.assertEqual(model, returned_model) + with self.assertRaises(AdminHttp404): + get_model_or_404(gibberish(like='email')) + + if hasattr(model, 'id'): + returned_model = get_model_or_404(model.id) + self.assertEqual(model, returned_model) + with self.assertRaises(AdminHttp404): + get_model_or_404(gibberish(like='number')) diff --git a/snf-admin-app/synnefo_admin/admin/tests/views.py b/snf-admin-app/synnefo_admin/admin/tests/views.py new file mode 100644 index 0000000000000000000000000000000000000000..b1f89551bd0015c14ea8d800496ee1626e68395e --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/tests/views.py @@ -0,0 +1,152 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import mock +import unittest + +import django.test +from django.conf import settings +from django.core.urlresolvers import reverse + +from synnefo_admin.admin.exceptions import AdminHttp404 +from synnefo_admin.admin import views +from synnefo_admin import admin_settings + +from .common import (for_all_views, AuthClient, get_user_mock, + AstakosClientMock, gibberish) + + +class TestAdminViewsUnit(unittest.TestCase): + + """Unit tests for Admin views.""" + + @for_all_views() + def test_import_module_success(self): + """Test if model view modules are imported successfully.""" + mod = views.get_view_module_or_404(self.current_view) + self.assertIsNotNone(mod) + + view = views.get_json_view_or_404(self.current_view) + self.assertIsNotNone(view) + + def test_import_module_fail(self): + """Test if importing malformed view modules fails properly.""" + with self.assertRaises(AdminHttp404) as cm1: + views.get_view_module_or_404(None) + self.assertEqual(cm1.exception.message, "No category provided.") + + gib = gibberish() + with self.assertRaises(AdminHttp404) as cm2: + views.get_view_module_or_404(gib) + with self.assertRaises(AdminHttp404) as cm3: + views.get_json_view_or_404(gib) + self.assertEqual(cm2.exception.message, + "No category found with this name: %s" % gib) + self.assertEqual(cm3.exception.message, + "No category found with this name: %s" % gib) + + +@mock.patch("astakosclient.AstakosClient", new=AstakosClientMock) +@mock.patch("snf_django.lib.astakos.get_user", new=get_user_mock) +class TestAdminViewsIntegration(django.test.TestCase): + + """Integration tests for admin views.""" + + def setUp(self): + """Common setUp method for all tests of this suite.""" + settings.SKIP_SSH_VALIDATION = True + admin_settings.ADMIN_ENABLED = True + self.client = AuthClient() + + def assertHttpStatusModelView(self, view, status, *args, **kwargs): + """Hit all urls of model views and assert their status. + + For each view_type ('user', 'vm', ...) match three urls: + 1) admin-list, 2) admin-json, 3) admin-details. + + Hit each of these urls for the given view type and assert that the + returned status is the same as the provided one. + """ + # admin is disabled + r = self.client.get(reverse('admin-list', args=[view]), *args, + **kwargs) + self.assertEqual(r.status_code, status) + + r = self.client.get(reverse('admin-json', args=[view]), *args, + **kwargs) + self.assertEqual(r.status_code, status) + + r = self.client.get(reverse('admin-details', args=[view, gibberish()]), + *args, **kwargs) + self.assertEqual(r.status_code, status) + + @for_all_views() + def test_enabled_setting_for_model_views(self): + """Test if the ADMIN_ENABLED setting is respected by model views.""" + admin_settings.ADMIN_ENABLED = False + self.assertHttpStatusModelView(self.current_view, 404, + user_token="0001") + + @for_all_views(views=['stats-component', 'stats-component-details']) + def test_enabled_setting_for_stats(self): + """Test if the ADMIN_ENABLED setting is respected by stats views.""" + admin_settings.ADMIN_ENABLED = False + + # admin is disabled + r = self.client.get(reverse('admin-%s' % self.current_view, + args=[gibberish()]), + user_token="0001") + self.assertEqual(r.status_code, 404) + + @for_all_views(views=['default', 'home', 'logout', 'charts', 'stats', + 'actions']) + def test_enabled_setting_for_other_views(self): + """Test if the ADMIN_ENABLED setting is respected by the rest views.""" + admin_settings.ADMIN_ENABLED = False + + # admin is disabled + r = self.client.get(reverse('admin-%s' % self.current_view), + user_token="0001") + self.assertEqual(r.status_code, 404) + + @for_all_views() + def test_view_permissions_for_model_views(self): + """Test if unauthorized users can access the admin views.""" + # anonymous user gets 403 + self.assertHttpStatusModelView(self.current_view, 403, user_token=None) + + # user not in admin group gets 403 + self.assertHttpStatusModelView(self.current_view, 403) + + def test_404_in_model_views(self): + """Test if authorized users get 404 in invalid admin model views.""" + # valid user in wrong model view gets 404 + self.assertHttpStatusModelView(gibberish(), 404, user_token='0001') + + @for_all_views(views=['stats-component', 'stats-component-details']) + def test_404_in_stats_views(self): + """Test if authorized users get 404 in invalid stats views.""" + r = self.client.get(reverse('admin-%s' % self.current_view, + args=[gibberish()]), + user_token="0001") + self.assertEqual(r.status_code, 404) + + @for_all_views(views=['default', 'home', 'logout', 'charts', 'stats', + 'actions']) + def test_404_in_other_views(self): + """Test if authorized users get 404 in all other views.""" + r = self.client.get(reverse('admin-%s' % self.current_view) + '/' + + gibberish(), user_token="0001") + self.assertEqual(r.status_code, 404) diff --git a/snf-admin-app/synnefo_admin/admin/urls.py b/snf-admin-app/synnefo_admin/admin/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..3ae7fbeeac1044ce1268685138df4aa8a78d6435 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/urls.py @@ -0,0 +1,36 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +"""Url configuration for the admin interface""" + +from django.conf.urls import patterns, url + +urlpatterns = patterns( + 'synnefo_admin.admin.views', + url(r'^$', 'catalog', name='admin-default'), + url(r'^home$', 'home', name='admin-home'), + url(r'^logout$', 'logout', name='admin-logout'), + url(r'^charts$', 'charts', name='admin-charts'), + url(r'^stats$', 'stats', name='admin-stats'), + url(r'^stats/(?P<component>.*)/detail$', 'stats_component_details', + name='admin-stats-component-details'), + url(r'^stats/(?P<component>.*)$', 'stats_component', + name='admin-stats-component'), + url(r'^json/(?P<type>.*)$', 'json_list', name='admin-json'), + url(r'^actions/$', 'admin_actions', name='admin-actions'), + url(r'^(?P<type>.*)/(?P<id>.*)$', 'details', name='admin-details'), + url(r'^(?P<type>.*)$', 'catalog', name='admin-list'), +) diff --git a/snf-admin-app/synnefo_admin/admin/utils.py b/snf-admin-app/synnefo_admin/admin/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..b47ef3b9a28d224d8c6641b95fc2d14a7191afa2 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/utils.py @@ -0,0 +1,296 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import functools +import logging +import inspect +from importlib import import_module + +from django.views.decorators.gzip import gzip_page +from django.template import Context, Template +from django.core.urlresolvers import reverse +from django.utils.html import escape + +from astakos.im.models import Resource +from synnefo.db.models import (VirtualMachine, Volume, Network, IPAddress, + IPAddressLog) +from astakos.im.models import AstakosUser, Project, ProjectApplication + +from synnefo.util import units +from astakos.im.user_utils import send_plain as send_email +from snf_django.lib.api import faults + +from synnefo_admin import admin_settings +from astakos.im import settings as astakos_settings + +from .actions import get_allowed_actions, get_permitted_actions +logger = logging.getLogger(__name__) + + +"""A mapping between model names and Django models.""" +model_dict = { + "user": AstakosUser, + "vm": VirtualMachine, + "volume": Volume, + "network": Network, + "ip": IPAddress, + "ip_log": IPAddressLog, + "project": Project, + "application": ProjectApplication, +} + + +def default_view(): + """Return the first registered view based on ADMIN_VIEWS. + + If the ADMIN_VIEWS dict is empty, return None. + """ + return next(admin_settings.ADMIN_VIEWS.iterkeys(), None) + + +def __reverse_model_dict(): + """Create the a model dict with the class names as keys.""" + reversed_model_dict = {} + for key, value in model_dict.iteritems(): + reversed_model_dict[value.__name__] = key + return reversed_model_dict +reversed_model_dict = __reverse_model_dict() + + +def get_type_from_instance(inst): + """Get the name for the instance class that is used in admin app.""" + inst_cls_name = inst.__class__.__name__ + return reversed_model_dict[inst_cls_name] + + +def admin_log(request, *argc, **kwargs): + caller_name = inspect.stack()[1][3] + s = "User: %s, " % request.user['access']['user']['name'] + s += "View: %s, " % caller_name + + for key, value in kwargs.iteritems(): + s += "%s: %s, " % (key.capitalize(), value) + + if caller_name == "admin_actions": + logging.info(s) + else: + logging.debug(s) + + +def conditionally_gzip_page(func): + """Decorator to gzip response of unpaginated json requests.""" + @functools.wraps(func) + def wrapper(request, *args, **kwargs): + display_length = getattr(request.REQUEST, 'iDisplayLength', None) + if not display_length or display_length > 0: + return func(request, *args, **kwargs) + else: + return gzip_page(func)(request, *args, **kwargs) + return wrapper + + +def get_resource(name): + r = cached_resources.get(name, None) + if not r: + r = Resource.objects.get(name=name) + cached_resources[name] = r + return r +cached_resources = {} + + +def is_resource_useful(resource, limit): + """Simple function to check if the resource is useful to show. + + Values that have infinite or zero limits are discarded. + """ + displayed_limit = units.show(limit, resource.unit) + if limit == 0 or not resource.uplimit or displayed_limit == 'inf': + return False + return True + + +def get_actions(target, user=None, inst=None): + """Generic function for getting actions for various targets. + + Note: this function will import the action module for the target, which + means that it may be slow. + """ + if target in ['quota', 'nic', 'ip_log']: + return None + + mod = import_module('synnefo_admin.admin.resources.%ss.actions' % target) + actions = mod.cached_actions + if inst: + return get_allowed_actions(actions, inst, user) + else: + return get_permitted_actions(actions, user) + + +def update_actions_rbac(actions): + """Add allowed groups to actions dictionary from settings. + + Read the settings file to find the allowed groups for this action. + """ + for op, action in actions.iteritems(): + target = action.target + groups = [] + try: + groups = admin_settings.ADMIN_RBAC[target][op] + except KeyError: + pass + + action.allowed_groups = groups + + +def assert_valid_contact_request(request): + """Check if the request is a valid contact request. + + In order to be a valid contact request, it should contain a POST dictionary + and the following fields: sender, subject, text. If any of the above are + missing, raise BadRequest with the appropriate message. + """ + if not hasattr(request, 'POST'): + raise faults.BadRequest( + "Contact request does not have a POST dictionary.") + + required_fields = ['sender', 'subject', 'text'] + error_fields = set(required_fields) - set(request.POST.keys()) + + if error_fields: + error_fields = ', '.join(error_fields) + error_msg = "Contact request does not have the following fields: {}" + raise faults.BadRequest(error_msg.format(error_fields)) + + +class CustomSender(object): + + """Context manager for setting and restoring the SERVER_EMAIL setting.""" + + def __init__(self, sender): + """Store the default and the provided sender.""" + self.custom_sender = sender + self.default_sender = astakos_settings.SERVER_EMAIL + + def __enter__(self): + """Use the provided sender as default.""" + astakos_settings.SERVER_EMAIL = self.custom_sender + + def __exit__(self, exc_type, exc_value, traceback): + """Restore default sender.""" + astakos_settings.SERVER_EMAIL = self.default_sender + + +def render_email(user, request): + """Render an email and return its subject and body. + + This function takes as arguments a QueryDict and a user. The user will + serve as the target of the mail. The QueryDict should contain a `text` and + `subject` attribute that will be used as the body and subject of the mail + respectively. + + The email can optionally be customized with user information. If the user + has provided any one of the following variables: + + {{ full_name }}, {{ first_name }}, {{ last_name }}, {{ email }} + + then they will be rendered appropriately. + """ + c = Context({'full_name': user.realname, + 'first_name': user.first_name, + 'last_name': user.last_name, + 'email': user.email, }) + + # Render the mail body + t = Template(request['text']) + body = t.render(c) + + # Render the mail subject + t = Template(request['subject']) + subject = t.render(c) + return subject, body + + +def send_admin_email(user, request): + """Use request to render an email and send it to a user.""" + assert_valid_contact_request(request) + subject, body = render_email(user, request.POST) + sender = request.POST.get('sender') + send_email(user, sender=sender, subject=subject, template_name=None, + text=body) + + +def create_details_href(type, name, id): + """Create an href (name + url) for an item.""" + name = escape(name) + url = reverse('admin-details', args=[type, id]) + if type == 'user': + href = '<a href=%s>%s (%s)</a>' % (url, name, id) + elif type == 'ip': + href = '<a href=%s>%s</a>' % (url, name) + else: + href = '<a href=%s>%s (id:%s)</a>' % (url, name, id) + return href + + +def _filter_public_ip_log(qs): + network_ids = Network.objects.filter(public=True).values('id') + return qs.filter(network_id__in=network_ids) + + +def filter_public_ip_log(assoc): + if assoc.type == 'ip_log': + assoc.qs = _filter_public_ip_log(assoc.qs) + assoc.total = assoc.count_total() + + +def filter_distinct(assoc): + if hasattr(assoc, 'qs'): + assoc.qs = assoc.qs.distinct() + + +def exclude_deleted(assoc): + """Exclude deleted items.""" + if (admin_settings.ADMIN_SHOW_DELETED_ASSOCIATED_ITEMS or + not hasattr(assoc, 'qs')): + return + + if assoc.type in ['vm', 'volume', 'network', 'ip']: + assoc.qs = assoc.qs.exclude(deleted=True) + elif assoc.type == 'nic': + assoc.qs = assoc.qs.exclude(machine__deleted=True) + + assoc.excluded = assoc.total - assoc.qs.count() + + +def order_by_newest(assoc): + ord = getattr(assoc, 'order_by', None) + if ord: + assoc.qs = assoc.qs.order_by(ord) + + +def limit_shown(assoc): + limit = admin_settings.ADMIN_LIMIT_ASSOCIATED_ITEMS_PER_CATEGORY + assoc.items = assoc.items[:limit] + assoc.showing = assoc.count_items() + + +def customize_details_context(context): + """Perform generic customizations on the detail context.""" + for assoc in context['associations_list']: + filter_public_ip_log(assoc) + filter_distinct(assoc) + exclude_deleted(assoc) + order_by_newest(assoc) + limit_shown(assoc) diff --git a/snf-admin-app/synnefo_admin/admin/views.py b/snf-admin-app/synnefo_admin/admin/views.py new file mode 100644 index 0000000000000000000000000000000000000000..57fc2a02e8b6013ae01a1df6bd03a0962eaf6618 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin/views.py @@ -0,0 +1,362 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import logging +import json +from importlib import import_module + +from django.views.generic.simple import direct_to_template +from django.conf import settings +from django.core.exceptions import PermissionDenied +from django.http import Http404, HttpResponseRedirect, HttpResponse +from django.views.decorators.csrf import csrf_exempt +from django.core.serializers.json import DjangoJSONEncoder + +from urllib import unquote + +import astakosclient +from snf_django.lib import astakos +from synnefo_branding.utils import render_to_string +from snf_django.lib.api import faults + + +from astakos.im.messages import PLAIN_EMAIL_SUBJECT as sample_subject +from astakos.im import settings as astakos_settings +from astakos.admin import stats as astakos_stats +from synnefo.admin import stats as cyclades_stats + +from synnefo_admin.admin.exceptions import AdminHttp404, AdminHttp405 +from synnefo_admin import admin_settings + +from synnefo_admin.admin import exceptions +from synnefo_admin.admin.utils import (conditionally_gzip_page, + customize_details_context, admin_log, + default_view) + +from synnefo.ui.views import UI_MEDIA_URL + +JSON_MIMETYPE = "application/json" + +logger = logging.getLogger(__name__) + + +# Helper functions ### + + +def get_view_module(view_type): + """Import module for model view. + + We will import only modules for views that are specified in the ADMIN_VIEWS + setting. + """ + if view_type in admin_settings.ADMIN_VIEWS: + # The modules will not be loaded per-call but only once. + return import_module('synnefo_admin.admin.resources.%ss.views' % view_type) + return None + + +def get_view_module_or_404(view_type): + """Try to import a view module or raise 404.""" + if not view_type: + raise AdminHttp404("No category provided.") + + mod = get_view_module(view_type) + if not mod: + raise AdminHttp404("No category found with this name: %s" % view_type) + return mod + + +def get_json_view_or_404(view_type): + """Try to import a json view or raise 404.""" + mod = get_view_module_or_404(view_type) + # We expect that the module has a JSON_CLASS attribute with the appropriate + # subclass of django-eztable's DatatablesView. + return mod.JSON_CLASS.as_view() + + +def get_token_from_cookie(request, cookiename): + """Extract token from provided cookie. + + Extract token from the cookie name provided. Cookie should be in the same + form as astakos service sets its cookie contents:: + + <user_uniq>|<user_token> + """ + try: + cookie_content = unquote(request.COOKIES.get(cookiename, None)) + return cookie_content.split("|")[1] + except AttributeError: + pass + + return None + + +# Security functions ### + + +def admin_user_required(func, permitted_groups=admin_settings.\ + ADMIN_PERMITTED_GROUPS): + """ + Django view wrapper that checks if identified request user has admin + permissions (exists in admin group) + """ + def wrapper(request, *args, **kwargs): + if not admin_settings.ADMIN_ENABLED: + # we must never raise an AdminHttp404 exception here. + raise Http404 + + token = get_token_from_cookie(request, admin_settings.AUTH_COOKIE_NAME) + astakos.get_user(request, settings.ASTAKOS_AUTH_URL, + fallback_token=token, logger=logger) + if hasattr(request, 'user') and request.user: + groups = request.user['access']['user']['roles'] + groups = [g["name"] for g in groups] + + if not set(groups) & set(permitted_groups): + logger.debug("Failed to access admin view %r. No valid admin " + "group (%r) matches user groups (%r)", + request.user_uniq, permitted_groups, groups) + raise PermissionDenied + else: + logger.debug("Failed to access admin view %r. No authenticated " + "user found.", request.user_uniq) + logger.debug("auth_url (%s)", settings.ASTAKOS_AUTH_URL) + raise PermissionDenied + + logging.debug("User %s accessed admininterface view (%s)", + request.user_uniq, request.path) + return func(request, *args, **kwargs) + + return wrapper + + +# View functions ### + +default_dict = { + 'ADMIN_MEDIA_URL': admin_settings.ADMIN_MEDIA_URL, + 'UI_MEDIA_URL': UI_MEDIA_URL, + 'mail': { + 'sender': astakos_settings.SERVER_EMAIL, + 'subject': sample_subject, + 'body': render_to_string('im/plain_email.txt', { + 'baseurl': astakos_settings.BASE_URL, + 'site_name': astakos_settings.SITENAME, + 'support': astakos_settings.CONTACT_EMAIL}).replace('\n\n\n', '\n'), + 'legend': { + 'Full name': "{{ full_name }}", + 'First name': "{{ first_name }}", + 'Last name': "{{ last_name }}", + 'Email': "{{ email }}", + } + }, + 'views': admin_settings.ADMIN_VIEWS, +} + + +@admin_user_required +def logout(request): + """Logout view.""" + admin_log(request) + auth_token = request.user['access']['token']['id'] + ac = astakosclient.AstakosClient(auth_token, settings.ASTAKOS_AUTH_URL, + retry=2, use_pool=True, logger=logger) + logout_url = ac.ui_url + '/logout' + + return HttpResponseRedirect(logout_url) + + +@admin_user_required +def home(request): + """Home view.""" + admin_log(request) + return direct_to_template(request, "admin/home.html", + extra_context=default_dict) + + +@admin_user_required +def stats(request): + """Stats view.""" + admin_log(request) + return direct_to_template(request, "admin/stats.html", + extra_context=default_dict) + + +@admin_user_required +def charts(request): + """Charts view.""" + admin_log(request) + return direct_to_template(request, "admin/charts.html", + extra_context=default_dict) + + +@admin_user_required +def stats_component(request, component): + """Mirror public stats view for cyclades/astakos. + + This stats view will import the get_public_stats function of + cyclades/astakos and return its results to the caller. + """ + admin_log(request, component=component) + data = {} + status = 200 + if component == 'astakos': + data = astakos_stats.get_public_stats() + elif component == 'cyclades': + data = cyclades_stats.get_public_stats() + else: + status = 404 + return HttpResponse(json.dumps(data, cls=DjangoJSONEncoder), + mimetype=JSON_MIMETYPE, status=status) + + +@admin_user_required +def stats_component_details(request, component): + """Mirror detailed stats view for cyclades/astakos. + + This stats view will import the get_astakos/cyclades_stats function and + return its results to the caller. + """ + admin_log(request, component=component) + data = {} + status = 200 + if component == 'astakos': + data = astakos_stats.get_astakos_stats() + elif component == 'cyclades': + data = cyclades_stats.get_cyclades_stats() + else: + status = 404 + return HttpResponse(json.dumps(data, cls=DjangoJSONEncoder), + mimetype=JSON_MIMETYPE, status=status) + + +@admin_user_required +@conditionally_gzip_page +def json_list(request, type): + """Return a class-based view based on the given type.""" + admin_log(request, type=type) + + content_types = request.META.get("HTTP_ACCEPT", "") + if "application/json" not in content_types: + raise AdminHttp405("""\ +The JSON content of this page is for internal use. +You cannot view it on your browser.""") + view = get_json_view_or_404(type) + return view(request) + + +@admin_user_required +def details(request, type, id): + """Admin-Interface generic details view.""" + admin_log(request, type=type, id=id) + + mod = get_view_module_or_404(type) + context = mod.details(request, id) + customize_details_context(context) + context.update(default_dict) + context.update({'view_type': 'details'}) + + template = mod.templates['details'] + return direct_to_template(request, template, extra_context=context) + + +@admin_user_required +def catalog(request, type=default_view()): + """Admin-Interface generic list view.""" + admin_log(request, type=type) + + mod = get_view_module_or_404(type) + context = mod.catalog(request) + context.update(default_dict) + context.update({'view_type': 'list'}) + + template = mod.templates['list'] + return direct_to_template(request, template, extra_context=context) + + +@csrf_exempt +@admin_user_required +def admin_actions(request): + """Entry-point for all admin actions. + + Expects a JSON with the following fields: <TODO> + """ + admin_log(request, json=request.REQUEST) + status = 200 + response = { + 'result': "All actions finished successfully.", + 'error_ids': [], + } + + if request.method != "POST": + status = 405 + response['result'] = "Only POST is allowed." + + objs = json.loads(request.body) + request.POST = objs + + target = objs['target'] + op = objs['op'] + ids = objs['ids'] + if type(ids) is not list: + ids = ids.replace('[', '').replace(']', '').replace(' ', '').split(',') + + try: + mod = get_view_module_or_404(target) + except Http404: + status = 404 + response['result'] = "You have requested an unknown operation." + + for id in ids: + try: + mod.do_action(request, op, id) + except faults.BadRequest as e: + status = 400 + response['result'] = e.message + response['error_ids'].append(id) + except exceptions.AdminActionNotPermitted: + status = 403 + response['result'] = "You are not allowed to do this operation." + response['error_ids'].append(id) + except faults.NotAllowed: + status = 403 + response['result'] = "You are not allowed to do this operation." + response['error_ids'].append(id) + except exceptions.AdminActionUnknown: + status = 404 + response['result'] = "You have requested an unknown operation." + break + except exceptions.AdminActionNotImplemented: + status = 501 + response['result'] = "You have requested an unimplemented action." + break + except exceptions.AdminActionCannotApply: + status = 400 + response['result'] = """ + You have requested an action that cannot apply to a target. + """ + response['error_ids'].append(id) + except Exception as e: + logging.exception("Uncaught exception") + status = 500 + response['result'] = e.message + response['error_ids'].append(id) + + if hasattr(mod, 'wait_action'): + wait_ids = set(ids) - set(response['error_ids']) + for id in wait_ids: + mod.wait_action(request, op, id) + + return HttpResponse(json.dumps(response, cls=DjangoJSONEncoder), + mimetype=JSON_MIMETYPE, status=status) diff --git a/snf-admin-app/synnefo_admin/admin_settings.py b/snf-admin-app/synnefo_admin/admin_settings.py new file mode 100644 index 0000000000000000000000000000000000000000..7658de48670a09d12e0385e2c95481ef87831195 --- /dev/null +++ b/snf-admin-app/synnefo_admin/admin_settings.py @@ -0,0 +1,144 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +""" +Settings for the snf-admin-app. +""" + +# -------------------------------------------------------------------- +# Process Admin settings + +from django.conf import settings +from synnefo.lib.dict import SnfOrderedDict +from synnefo.lib import parse_base_url + +ADMIN_ENABLED = getattr(settings, 'ADMIN_ENABLED', True) + +BASE_URL = getattr(settings, 'ADMIN_BASE_URL', + 'https://admin.example.synnefo.org/admin/') +BASE_HOST, BASE_PATH = parse_base_url(BASE_URL) + +ADMIN_MEDIA_URL = getattr(settings, 'ADMIN_MEDIA_URL', + settings.MEDIA_URL + 'admin/') + +AUTH_COOKIE_NAME = getattr(settings, 'ADMIN_AUTH_COOKIE_NAME', + getattr(settings, 'UI_AUTH_COOKIE_NAME', + '_pithos2_a')) + +# A dictionary with the enabled admin model views. +DEFAULT_ADMIN_VIEWS = { + 'user': {'label': 'Users'}, + 'vm': {'label': 'VMs'}, + 'volume': {'label': 'Volumes'}, + 'network': {'label': 'Networks'}, + 'ip': {'label': 'IPs'}, + 'ip_log': {'label': 'IP History'}, + 'project': {'label': 'Projects'}, + 'group': {'label': 'User Groups'}, + #'auth_provider': {'label': 'User Auth Providers'}, +} +# A list with the appropriate appearance order of the above views in the UI. +DEFAULT_ADMIN_VIEWS_ORDER = ['user', 'vm', 'volume', 'network', 'ip', 'ip_log', + 'project', 'group'] + +ADMIN_VIEWS = getattr(settings, 'ADMIN_VIEWS', DEFAULT_ADMIN_VIEWS) +ADMIN_VIEWS_ORDER = getattr(settings, 'ADMIN_VIEWS_ORDER', + DEFAULT_ADMIN_VIEWS_ORDER) + + +# Combine the above settings into one ordered dictionary. +# Note: View names that don't exist in the ADMIN_VIEWS settings will silently +# be ignored. +ADMIN_VIEWS = SnfOrderedDict(ADMIN_VIEWS, ADMIN_VIEWS_ORDER, strict=False) + +# Check if the user has defined his/her own values for the following three +# groups. +ADMIN_READONLY_GROUP = getattr(settings, 'ADMIN_READONLY_GROUP', + 'admin-readonly') +ADMIN_HELPDESK_GROUP = getattr(settings, 'ADMIN_HELPDESK_GROUP', 'helpdesk') +ADMIN_GROUP = getattr(settings, 'ADMIN_GROUP', 'admin') + +# The user can either use the above three groups to control who has access +# to the admin tool, or define its own. +DEFAULT_ADMIN_PERMITTED_GROUPS = [ADMIN_READONLY_GROUP, ADMIN_HELPDESK_GROUP, + ADMIN_GROUP] +ADMIN_PERMITTED_GROUPS = getattr(settings, 'ADMIN_PERMITTED_GROUPS', + DEFAULT_ADMIN_PERMITTED_GROUPS) + +# The user can either use our RBAC definition, which uses the above 3 groups +# (readonly, helpdesk, admin), or define its own. +DEFAULT_ADMIN_RBAC = { + 'user': { + 'activate': [ADMIN_GROUP], + 'deactivate': [ADMIN_GROUP], + 'accept': [ADMIN_GROUP], + 'reject': [ADMIN_GROUP], + 'verify': [ADMIN_GROUP], + 'resend_verification': [ADMIN_GROUP], + 'contact': [ADMIN_HELPDESK_GROUP, ADMIN_GROUP], + }, 'vm': { + 'start': [ADMIN_HELPDESK_GROUP, ADMIN_GROUP], + 'shutdown': [ADMIN_HELPDESK_GROUP, ADMIN_GROUP], + 'reboot': [ADMIN_HELPDESK_GROUP, ADMIN_GROUP], + 'destroy': [ADMIN_GROUP], + 'suspend': [ADMIN_HELPDESK_GROUP, ADMIN_GROUP], + 'unsuspend': [ADMIN_HELPDESK_GROUP, ADMIN_GROUP], + 'contact': [ADMIN_HELPDESK_GROUP, ADMIN_GROUP], + }, 'volume': { + 'contact': [ADMIN_HELPDESK_GROUP, ADMIN_GROUP], + }, 'network': { + 'drain': [ADMIN_GROUP], + 'undrain': [ADMIN_GROUP], + 'destroy': [ADMIN_GROUP], + 'contact': [ADMIN_HELPDESK_GROUP, ADMIN_GROUP], + }, 'ip': { + 'destroy': [ADMIN_GROUP], + 'contact': [ADMIN_HELPDESK_GROUP, ADMIN_GROUP], + }, 'project': { + 'approve': [ADMIN_GROUP], + 'deny': [ADMIN_GROUP], + 'suspend': [ADMIN_GROUP], + 'unsuspend': [ADMIN_GROUP], + 'terminate': [ADMIN_GROUP], + 'reinstate': [ADMIN_GROUP], + 'contact': [ADMIN_GROUP], + }, +} +ADMIN_RBAC = getattr(settings, 'ADMIN_RBAC', DEFAULT_ADMIN_RBAC) + +# With this option, the admin can define whether to show deleted items on the +# details page of another item. Note that the details page of the deleted +# item will be shown properly. +ADMIN_SHOW_DELETED_ASSOCIATED_ITEMS = getattr( + settings, 'ADMIN_SHOW_DELETED_ASSOCIATED_ITEMS', False) + +# With this option, the admin can define whether to show users that do not +# belong to an object, in the details of that object. +ADMIN_SHOW_ONLY_ACTIVE_PROJECT_MEMBERS = getattr( + settings, 'ADMIN_SHOW_ONLY_ACTIVE_PROJECT_MEMBERS', True) + +# With this option, the admin can define the number of associated items that +# will be shown for each category, so as not to flood the page. +ADMIN_LIMIT_ASSOCIATED_ITEMS_PER_CATEGORY = getattr( + settings, 'ADMIN_LIMIT_ASSOCIATED_ITEMS_PER_CATEGORY', 50) + +# With this option, the admin can define the number of suspended VMs of a user +# that will be shown in his/her table summary. +ADMIN_LIMIT_SUSPENDED_VMS_IN_SUMMARY = getattr( + settings, 'ADMIN_LIMIT_SUSPENDED_VMS_IN_SUMMARY', 10) + +# The sign that will indicate that a filter term concerns a model field. +ADMIN_FIELD_SIGN = getattr(settings, 'ADMIN_FIELD_SIGN', '=') diff --git a/snf-admin-app/synnefo_admin/app_settings/__init__.py b/snf-admin-app/synnefo_admin/app_settings/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a1a672c1dc68a25295c19e51722d7919eab36a33 --- /dev/null +++ b/snf-admin-app/synnefo_admin/app_settings/__init__.py @@ -0,0 +1,23 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +""" +Django settings metadata. To be used in setup.py snf-webproject entry points. +""" + +installed_apps = ['eztables', 'django_filters', 'synnefo_admin.admin'] +static_files = {'synnefo_admin': 'admin/static'} +middleware_classes = ['synnefo_admin.admin.middleware.AdminMiddleware'] diff --git a/snf-admin-app/synnefo_admin/app_settings/default/__init__.py b/snf-admin-app/synnefo_admin/app_settings/default/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..eb2130c4c6db11b641932e44a129286e91c9e34b --- /dev/null +++ b/snf-admin-app/synnefo_admin/app_settings/default/__init__.py @@ -0,0 +1,14 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. diff --git a/snf-admin-app/synnefo_admin/conf/20-snf-admin-app-general.conf b/snf-admin-app/synnefo_admin/conf/20-snf-admin-app-general.conf new file mode 100644 index 0000000000000000000000000000000000000000..5b3c78bd5b290e8025ac039c7e47d4ff2353897e --- /dev/null +++ b/snf-admin-app/synnefo_admin/conf/20-snf-admin-app-general.conf @@ -0,0 +1,98 @@ +## Boolean option to enable or disable admin +#ADMIN_ENABLED = True + +## Top-level URL for deployment. +#ADMIN_BASE_URL = "https://host:port/admin" + +## If not set, defaults to MEDIA_URL + 'admin/'. Uncomment it only when you're +## sure. +#ADMIN_MEDIA_URL = "" +#ADMIN_AUTH_COOKIE_NAME = '_pithos2_a' + +## A dictionary with the enabled admin model views. +#ADMIN_VIEWS = { +# 'user': {'label': 'Users'}, +# 'vm': {'label': 'VMs'}, +# 'volume': {'label': 'Volumes'}, +# 'network': {'label': 'Networks'}, +# 'ip': {'label': 'IPs'}, +# 'ip_log': {'label': 'IP History'}, +# 'project': {'label': 'Projects'}, +# 'group': {'label': 'User Groups'}, +# #'auth_provider': {'label': 'User Auth Providers'}, +#} +## A list with the appropriate appearance order of the above views in the UI. +## Note: View names that don't exist in the ADMIN_VIEWS settings will silently +## be ignored. +#ADMIN_VIEWS_ORDER = ['user', 'vm', 'volume', 'network', 'ip', 'ip_log', +# 'project', 'group'] + +## Groups that will have access to the admin panel. +## There are three pre-defined categories (groups) and a list of permitted +## groups that includes them. Users that belong to any of the following groups +## simply have access to Admin. In order to do actions, the ADMIN_RBAC setting +## must also be tweaked. +## For more info, please consult the Admin Guide. +#ADMIN_READONLY_GROUP = 'admin-readonly' +#ADMIN_HELPDESK_GROUP = 'helpdesk' +#ADMIN_GROUP = 'admin' +#ADMIN_PERMITTED_GROUPS = [ADMIN_READONLY_GROUP, ADMIN_HELPDESK_GROUP, +# ADMIN_GROUP] + +## Define which groups will have access to the actions of the admin panel. +#ADMIN_RBAC = { +# 'user': { +# 'activate': [ADMIN_GROUP], +# 'deactivate': [ADMIN_GROUP], +# 'accept': [ADMIN_GROUP], +# 'reject': [ADMIN_GROUP], +# 'verify': [ADMIN_GROUP], +# 'resend_verification': [ADMIN_GROUP], +# 'contact': [ADMIN_HELPDESK_GROUP, ADMIN_GROUP], +# }, 'vm': { +# 'start': [ADMIN_HELPDESK_GROUP, ADMIN_GROUP], +# 'shutdown': [ADMIN_HELPDESK_GROUP, ADMIN_GROUP], +# 'reboot': [ADMIN_HELPDESK_GROUP, ADMIN_GROUP], +# 'destroy': [ADMIN_GROUP], +# 'suspend': [ADMIN_HELPDESK_GROUP, ADMIN_GROUP], +# 'unsuspend': [ADMIN_HELPDESK_GROUP, ADMIN_GROUP], +# 'contact': [ADMIN_HELPDESK_GROUP, ADMIN_GROUP], +# }, 'volume': { +# 'contact': [ADMIN_HELPDESK_GROUP, ADMIN_GROUP], +# }, 'network': { +# 'drain': [ADMIN_GROUP], +# 'undrain': [ADMIN_GROUP], +# 'destroy': [ADMIN_GROUP], +# 'contact': [ADMIN_HELPDESK_GROUP, ADMIN_GROUP], +# }, 'ip': { +# 'destroy': [ADMIN_GROUP], +# 'contact': [ADMIN_HELPDESK_GROUP, ADMIN_GROUP], +# }, 'project': { +# 'approve': [ADMIN_GROUP], +# 'deny': [ADMIN_GROUP], +# 'suspend': [ADMIN_GROUP], +# 'unsuspend': [ADMIN_GROUP], +# 'terminate': [ADMIN_GROUP], +# 'reinstate': [ADMIN_GROUP], +# 'contact': [ADMIN_GROUP], +# }, +#} + +## Option to show deleted items on the details page of another item. Note that +## the details page of the deleted item will be shown properly. +#ADMIN_SHOW_DELETED_ASSOCIATED_ITEMS = False + +## Option to show only active project members in the details page of that +## project. +#ADMIN_SHOW_ONLY_ACTIVE_PROJECT_MEMBERS = True + +## Number of associated items that will be shown for each category, so as not +## to flood the page. +#ADMIN_LIMIT_ASSOCIATED_ITEMS_PER_CATEGORY = 50 + +## Number of suspended VMs of a user that will be shown in his/her table +## summary. +#ADMIN_LIMIT_SUSPENDED_VMS_IN_SUMMARY = 10 + +## The sign that will indicate that a filter term concerns a model field. +#ADMIN_FIELD_SIGN = '=' diff --git a/snf-admin-app/synnefo_admin/urls.py b/snf-admin-app/synnefo_admin/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..defc9946130f9985f014755b60f242e4283353ac --- /dev/null +++ b/snf-admin-app/synnefo_admin/urls.py @@ -0,0 +1,31 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +""" +This file can be used as ROOT_URLCONF for Admin-only installations. The Admin +path (BASE_PATH) is constructed from the ADMIN_BASE_URL setting. +""" + +from django.conf.urls import patterns, include +from snf_django.utils.urls import extend_path_with_slash +from snf_django.lib.api.utils import prefix_pattern +from synnefo_admin.admin_settings import BASE_PATH + +urlpatterns = patterns( + '', + (prefix_pattern(BASE_PATH), include('synnefo_admin.admin.urls')), +) + +extend_path_with_slash(urlpatterns, BASE_PATH) diff --git a/snf-astakos-app/MANIFEST.in b/snf-astakos-app/MANIFEST.in index dfe81b52b6005f8ecd4d429b5152f1508ee35784..fde33e3a5657b96ed87e75da51c89b52bcc08203 100644 --- a/snf-astakos-app/MANIFEST.in +++ b/snf-astakos-app/MANIFEST.in @@ -1,8 +1,8 @@ -include distribute_setup.py +include distribute_setup.py README.md global-include */templates/* */static/* global-exclude */.DS_Store include astakos/settings.d/* -recursive-include astakos/im/templates/ *.html *.txt -recursive-include astakos/im/static/ *.js *.css *.less *.html *.txt *.png *.htc +recursive-include astakos/im/templates *.html *.txt +recursive-include astakos/im/static *.js *.css *.less *.html *.txt *.png *.htc prune docs prune other diff --git a/snf-astakos-app/README.md b/snf-astakos-app/README.md new file mode 100644 index 0000000000000000000000000000000000000000..68f20f68d21f6cc8f5be9939735e034dcfcc9aa7 --- /dev/null +++ b/snf-astakos-app/README.md @@ -0,0 +1,27 @@ +snf-astakos-app +=============== + +Overview +-------- + +This is Synnefo's snf-astakos-app component. Please see the [official Synnefo +site](http://www.synnefo.org) for more information. + + +Copyright and license +===================== + +Copyright (C) 2010-2014 GRNET S.A. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. diff --git a/snf-astakos-app/astakos/admin/admin_urls.py b/snf-astakos-app/astakos/admin/admin_urls.py index f18ce7661c67ee6e86df76aab27bdfa5936461b5..c1cc937750f47bb9b9bb43a451dd07127b649bbb 100644 --- a/snf-astakos-app/astakos/admin/admin_urls.py +++ b/snf-astakos-app/astakos/admin/admin_urls.py @@ -1,40 +1,22 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. -from django.conf.urls import url, patterns +from django.conf.urls import url, patterns, include +from django.http import Http404 from astakos.admin import views -from django.http import Http404 def index(request): diff --git a/snf-astakos-app/astakos/admin/stats.py b/snf-astakos-app/astakos/admin/stats.py index e269dbffe3364ac8c3ccba274e6b516a4dbf9d23..a9245ffb59b1ed60fd5180509f9d47f94810b951 100644 --- a/snf-astakos-app/astakos/admin/stats.py +++ b/snf-astakos-app/astakos/admin/stats.py @@ -1,39 +1,21 @@ -# Copyright 2014 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import datetime -from django.conf import settings from django.db.models import Sum, Count +from astakos.im import settings from astakos.im.models import AstakosUser, Resource from astakos.quotaholder_app.models import Holding @@ -58,19 +40,26 @@ def get_astakos_stats(): stats["users"]["all"] = {"total": users.count(), "verified": verified.count(), "active": active.count()} + # Get all holdings from DB. Filter with 'source=None' in order to get + # only the (base and user) projects, and not the user per project holdings + holdings = Holding.objects.filter(source=None)\ + .values("resource")\ + .annotate(usage_sum=Sum("usage_max"), + limit_sum=Sum("limit")) + holdings = dict([(h["resource"], h) for h in holdings]) + resources_stats = {} for resource in Resource.objects.all(): - info = Holding.objects\ - .filter(resource=resource.name)\ - .aggregate(usage_sum=Sum("usage_max"), - limit_sum=Sum("limit")) - resources_stats[resource.name] = {"used": info["usage_sum"] or 0, - "allocated": info["limit_sum"] or 0, - "unit": resource.unit, - "description": resource.desc} + res_holdings = holdings.get(resource.name, {}) + resources_stats[resource.name] = { + "used": res_holdings.get("usage_sum") or 0, + "allocated": res_holdings.get("limit_sum") or 0, + "unit": resource.unit, + "description": resource.desc + } stats["resources"]["all"] = resources_stats - for provider in settings.ASTAKOS_IM_MODULES: + for provider in settings.IM_MODULES: # Add provider stats["providers"].append(provider) @@ -91,6 +80,8 @@ def get_astakos_stats(): # Add stats about resources users_uuids = exclusive.values_list("uuid", flat=True) + # The 'holder' attribute contains user UUIDs prefixed with 'user:' + users_uuids = ["user:" + uuid for uuid in users_uuids] resources_stats = {} for resource in Resource.objects.all(): info = Holding.objects\ @@ -99,8 +90,8 @@ def get_astakos_stats(): .aggregate(usage_sum=Sum("usage_max"), limit_sum=Sum("limit")) resources_stats[resource.name] = { - "used": info["usage_sum"] or 0, - "allocated": info["limit_sum"] or 0, + "used": info.get("usage_sum") or 0, + "allocated": info.get("limit_sum") or 0, "unit": resource.unit, "description": resource.desc} stats["resources"][provider] = resources_stats diff --git a/snf-astakos-app/astakos/admin/views.py b/snf-astakos-app/astakos/admin/views.py index f307ac41ed81d296890c227b0a467615df4ee44e..e9e9f451ff95630dd02dfcc9645c66332f2b7fb7 100644 --- a/snf-astakos-app/astakos/admin/views.py +++ b/snf-astakos-app/astakos/admin/views.py @@ -1,51 +1,42 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import logging + +from functools import wraps + from django import http from django.utils import simplejson as json +from django.forms.models import model_to_dict +from django.core.validators import validate_email, ValidationError + from snf_django.lib import api -from astakos.im import settings -from synnefo.lib.services import get_path +from snf_django.lib.api import faults +from astakos.im import settings from astakos.admin import stats +from astakos.im.models import AstakosUser, get_latest_terms +from astakos.im.auth import make_local_user logger = logging.getLogger(__name__) -PERMITTED_GROUPS = settings.ADMIN_STATS_PERMITTED_GROUPS +STATS_PERMITTED_GROUPS = settings.ADMIN_STATS_PERMITTED_GROUPS + try: - AUTH_URL = get_path(settings.astakos_services, - "astakos_identity.endpoints")[0]["publicURL"] + AUTH_URL = settings.astakos_services \ + ["astakos_identity"]["endpoints"][0]["publicURL"] except (IndexError, KeyError) as e: logger.error("Failed to load Astakos Auth URL: %s", e) AUTH_URL = None @@ -63,9 +54,10 @@ def get_public_stats(request): @api.api_method(http_method='GET', user_required=True, token_required=True, astakos_auth_url=AUTH_URL, logger=logger, serializations=['json']) -@api.user_in_groups(permitted_groups=PERMITTED_GROUPS, +@api.user_in_groups(permitted_groups=STATS_PERMITTED_GROUPS, logger=logger) def get_astakos_stats(request): _stats = stats.get_astakos_stats() data = json.dumps(_stats) return http.HttpResponse(data, status=200, content_type='application/json') + diff --git a/snf-astakos-app/astakos/api/keystone_urls.py b/snf-astakos-app/astakos/api/keystone_urls.py index 73aa9af6c74ea54146c9a09e082fb717ca4aba76..ad042a1e0c369fc3bc287f40991cc27347382592 100644 --- a/snf-astakos-app/astakos/api/keystone_urls.py +++ b/snf-astakos-app/astakos/api/keystone_urls.py @@ -1,40 +1,35 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.conf.urls import patterns, url from snf_django.lib.api import api_endpoint_not_found +from snf_django.lib.api.urls import api_patterns +from astakos.im import settings + +urlpatterns = patterns('') + +if settings.ADMIN_API_ENABLED: + urlpatterns += api_patterns( + 'astakos.api.user', + (r'^v2.0/users(?:/|.json|.xml)?$', 'users_demux'), + (r'^v2.0/users/detail(?:.json|.xml)?$', 'users_list', {'detail': True}), + (r'^v2.0/users/([-\w]+)(?:/|.json|.xml)?$', 'user_demux'), + (r'^v2.0/users/([-\w]+)/action(?:/|.json|.xml)?$', 'user_action') + ) -urlpatterns = patterns( +urlpatterns += patterns( 'astakos.api.tokens', url(r'^v2.0/tokens/(?P<token_id>.+?)/?$', 'validate_token', name='validate_token'), diff --git a/snf-astakos-app/astakos/api/projects.py b/snf-astakos-app/astakos/api/projects.py index 45b99acdf9e1cb8f99476bc5262dae95b2595920..1c3f86c651677a0403f96ef8f053e6ad915aeb31 100644 --- a/snf-astakos-app/astakos/api/projects.py +++ b/snf-astakos-app/astakos/api/projects.py @@ -1,54 +1,39 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import operator -import re from django.utils import simplejson as json from django.views.decorators.csrf import csrf_exempt from django.http import HttpResponse from django.db.models import Q -from django.db import transaction +from astakos.im import transaction from astakos.api.util import json_response from snf_django.lib import api from snf_django.lib.api import faults -from .util import user_from_token, invert_dict, read_json_body +from snf_django.lib.api import utils +from .util import user_from_token, invert_dict, check_is_dict from astakos.im import functions from astakos.im.models import ( AstakosUser, Project, ProjectApplication, ProjectMembership, - ProjectResourceGrant, ProjectLog, ProjectMembershipLog) -import synnefo.util.date as date_util + ProjectResourceQuota, ProjectResourceGrant, ProjectLog, + ProjectMembershipLog) +from synnefo.util import units MEMBERSHIP_POLICY_SHOW = { @@ -69,13 +54,11 @@ APPLICATION_STATE_SHOW = { } PROJECT_STATE_SHOW = { - Project.O_PENDING: "pending", - Project.O_ACTIVE: "active", - Project.O_DENIED: "denied", - Project.O_DISMISSED: "dismissed", - Project.O_CANCELLED: "cancelled", - Project.O_SUSPENDED: "suspended", - Project.O_TERMINATED: "terminated", + Project.UNINITIALIZED: "uninitialized", + Project.NORMAL: "active", + Project.SUSPENDED: "suspended", + Project.TERMINATED: "terminated", + Project.DELETED: "deleted", } PROJECT_STATE = invert_dict(PROJECT_STATE_SHOW) @@ -91,8 +74,7 @@ MEMBERSHIP_STATE_SHOW = { } -def _application_details(application, all_grants): - grants = all_grants.get(application.id, []) +def _grant_details(grants): resources = {} for grant in grants: if not grant.resource.api_visible: @@ -101,71 +83,75 @@ def _application_details(application, all_grants): "member_capacity": grant.member_capacity, "project_capacity": grant.project_capacity, } + return resources - join_policy = MEMBERSHIP_POLICY_SHOW[application.member_join_policy] - leave_policy = MEMBERSHIP_POLICY_SHOW[application.member_leave_policy] + +def _application_details(application, all_grants): + grants = all_grants.get(application.id, []) + resources = _grant_details(grants) + join_policy = MEMBERSHIP_POLICY_SHOW.get(application.member_join_policy) + leave_policy = MEMBERSHIP_POLICY_SHOW.get(application.member_leave_policy) d = { + "id": application.id, + "state": APPLICATION_STATE_SHOW[application.state], "name": application.name, - "owner": application.owner.uuid, + "owner": application.owner.uuid if application.owner else None, "applicant": application.applicant.uuid, "homepage": application.homepage, "description": application.description, "start_date": application.start_date, "end_date": application.end_date, + "comments": application.comments, "join_policy": join_policy, "leave_policy": leave_policy, "max_members": application.limit_on_members_number, + "private": application.private, "resources": resources, } return d -def get_applications_details(applications): - grants = ProjectResourceGrant.objects.grants_per_app(applications) - - l = [] - for application in applications: - d = { - "id": application.id, - "project": application.chain_id, - "state": APPLICATION_STATE_SHOW[application.state], - "comments": application.comments, - } - d.update(_application_details(application, grants)) - l.append(d) - return l - - -def get_application_details(application): - return get_applications_details([application])[0] - - def get_projects_details(projects, request_user=None): - pendings = ProjectApplication.objects.pending_per_project(projects) - applications = [p.application for p in projects] - grants = ProjectResourceGrant.objects.grants_per_app(applications) + applications = [p.last_application for p in projects if p.last_application] + proj_quotas = ProjectResourceQuota.objects.quotas_per_project(projects) + app_grants = ProjectResourceGrant.objects.grants_per_app(applications) deactivations = ProjectLog.objects.last_deactivations(projects) l = [] for project in projects: - application = project.application + join_policy = MEMBERSHIP_POLICY_SHOW[project.member_join_policy] + leave_policy = MEMBERSHIP_POLICY_SHOW[project.member_leave_policy] + quotas = proj_quotas.get(project.id, []) + resources = _grant_details(quotas) + d = { - "id": project.id, - "application": application.id, - "state": PROJECT_STATE_SHOW[project.overall_state()], + "id": project.uuid, + "state": PROJECT_STATE_SHOW[project.state], "creation_date": project.creation_date, - } + "name": project.realname, + "owner": project.owner.uuid if project.owner else None, + "homepage": project.homepage, + "description": project.description, + "end_date": project.end_date, + "join_policy": join_policy, + "leave_policy": leave_policy, + "max_members": project.limit_on_members_number, + "private": project.private, + "system_project": project.is_base, + "resources": resources, + } + check = functions.project_check_allowed if check(project, request_user, level=functions.APPLICANT_LEVEL, silent=True): - d["comments"] = application.comments - pending = pendings.get(project.id) - d["pending_application"] = pending.id if pending else None + application = project.last_application + if application: + d["last_application"] = _application_details( + application, app_grants) deact = deactivations.get(project.id) if deact is not None: d["deactivation_date"] = deact.date - d.update(_application_details(application, grants)) l.append(d) return l @@ -189,7 +175,7 @@ def get_memberships_details(memberships, request_user): d = { "id": membership.id, "user": membership.person.uuid, - "project": membership.project_id, + "project": membership.project.uuid, "state": MEMBERSHIP_STATE_SHOW[membership.state], "allowed_actions": allowed_actions, } @@ -219,13 +205,13 @@ def _get_project_state(val): def _project_state_query(val): if isinstance(val, list): states = [_get_project_state(v) for v in val] - return Project.o_states_q(states) - return Project.o_state_q(_get_project_state(val)) + return Q(state__in=states) + return Q(state=_get_project_state(val)) PROJECT_QUERY = { - "name": _query("application__name"), - "owner": _query("application__owner__uuid"), + "name": _query("realname"), + "owner": _query("owner__uuid"), "state": _project_state_query, } @@ -276,27 +262,46 @@ def projects(request): @transaction.commit_on_success def get_projects(request): user = request.user - input_data = read_json_body(request, default={}) - filters = input_data.get("filter", {}) + filters = {} + for key in PROJECT_QUERY.keys(): + value = request.GET.get(key) + if value is not None: + filters[key] = value + mode = request.GET.get("mode", "default") query = make_project_query(filters) - projects = _get_projects(query, request_user=user) + projects = _get_projects(query, mode=mode, request_user=user) data = get_projects_details(projects, request_user=user) return json_response(data) -def _get_projects(query, request_user=None): +def _get_projects(query, mode="default", request_user=None): projects = Project.objects.filter(query) - if not request_user.is_project_admin(): + filters = [Q()] + if mode == "member": + membs = request_user.projectmembership_set.\ + actually_accepted_and_active() + memb_projects = membs.values_list("project", flat=True) + is_memb = Q(id__in=memb_projects) + filters.append(is_memb) + elif mode in ["related", "default"]: membs = request_user.projectmembership_set.any_accepted() memb_projects = membs.values_list("project", flat=True) is_memb = Q(id__in=memb_projects) - owned = (Q(application__owner=request_user) | - Q(application__applicant=request_user)) - active = Project.o_state_q(Project.O_ACTIVE) - projects = projects.filter(is_memb | owned | active) - return projects.select_related( - "application", "application__owner", "application__applicant") + owned = Q(owner=request_user) + if not request_user.is_project_admin(): + filters.append(is_memb) + filters.append(owned) + elif mode in ["active", "default"]: + active = (Q(state=Project.NORMAL) & Q(private=False)) + if not request_user.is_project_admin(): + filters.append(active) + else: + raise faults.BadRequest("Unrecognized mode '%s'." % mode) + + q = reduce(operator.or_, filters) + projects = projects.filter(q) + return projects.select_related("last_application") @api.api_method(http_method="POST", token_required=True, user_required=False) @@ -304,9 +309,8 @@ def _get_projects(query, request_user=None): @transaction.commit_on_success def create_project(request): user = request.user - data = request.body - app_data = json.loads(data) - return submit_application(app_data, user, project_id=None) + app_data = utils.get_json_body(request) + return submit_new_project(app_data, user) @csrf_exempt @@ -314,9 +318,9 @@ def project(request, project_id): method = request.method if method == "GET": return get_project(request, project_id) - if method == "POST": + if method == "PUT": return modify_project(request, project_id) - return api.api_method_not_allowed(request, allowed_methods=['GET', 'POST']) + return api.api_method_not_allowed(request, allowed_methods=['GET', 'PUT']) @api.api_method(http_method="GET", token_required=True, user_required=False) @@ -331,50 +335,47 @@ def get_project(request, project_id): def _get_project(project_id, request_user=None): - project = functions.get_project_by_id(project_id) + project = functions.get_project_by_uuid(project_id) functions.project_check_allowed( project, request_user, level=functions.ANY_LEVEL) return project -@api.api_method(http_method="POST", token_required=True, user_required=False) +@api.api_method(http_method="PUT", token_required=True, user_required=False) @user_from_token @transaction.commit_on_success def modify_project(request, project_id): user = request.user - data = request.body - app_data = json.loads(data) - return submit_application(app_data, user, project_id=project_id) - - -def _get_date(d, key): - date_str = d.get(key) - if date_str is not None: - try: - return date_util.isoparse(date_str) - except: - raise faults.BadRequest("Invalid %s" % key) - else: - return None + app_data = utils.get_json_body(request) + return submit_modification(app_data, user, project_id=project_id) -def _get_maybe_string(d, key): +def _get_maybe_string(d, key, default=None): value = d.get(key) if value is not None and not isinstance(value, basestring): raise faults.BadRequest("%s must be string" % key) + if value is None: + return default return value -DOMAIN_VALUE_REGEX = re.compile( - r'^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}$', - re.IGNORECASE) +def _get_maybe_boolean(d, key, default=None): + value = d.get(key) + if value is not None and not isinstance(value, bool): + raise faults.BadRequest("%s must be boolean" % key) + if value is None: + return default + return value -def valid_project_name(name): - return DOMAIN_VALUE_REGEX.match(name) is not None +def _parse_max_members(s): + try: + return units.parse(s) + except units.ParseError: + raise faults.BadRequest("Invalid max_members") -def submit_application(app_data, user, project_id=None): +def submit_new_project(app_data, user): uuid = app_data.get("owner") if uuid is None: owner = user @@ -389,9 +390,6 @@ def submit_application(app_data, user, project_id=None): except KeyError: raise faults.BadRequest("Name missing.") - if not valid_project_name(name): - raise faults.BadRequest("Project name should be in domain format") - join_policy = app_data.get("join_policy", "moderated") try: join_policy = MEMBERSHIP_POLICY[join_policy] @@ -404,16 +402,79 @@ def submit_application(app_data, user, project_id=None): except KeyError: raise faults.BadRequest("Invalid leave policy") - start_date = _get_date(app_data, "start_date") - end_date = _get_date(app_data, "end_date") + start_date = app_data.get("start_date") + end_date = app_data.get("end_date") if end_date is None: raise faults.BadRequest("Missing end date") + try: + max_members = _parse_max_members(app_data["max_members"]) + except KeyError: + max_members = units.PRACTICALLY_INFINITE + + private = bool(_get_maybe_boolean(app_data, "private")) + homepage = _get_maybe_string(app_data, "homepage", "") + description = _get_maybe_string(app_data, "description", "") + comments = _get_maybe_string(app_data, "comments", "") + resources = app_data.get("resources", {}) + + submit = functions.submit_application + with ExceptionHandler(): + application = submit( + owner=owner, + name=name, + project_id=None, + homepage=homepage, + description=description, + start_date=start_date, + end_date=end_date, + member_join_policy=join_policy, + member_leave_policy=leave_policy, + limit_on_members_number=max_members, + private=private, + comments=comments, + resources=resources, + request_user=user) + + result = {"application": application.id, + "id": application.chain.uuid, + } + return json_response(result, status_code=201) + + +def submit_modification(app_data, user, project_id): + owner = app_data.get("owner") + if owner is not None: + try: + owner = AstakosUser.objects.accepted().get(uuid=owner) + except AstakosUser.DoesNotExist: + raise faults.BadRequest("User does not exist.") + + name = app_data.get("name") + + join_policy = app_data.get("join_policy") + if join_policy is not None: + try: + join_policy = MEMBERSHIP_POLICY[join_policy] + except KeyError: + raise faults.BadRequest("Invalid join policy") + + leave_policy = app_data.get("leave_policy") + if leave_policy is not None: + try: + leave_policy = MEMBERSHIP_POLICY[leave_policy] + except KeyError: + raise faults.BadRequest("Invalid leave policy") + + start_date = app_data.get("start_date") + end_date = app_data.get("end_date") + max_members = app_data.get("max_members") - if not isinstance(max_members, (int, long)) or max_members < 0: - raise faults.BadRequest("Invalid max_members") + if max_members is not None: + max_members = _parse_max_members(max_members) + private = _get_maybe_boolean(app_data, "private") homepage = _get_maybe_string(app_data, "homepage") description = _get_maybe_string(app_data, "description") comments = _get_maybe_string(app_data, "comments") @@ -432,12 +493,13 @@ def submit_application(app_data, user, project_id=None): member_join_policy=join_policy, member_leave_policy=leave_policy, limit_on_members_number=max_members, + private=private, comments=comments, resources=resources, request_user=user) result = {"application": application.id, - "id": application.chain_id + "id": application.chain.uuid, } return json_response(result, status_code=201) @@ -445,6 +507,7 @@ def submit_application(app_data, user, project_id=None): def get_action(actions, input_data): action = None data = None + check_is_dict(input_data) for option in actions.keys(): if option in input_data: if action: @@ -465,100 +528,34 @@ PROJECT_ACTION = { } -@csrf_exempt -@api.api_method(http_method="POST", token_required=True, user_required=False) -@user_from_token -@transaction.commit_on_success -def project_action(request, project_id): - user = request.user - data = request.body - input_data = json.loads(data) - - func, action_data = get_action(PROJECT_ACTION, input_data) - with ExceptionHandler(): - func(project_id, request_user=user, reason=action_data) - return HttpResponse() - - -@csrf_exempt -def applications(request): - method = request.method - if method == "GET": - return get_applications(request) - return api.api_method_not_allowed(request, allowed_methods=['GET']) - - -def make_application_query(input_data): - project_id = input_data.get("project") - if project_id is not None: - if not isinstance(project_id, (int, long)): - raise faults.BadRequest("'project' must be integer") - return Q(chain=project_id) - return Q() - - -@api.api_method(http_method="GET", token_required=True, user_required=False) -@user_from_token -@transaction.commit_on_success -def get_applications(request): - user = request.user - input_data = read_json_body(request, default={}) - query = make_application_query(input_data) - apps = _get_applications(query, request_user=user) - data = get_applications_details(apps) - return json_response(data) - - -def _get_applications(query, request_user=None): - apps = ProjectApplication.objects.filter(query) - - if not request_user.is_project_admin(): - owned = (Q(owner=request_user) | - Q(applicant=request_user)) - apps = apps.filter(owned) - return apps.select_related() - - -@csrf_exempt -@api.api_method(http_method="GET", token_required=True, user_required=False) -@user_from_token -@transaction.commit_on_success -def application(request, app_id): - user = request.user - with ExceptionHandler(): - application = _get_application(app_id, user) - data = get_application_details(application) - return json_response(data) - - -def _get_application(app_id, request_user=None): - application = functions.get_application(app_id) - functions.app_check_allowed( - application, request_user, level=functions.APPLICANT_LEVEL) - return application - - APPLICATION_ACTION = { "approve": functions.approve_application, - "deny": functions.deny_application, + "deny": functions.deny_application, "dismiss": functions.dismiss_application, - "cancel": functions.cancel_application, + "cancel": functions.cancel_application, } +PROJECT_ACTION.update(APPLICATION_ACTION) +APP_ACTION_FUNCS = APPLICATION_ACTION.values() + + @csrf_exempt @api.api_method(http_method="POST", token_required=True, user_required=False) @user_from_token @transaction.commit_on_success -def application_action(request, app_id): +def project_action(request, project_id): user = request.user - data = request.body - input_data = json.loads(data) + input_data = utils.get_json_body(request) - func, action_data = get_action(APPLICATION_ACTION, input_data) + func, action_data = get_action(PROJECT_ACTION, input_data) with ExceptionHandler(): - func(app_id, request_user=user, reason=action_data) - + kwargs = {"request_user": user, + "reason": action_data.get("reason", ""), + } + if func in APP_ACTION_FUNCS: + kwargs["application_id"] = action_data["app_id"] + func(project_id=project_id, **kwargs) return HttpResponse() @@ -575,9 +572,7 @@ def memberships(request): def make_membership_query(input_data): project_id = input_data.get("project") if project_id is not None: - if not isinstance(project_id, (int, long)): - raise faults.BadRequest("'project' must be integer") - return Q(project=project_id) + return Q(project__uuid=project_id) return Q() @@ -586,8 +581,7 @@ def make_membership_query(input_data): @transaction.commit_on_success def get_memberships(request): user = request.user - input_data = read_json_body(request, default={}) - query = make_membership_query(input_data) + query = make_membership_query(request.GET) memberships = _get_memberships(query, request_user=user) data = get_memberships_details(memberships, user) return json_response(data) @@ -596,14 +590,12 @@ def get_memberships(request): def _get_memberships(query, request_user=None): memberships = ProjectMembership.objects if not request_user.is_project_admin(): - owned = Q(project__application__owner=request_user) + owned = Q(project__owner=request_user) memb = Q(person=request_user) memberships = memberships.filter(owned | memb) return memberships.select_related( - "project", "project__application", - "project__application__owner", "project__application__applicant", - "person").filter(query) + "project", "project__owner", "person").filter(query) def join_project(data, request_user): @@ -674,7 +666,7 @@ MEMBERSHIP_ACTION = { @transaction.commit_on_success def membership_action(request, memb_id): user = request.user - input_data = read_json_body(request, default={}) + input_data = utils.get_json_body(request) func, action_data = get_action(MEMBERSHIP_ACTION, input_data) with ExceptionHandler(): func(memb_id, user, reason=action_data) diff --git a/snf-astakos-app/astakos/api/quotas.py b/snf-astakos-app/astakos/api/quotas.py index 0e2f019a2bf57dda0a5dff445b5a93c64ce6e58a..6555305fa4b118b82e9a210766d5f523585165d9 100644 --- a/snf-astakos-app/astakos/api/quotas.py +++ b/snf-astakos-app/astakos/api/quotas.py @@ -1,53 +1,36 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. -from django.utils import simplejson as json from django.views.decorators.csrf import csrf_exempt from django.http import HttpResponse -from django.db import transaction +from astakos.im import transaction from snf_django.lib import api from snf_django.lib.api.faults import BadRequest, ItemNotFound +from snf_django.lib.api import utils from django.core.cache import cache from astakos.im import settings from astakos.im import register -from astakos.im.quotas import get_user_quotas, service_get_quotas +from astakos.im.quotas import get_user_quotas, service_get_quotas, \ + service_get_project_quotas, project_ref import astakos.quotaholder_app.exception as qh_exception import astakos.quotaholder_app.callpoint as qh -from .util import (json_response, is_integer, are_integer, +from .util import (json_response, is_integer, are_integer, check_is_dict, user_from_token, component_from_token) @@ -65,19 +48,39 @@ def get_visible_resources(): def quotas(request): visible_resources = get_visible_resources() resource_names = [r.name for r in visible_resources] - result = get_user_quotas(request.user, resources=resource_names) + memberships = request.user.projectmembership_set.actually_accepted() + sources = [project_ref(m.project.uuid) for m in memberships] + result = get_user_quotas(request.user, resources=resource_names, + sources=sources) return json_response(result) @api.api_method(http_method='GET', token_required=True, user_required=False) @component_from_token def service_quotas(request): - user = request.GET.get('user') - users = [user] if user is not None else None - result = service_get_quotas(request.component_instance, users=users) + userstr = request.GET.get('user') + users = userstr.split(",") if userstr is not None else None + projectstr = request.GET.get('project') + projects = projectstr.split(",") if projectstr is not None else None + result = service_get_quotas(request.component_instance, users=users, + sources=projects) + + if userstr is not None and result == {}: + raise ItemNotFound("No user with UUID '%s'" % userstr) + + return json_response(result) + - if user is not None and result == {}: - raise ItemNotFound("No such user '%s'" % user) +@api.api_method(http_method='GET', token_required=True, user_required=False) +@component_from_token +def service_project_quotas(request): + projectstr = request.GET.get('project') + projects = projectstr.split(',') if projectstr is not None else None + result = service_get_project_quotas(request.component_instance, + projects=projects) + + if projectstr is not None and result == {}: + raise ItemNotFound("No project with UUID '%s'" % projectstr) return json_response(result) @@ -102,7 +105,7 @@ def commissions(request): @api.api_method(http_method='GET', token_required=True, user_required=False) @component_from_token def get_pending_commissions(request): - client_key = str(request.component_instance) + client_key = unicode(request.component_instance) result = qh.get_pending_commissions(clientkey=client_key) return json_response(result) @@ -121,7 +124,7 @@ def _provisions_to_list(provisions): if not is_integer(quantity): raise ValueError() except (TypeError, KeyError, ValueError): - raise BadRequest("Malformed provision %s" % str(provision)) + raise BadRequest("Malformed provision %s" % unicode(provision)) return lst @@ -129,13 +132,10 @@ def _provisions_to_list(provisions): @api.api_method(http_method='POST', token_required=True, user_required=False) @component_from_token def issue_commission(request): - data = request.body - try: - input_data = json.loads(data) - except json.JSONDecodeError: - raise BadRequest("POST data should be in json format.") + input_data = utils.get_json_body(request) + check_is_dict(input_data) - client_key = str(request.component_instance) + client_key = unicode(request.component_instance) provisions = input_data.get('provisions') if provisions is None: raise BadRequest("Provisions are missing.") @@ -219,13 +219,10 @@ def conflictingCF(serial): @component_from_token @transaction.commit_on_success def resolve_pending_commissions(request): - data = request.body - try: - input_data = json.loads(data) - except json.JSONDecodeError: - raise BadRequest("POST data should be in json format.") + input_data = utils.get_json_body(request) + check_is_dict(input_data) - client_key = str(request.component_instance) + client_key = unicode(request.component_instance) accept = input_data.get('accept', []) reject = input_data.get('reject', []) @@ -255,7 +252,7 @@ def resolve_pending_commissions(request): @component_from_token def get_commission(request, serial): data = request.GET - client_key = str(request.component_instance) + client_key = unicode(request.component_instance) try: serial = int(serial) except ValueError: @@ -275,18 +272,15 @@ def get_commission(request, serial): @component_from_token @transaction.commit_on_success def serial_action(request, serial): - data = request.body - try: - input_data = json.loads(data) - except json.JSONDecodeError: - raise BadRequest("POST data should be in json format.") + input_data = utils.get_json_body(request) + check_is_dict(input_data) try: serial = int(serial) except ValueError: raise BadRequest("Serial should be an integer.") - client_key = str(request.component_instance) + client_key = unicode(request.component_instance) accept = 'accept' in input_data reject = 'reject' in input_data diff --git a/snf-astakos-app/astakos/api/service.py b/snf-astakos-app/astakos/api/service.py index d71b2108dee946e02bc478413aea1f24ec07f6a1..5efdaba79ad8a56a6d87e750697dc3b87f6c9c9e 100644 --- a/snf-astakos-app/astakos/api/service.py +++ b/snf-astakos-app/astakos/api/service.py @@ -1,35 +1,17 @@ -# Copyright 2011-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.views.decorators.csrf import csrf_exempt diff --git a/snf-astakos-app/astakos/api/services.py b/snf-astakos-app/astakos/api/services.py index 38a2b7ae6accfecdf6cb0b3f6716c055a9fdd136..a47717a4f746011ad9ce1efdac837658c7f90355 100644 --- a/snf-astakos-app/astakos/api/services.py +++ b/snf-astakos-app/astakos/api/services.py @@ -1,35 +1,17 @@ -# Copyright (C) 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. astakos_services = { diff --git a/snf-astakos-app/astakos/api/tokens.py b/snf-astakos-app/astakos/api/tokens.py index 8afb3eae7bea37cafd6a827bc3652778862697ea..8393dd9c78dfd777463976392271c288cae65396 100644 --- a/snf-astakos-app/astakos/api/tokens.py +++ b/snf-astakos-app/astakos/api/tokens.py @@ -1,35 +1,17 @@ -# Copyright 2011-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from collections import defaultdict @@ -91,7 +73,7 @@ def authenticate(request): d = defaultdict(dict) if not public_mode: - req = utils.get_request_dict(request) + req = utils.get_json_body(request) uuid = None try: diff --git a/snf-astakos-app/astakos/api/urls.py b/snf-astakos-app/astakos/api/urls.py index ac7e44fb2bd8a3f5de08b9ea532bfe2b6d66ae05..95d3028c65fe1fda7b048cc32d78ecadcc1d4561 100644 --- a/snf-astakos-app/astakos/api/urls.py +++ b/snf-astakos-app/astakos/api/urls.py @@ -1,37 +1,20 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.conf.urls import patterns, url, include + from snf_django.lib.api import api_endpoint_not_found @@ -39,6 +22,7 @@ astakos_account_v1_0 = patterns( 'astakos.api.quotas', url(r'^quotas/?$', 'quotas', name="astakos-api-quotas"), url(r'^service_quotas/?$', 'service_quotas'), + url(r'^service_project_quotas/?$', 'service_project_quotas'), url(r'^resources/?$', 'resources'), url(r'^commissions/?$', 'commissions'), url(r'^commissions/action/?$', 'resolve_pending_commissions'), @@ -60,19 +44,14 @@ astakos_account_v1_0 += patterns( astakos_account_v1_0 += patterns( 'astakos.api.projects', url(r'^projects/?$', 'projects', name='api_projects'), - url(r'^projects/(?P<project_id>\d+)/?$', 'project', name='api_project'), - url(r'^projects/(?P<project_id>\d+)/action/?$', 'project_action', - name='api_project_action'), - url(r'^projects/apps/?$', 'applications', name='api_applications'), - url(r'^projects/apps/(?P<app_id>\d+)/?$', 'application', - name='api_application'), - url(r'^projects/apps/(?P<app_id>\d+)/action/?$', 'application_action', - name='api_application_action'), url(r'^projects/memberships/?$', 'memberships', name='api_memberships'), url(r'^projects/memberships/(?P<memb_id>\d+)/?$', 'membership', name='api_membership'), url(r'^projects/memberships/(?P<memb_id>\d+)/action/?$', 'membership_action', name='api_membership_action'), + url(r'^projects/(?P<project_id>[^/]+)/?$', 'project', name='api_project'), + url(r'^projects/(?P<project_id>[^/]+)/action/?$', 'project_action', + name='api_project_action'), ) urlpatterns = patterns( diff --git a/snf-astakos-app/astakos/api/user.py b/snf-astakos-app/astakos/api/user.py index f91aea0893cbffc88d68ff502107401ed6af8809..4b5a18deef6c98821f2272bb3fc2ffc3de8d5c65 100644 --- a/snf-astakos-app/astakos/api/user.py +++ b/snf-astakos-app/astakos/api/user.py @@ -1,47 +1,48 @@ -# Copyright 2011-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import logging + +from functools import wraps, partial from django.views.decorators.csrf import csrf_exempt +from django import http +from astakos.im import transaction +from django.utils import simplejson as json +from django.forms.models import model_to_dict +from django.core.validators import validate_email, ValidationError + from snf_django.lib import api +from snf_django.lib.api import faults from .util import ( get_uuid_displayname_catalogs as get_uuid_displayname_catalogs_util, send_feedback as send_feedback_util, user_from_token) -import logging -logger = logging.getLogger(__name__) +from astakos.im import settings +from astakos.admin import stats +from astakos.im.models import AstakosUser, get_latest_terms +from astakos.im.auth import make_local_user +from astakos.im import activation_backends + +ADMIN_GROUPS = settings.ADMIN_API_PERMITTED_GROUPS +activation_backend = activation_backends.get_backend() + +logger = logging.getLogger(__name__) @csrf_exempt @api.api_method(http_method="POST", token_required=True, user_required=False, @@ -67,3 +68,282 @@ def send_feedback(request, email_template_name='im/feedback_mail.txt'): # unauthorised (401) return send_feedback_util(request, email_template_name) + + +# API ADMIN UTILS AND ENDPOINTS + +def user_api_method(http_method): + """ + Common decorator for user admin api views. + """ + def wrapper(func): + @api.api_method(http_method=http_method, user_required=True, + token_required=True, logger=logger, + serializations=['json']) + @api.user_in_groups(permitted_groups=ADMIN_GROUPS, + logger=logger) + @wraps(func) + def method(*args, **kwargs): + return func(*args, **kwargs) + + return method + return wrapper + + +def user_to_dict(user, detail=True): + user_fields = ['first_name', 'last_name', 'email'] + date_fields = ['date_joined', 'moderated_at', 'verified_at', + 'auth_token_expires'] + status_fields = ['is_active', 'is_rejected', 'deactivated_reason', + 'accepted_policy', 'rejected_reason'] + if not detail: + fields = user_fields + date_fields = [] + d = model_to_dict(user, fields=user_fields + status_fields) + d['id'] = user.uuid + for date_field in date_fields: + val = getattr(user, date_field) + if val: + d[date_field] = api.utils.isoformat(getattr(user, date_field)) + else: + d[date_field] = None + + methods = d['authentication_methods'] = [] + d['roles'] = list(user.groups.values_list("name", flat=True)) + + for provider in user.auth_providers.filter(): + method_fields = ['identifier', 'active', 'affiliation'] + method = model_to_dict(provider, fields=method_fields) + method['backend'] = provider.auth_backend + method['metadata'] = provider.info + if provider.auth_backend == 'astakos': + method['identifier'] = user.email + methods.append(method) + + return d + + +def users_demux(request): + if request.method == 'GET': + return users_list(request) + elif request.method == 'POST': + return users_create(request) + else: + return api.api_method_not_allowed(request) + + +def user_demux(request, user_id): + if request.method == 'GET': + return user_detail(request, user_id) + elif request.method == 'PUT': + return user_update(request, user_id) + else: + return api.api_method_not_allowed(request) + + +@user_api_method('GET') +def users_list(request, action='list', detail=False): + logger.debug('users_list detail=%s', detail) + users = AstakosUser.objects.filter() + dict_func = partial(user_to_dict, detail=detail) + users_dicts = map(dict_func, users) + data = json.dumps({'users': users_dicts}) + return http.HttpResponse(data, status=200, + content_type='application/json') + + +@user_api_method('POST') +@transaction.commit_on_success +def users_create(request): + user_id = request.user_uniq + req = api.utils.get_json_body(request) + logger.info('users_create: %s request: %s', user_id, req) + + user_data = req.get('user', {}) + email = user_data.get('username', None) + + first_name = user_data.get('first_name', None) + last_name = user_data.get('last_name', None) + affiliation = user_data.get('affiliation', None) + password = user_data.get('password', None) + metadata = user_data.get('metadata', {}) + + password_gen = AstakosUser.objects.make_random_password + if not password: + password = password_gen() + + try: + validate_email(email) + except ValidationError: + raise faults.BadRequest("Invalid username (email format required)") + + if AstakosUser.objects.verified_user_exists(email): + raise faults.Conflict("User '%s' already exists" % email) + + if not first_name: + raise faults.BadRequest("Invalid first_name") + + if not last_name: + raise faults.BadRequest("Invalid last_name") + + has_signed_terms = not(get_latest_terms()) + + try: + user = make_local_user(email, first_name=first_name, + last_name=last_name, password=password, + has_signed_terms=has_signed_terms) + if metadata: + # we expect a unique local auth provider for the user + provider = user.auth_providers.get() + provider.info = metadata + provider.affiliation = affiliation + provider.save() + + user = AstakosUser.objects.get(pk=user.pk) + code = user.verification_code + ver_res = activation_backend.handle_verification(user, code) + if ver_res.is_error(): + raise Exception(ver_res.message) + mod_res = activation_backend.handle_moderation(user, accept=True) + if mod_res.is_error(): + raise Exception(ver_res.message) + + except Exception, e: + raise faults.BadRequest(e.message) + + user_data = { + 'id': user.uuid, + 'password': password, + 'auth_token': user.auth_token, + } + data = json.dumps({'user': user_data}) + return http.HttpResponse(data, status=200, content_type='application/json') + + +@user_api_method('POST') +@transaction.commit_on_success +def user_action(request, user_id): + admin_id = request.user_uniq + req = api.utils.get_json_body(request) + logger.info('user_action: %s user: %s request: %s', admin_id, user_id, req) + if 'activate' in req: + try: + user = AstakosUser.objects.get(uuid=user_id) + except AstakosUser.DoesNotExist: + raise faults.ItemNotFound("User not found") + + activation_backend.activate_user(user) + + user = AstakosUser.objects.get(uuid=user_id) + user_data = { + 'id': user.uuid, + 'is_active': user.is_active + } + data = json.dumps({'user': user_data}) + return http.HttpResponse(data, status=200, + content_type='application/json') + if 'deactivate' in req: + try: + user = AstakosUser.objects.get(uuid=user_id) + except AstakosUser.DoesNotExist: + raise faults.ItemNotFound("User not found") + + activation_backend.deactivate_user( + user, reason=req['deactivate'].get('reason', None)) + + user = AstakosUser.objects.get(uuid=user_id) + user_data = { + 'id': user.uuid, + 'is_active': user.is_active + } + data = json.dumps({'user': user_data}) + return http.HttpResponse(data, status=200, + content_type='application/json') + + if 'renewToken' in req: + try: + user = AstakosUser.objects.get(uuid=user_id) + except AstakosUser.DoesNotExist: + raise faults.ItemNotFound("User not found") + user.renew_token() + user.save() + user_data = { + 'id': user.uuid, + 'auth_token': user.auth_token, + } + data = json.dumps({'user': user_data}) + return http.HttpResponse(data, status=200, + content_type='application/json') + + raise faults.BadRequest("Invalid action") + + +@user_api_method('PUT') +@transaction.commit_on_success +def user_update(request, user_id): + admin_id = request.user_uniq + req = api.utils.get_json_body(request) + logger.info('user_update: %s user: %s request: %s', admin_id, user_id, req) + + user_data = req.get('user', {}) + + try: + user = AstakosUser.objects.get(uuid=user_id) + except AstakosUser.DoesNotExist: + raise faults.ItemNotFound("User not found") + + email = user_data.get('username', None) + first_name = user_data.get('first_name', None) + last_name = user_data.get('last_name', None) + affiliation = user_data.get('affiliation', None) + password = user_data.get('password', None) + metadata = user_data.get('metadata', {}) + + if 'password' in user_data: + user.set_password(password) + + if 'username' in user_data: + try: + validate_email(email) + except ValidationError: + raise faults.BadRequest("Invalid username (email format required)") + + if AstakosUser.objects.verified_user_exists(email): + raise faults.Conflict("User '%s' already exists" % email) + + user.email = email + + if 'first_name' in user_data: + user.first_name = first_name + + if 'last_name' in user_data: + user.last_name = last_name + + try: + user.save() + if 'metadata' in user_data: + provider = user.auth_providers.get(auth_backend="astakos") + provider.info = metadata + if affiliation in user_data: + provider.affiliation = affiliation + provider.save() + + except Exception, e: + raise faults.BadRequest(e.message) + + data = json.dumps({'user': user_to_dict(user)}) + return http.HttpResponse(data, status=200, content_type='application/json') + + +@user_api_method('GET') +def user_detail(request, user_id): + admin_id = request.user_uniq + logger.info('user_detail: %s user: %s', admin_id, user_id) + try: + user = AstakosUser.objects.get(uuid=user_id) + except AstakosUser.DoesNotExist: + raise faults.ItemNotFound("User not found") + + user_data = user_to_dict(user, detail=True) + data = json.dumps({'user': user_data}) + return http.HttpResponse(data, status=200, content_type='application/json') diff --git a/snf-astakos-app/astakos/api/util.py b/snf-astakos-app/astakos/api/util.py index 8b8188211225aa318d3dc64bf287f6befebe2022..35110d8da576a882d394b62d6254a31a9b07798a 100644 --- a/snf-astakos-app/astakos/api/util.py +++ b/snf-astakos-app/astakos/api/util.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from functools import wraps from time import time, mktime @@ -44,7 +26,7 @@ from snf_django.lib.api import faults from snf_django.lib.api.utils import isoformat from astakos.im.forms import FeedbackForm -from astakos.im.functions import send_feedback as send_feedback_func +from astakos.im.user_utils import send_feedback as send_feedback_func import logging logger = logging.getLogger(__name__) @@ -81,16 +63,9 @@ def xml_response(content, template, status_code=None): return response -def read_json_body(request, default=None): - body = request.body - if not body and request.method == "GET": - body = request.GET.get("body") - if not body: - return default - try: - return json.loads(body) - except json.JSONDecodeError: - raise faults.BadRequest("Request body should be in json format.") +def check_is_dict(obj): + if not isinstance(obj, dict): + raise faults.BadRequest("Request should be a JSON dict") def is_integer(x): diff --git a/snf-astakos-app/astakos/im/activation_backends.py b/snf-astakos-app/astakos/im/activation_backends.py index 5a7535f8c3d9973cb4cf17bfdab41424b228e2db..51bbc43abf920581451967ec36d95ed340ca18a6 100644 --- a/snf-astakos-app/astakos/im/activation_backends.py +++ b/snf-astakos-app/astakos/im/activation_backends.py @@ -1,46 +1,29 @@ -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.utils.importlib import import_module from django.core.exceptions import ImproperlyConfigured from django.utils.translation import ugettext as _ +from snf_django.lib.api import faults + from astakos.im import models from astakos.im import functions from astakos.im import settings from astakos.im import forms - -from astakos.im.quotas import qh_sync_new_user +from astakos.im import user_utils import astakos.im.messages as astakos_messages @@ -85,20 +68,20 @@ class ActivationBackend(object): ActivationBackend handles user verification/activation. Example usage:: - >>> # it is wise to not instantiate a backend class directly but use - >>> # get_backend method instead. - >>> backend = get_backend() - >>> formCls = backend.get_signup_form(request.POST) - >>> if form.is_valid(): - >>> user = form.create_user() - >>> activation = backend.handle_registration(user) - >>> # activation.status is one of backend.Result.{*} activation result - >>> # types - >>> - >>> # sending activation notifications is not done automatically - >>> # we need to call send_result_notifications - >>> backend.send_result_notifications(activation) - >>> return HttpResponse(activation.message) + #>>> # it is wise to not instantiate a backend class directly but use + #>>> # get_backend method instead. + #>>> backend = get_backend() + #>>> formCls = backend.get_signup_form(request.POST) + #>>> if form.is_valid(): + #>>> user = form.create_user() + #>>> activation = backend.handle_registration(user) + #>>> # activation.status is one of backend.Result.{*} activation result + #>>> # types + #>>> + #>>> # sending activation notifications is not done automatically + #>>> # we need to call send_result_notifications + #>>> backend.send_result_notifications(activation) + #>>> return HttpResponse(activation.message) """ verification_template_name = 'im/activation_email.txt' @@ -183,6 +166,96 @@ class ActivationBackend(object): return ActivationResult(self.Result.PENDING_VERIFICATION) + def validate_user_action(self, user, action, verification_code='', + silent=True): + """Check if an action can apply on a user. + + Arguments: + user: The target user. + action: The name of the action (in capital letters). + verification_code: Needed only in "VERIFY" action. + silent: If set to True, suppress exceptions. + + Returns: + A `(success, message)` tuple. `success` is a boolean value that + shows if the action can apply on a user, and `message` explains + why the action cannot apply on a user. + + If an action can apply on a user, this function will always return + `(True, None)`. + + Exceptions: + faults.NotAllowed: When the action cannot apply on a user. + faults.BadRequest: When the action is unknown/malformed. + """ + def fail(e=Exception, msg=""): + if silent: + return False, msg + else: + raise e(msg) + + if action == "VERIFY": + if user.email_verified: + msg = _(astakos_messages.ACCOUNT_ALREADY_VERIFIED) + return fail(faults.NotAllowed, msg) + if not (user.verification_code and + user.verification_code == verification_code): + msg = _(astakos_messages.VERIFICATION_FAILED) + return fail(faults.NotAllowed, msg) + + elif action == "ACCEPT": + if user.moderated and not user.is_rejected: + msg = _(astakos_messages.ACCOUNT_ALREADY_MODERATED) + return fail(faults.NotAllowed, msg) + if not user.email_verified: + msg = _(astakos_messages.ACCOUNT_NOT_VERIFIED) + return fail(faults.NotAllowed, msg) + + elif action == "ACTIVATE": + if not user.email_verified: + msg = _(astakos_messages.ACCOUNT_NOT_VERIFIED) + return fail(faults.NotAllowed, msg) + if not user.moderated: + msg = _(astakos_messages.ACCOUNT_NOT_MODERATED) + return fail(faults.NotAllowed, msg) + if user.is_rejected: + msg = _(astakos_messages.ACCOUNT_REJECTED) + return fail(faults.NotAllowed, msg) + if user.is_active: + msg = _(astakos_messages.ACCOUNT_ALREADY_ACTIVE) + return fail(faults.NotAllowed, msg) + + elif action == "DEACTIVATE": + if not user.email_verified: + msg = _(astakos_messages.ACCOUNT_NOT_VERIFIED) + return fail(faults.NotAllowed, msg) + if not user.moderated: + msg = _(astakos_messages.ACCOUNT_NOT_MODERATED) + return fail(faults.NotAllowed, msg) + if user.is_rejected: + msg = _(astakos_messages.ACCOUNT_REJECTED) + return fail(faults.NotAllowed, msg) + + elif action == "REJECT": + if user.moderated: + msg = _(astakos_messages.ACCOUNT_ALREADY_MODERATED) + return fail(faults.NotAllowed, msg) + if user.is_active: + msg = _(astakos_messages.ACCOUNT_ALREADY_ACTIVE) + return fail(faults.NotAllowed, msg) + if not user.email_verified: + msg = _(astakos_messages.ACCOUNT_NOT_VERIFIED) + return fail(faults.NotAllowed, msg) + + elif action == "SEND_VERIFICATION_MAIL": + if user.email_verified: + return fail(faults.NotAllowed) + + else: + return fail(faults.BadRequest, "Unknown action: {}.".format(action)) + + return True, None + def verify_user(self, user, verification_code): """ Process user verification using provided verification_code. This @@ -191,25 +264,22 @@ class ActivationBackend(object): """ logger.info("Verifying user: %s", user.log_display) - if user.email_verified: - logger.warning("User email already verified: %s", - user.log_display) - msg = astakos_messages.ACCOUNT_ALREADY_VERIFIED + ok, msg = self.validate_user_action(user, "VERIFY", verification_code) + if not ok: + if msg == _(astakos_messages.ACCOUNT_ALREADY_VERIFIED): + logger.warning("User email already verified: %s", + user.log_display) + elif msg == _(astakos_messages.VERIFICATION_FAILED): + logger.error("User email verification failed (invalid " + "verification code): %s", user.log_display) return ActivationResult(self.Result.ERROR, msg) - if user.verification_code and \ - user.verification_code == verification_code: - user.email_verified = True - user.verified_at = datetime.datetime.now() - # invalidate previous code - user.renew_verification_code() - user.save() - logger.info("User email verified: %s", user.log_display) - else: - logger.error("User email verification failed " - "(invalid verification code): %s", user.log_display) - msg = astakos_messages.VERIFICATION_FAILED - return ActivationResult(self.Result.ERROR, msg) + user.email_verified = True + user.verified_at = datetime.datetime.now() + # invalidate previous code + user.renew_verification_code() + user.save() + logger.info("User email verified: %s", user.log_display) if not self.moderation_enabled: logger.warning("User preaccepted (%s): %s", 'auto_moderation', @@ -232,20 +302,20 @@ class ActivationBackend(object): def accept_user(self, user, policy='manual'): logger.info("Moderating user: %s", user.log_display) - if user.moderated and user.is_active: - logger.warning("User already accepted, moderation" - " skipped: %s", user.log_display) - msg = _(astakos_messages.ACCOUNT_ALREADY_MODERATED) - return ActivationResult(self.Result.ERROR, msg) - if not user.email_verified: - logger.warning("Cannot accept unverified user: %s", - user.log_display) - msg = _(astakos_messages.ACCOUNT_NOT_VERIFIED) + ok, msg = self.validate_user_action(user, "ACCEPT") + if not ok: + if msg == _(astakos_messages.ACCOUNT_ALREADY_MODERATED): + logger.warning("User already accepted, moderation skipped: %s", + user.log_display) + elif msg == _(astakos_messages.ACCOUNT_NOT_VERIFIED): + logger.warning("Cannot accept unverified user: %s", + user.log_display) return ActivationResult(self.Result.ERROR, msg) # store a snapshot of user details by the time he # got accepted. + if not user.accepted_email: user.accepted_email = user.email user.accepted_policy = policy @@ -253,9 +323,9 @@ class ActivationBackend(object): user.moderated_at = datetime.datetime.now() user.moderated_data = json.dumps(user.__dict__, default=lambda obj: - str(obj)) + unicode(obj)) user.save() - qh_sync_new_user(user) + functions.enable_base_project(user) if user.is_rejected: logger.warning("User has previously been " @@ -270,20 +340,8 @@ class ActivationBackend(object): return ActivationResult(self.Result.ACCEPTED) def activate_user(self, user): - if not user.email_verified: - msg = _(astakos_messages.ACCOUNT_NOT_VERIFIED) - return ActivationResult(self.Result.ERROR, msg) - - if not user.moderated: - msg = _(astakos_messages.ACCOUNT_NOT_MODERATED) - return ActivationResult(self.Result.ERROR, msg) - - if user.is_rejected: - msg = _(astakos_messages.ACCOUNT_REJECTED) - return ActivationResult(self.Result.ERROR, msg) - - if user.is_active: - msg = _(astakos_messages.ACCOUNT_ALREADY_ACTIVE) + ok, msg = self.validate_user_action(user, "ACTIVATE") + if not ok: return ActivationResult(self.Result.ERROR, msg) user.is_active = True @@ -294,38 +352,37 @@ class ActivationBackend(object): return ActivationResult(self.Result.ACTIVATED) def deactivate_user(self, user, reason=''): - user.is_active = False - user.deactivated_reason = reason + ok, msg = self.validate_user_action(user, "DEACTIVATE") + if not ok: + return ActivationResult(self.Result.ERROR, msg) + if user.is_active: user.deactivated_at = datetime.datetime.now() + user.is_active = False + user.deactivated_reason = reason user.save() logger.info("User deactivated: %s", user.log_display) return ActivationResult(self.Result.DEACTIVATED) def reject_user(self, user, reason): logger.info("Rejecting user: %s", user.log_display) - if user.moderated: - logger.warning("User already moderated: %s", user.log_display) - msg = _(astakos_messages.ACCOUNT_ALREADY_MODERATED) - return ActivationResult(self.Result.ERROR, msg) - - if user.is_active: - logger.warning("Cannot reject unverified user: %s", - user.log_display) - msg = _(astakos_messages.ACCOUNT_NOT_VERIFIED) - return ActivationResult(self.Result.ERROR, msg) - - if not user.email_verified: - logger.warning("Cannot reject unverified user: %s", - user.log_display) - msg = _(astakos_messages.ACCOUNT_NOT_VERIFIED) + ok, msg = self.validate_user_action(user, "REJECT") + if not ok: + if msg == _(astakos_messages.ACCOUNT_ALREADY_MODERATED): + logger.warning("User already moderated: %s", user.log_display) + elif msg == _(astakos_messages.ACCOUNT_ALREADY_ACTIVE): + logger.warning("Cannot reject active user: %s", + user.log_display) + elif msg == _(astakos_messages.ACCOUNT_NOT_VERIFIED): + logger.warning("Cannot reject unverified user: %s", + user.log_display) return ActivationResult(self.Result.ERROR, msg) user.moderated = True user.moderated_at = datetime.datetime.now() user.moderated_data = json.dumps(user.__dict__, default=lambda obj: - str(obj)) + unicode(obj)) user.is_rejected = True user.rejected_reason = reason user.save() @@ -337,7 +394,7 @@ class ActivationBackend(object): return self.prepare_user(user, email_verified=email_verified) def handle_verification(self, user, activation_code): - logger.info("Handling user email verirfication: %s", user.log_display) + logger.info("Handling user email verification: %s", user.log_display) return self.verify_user(user, activation_code) def handle_moderation(self, user, accept=True, reject_reason=None): @@ -349,13 +406,14 @@ class ActivationBackend(object): return self.reject_user(user, reject_reason) def send_user_verification_email(self, user): - if user.is_active: - raise Exception("User already active") + ok, _ = self.validate_user_action(user, "SEND_VERIFICATION_MAIL") + if not ok: + raise Exception("User email already verified.") # invalidate previous code user.renew_verification_code() user.save() - functions.send_verification(user) + user_utils.send_verification(user) user.activation_sent = datetime.datetime.now() user.save() @@ -364,7 +422,7 @@ class ActivationBackend(object): Send corresponding notifications based on the status of activation result. - Result.PENDING_VERIRFICATION + Result.PENDING_VERIFICATION * Send user the email verification url Result.PENDING_MODERATION @@ -386,7 +444,7 @@ class ActivationBackend(object): if result.status == self.Result.PENDING_MODERATION: logger.info("Sending notifications for user" " verification: %s", user.log_display) - functions.send_account_pending_moderation_notification( + user_utils.send_account_pending_moderation_notification( user, self.pending_moderation_template_name) # TODO: notify user @@ -394,11 +452,11 @@ class ActivationBackend(object): if result.status == self.Result.ACCEPTED: logger.info("Sending notifications for user" " moderation: %s", user.log_display) - functions.send_account_activated_notification( + user_utils.send_account_activated_notification( user, self.activated_email_template_name) - functions.send_greeting(user, - self.greeting_template_name) + user_utils.send_greeting(user, + self.greeting_template_name) # TODO: notify admins if result.status == self.Result.REJECTED: diff --git a/snf-astakos-app/astakos/im/astakos_resources.py b/snf-astakos-app/astakos/im/astakos_resources.py index 7cf3f179e13d28c0378d338f1d069ec93f98202f..41597b1f6d3ca46d51e757bd15d2127949fd5a86 100644 --- a/snf-astakos-app/astakos/im/astakos_resources.py +++ b/snf-astakos-app/astakos/im/astakos_resources.py @@ -1,37 +1,18 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from astakos.im.settings import astakos_services -from synnefo.util.keypath import get_path -resources = get_path(astakos_services, 'astakos_account.resources').values() +resources = astakos_services['astakos_account']['resources'].values() diff --git a/snf-astakos-app/astakos/im/auth.py b/snf-astakos-app/astakos/im/auth.py index 8419b9cf295042145f238819e9db35072024b889..038932836caa6a1c880f85fa374b0b094bbdeab4 100644 --- a/snf-astakos-app/astakos/im/auth.py +++ b/snf-astakos-app/astakos/im/auth.py @@ -1,39 +1,21 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. -import uuid import datetime from astakos.im import models +from astakos.im import functions def _finalize_astakosuser_object(user, has_signed_terms=False): @@ -43,7 +25,7 @@ def _finalize_astakosuser_object(user, has_signed_terms=False): user.date_signed_terms = datetime.datetime.now() user.renew_verification_code() - user.uuid = str(uuid.uuid4()) + user.uuid = functions.new_uuid() user.renew_token() user.save() diff --git a/snf-astakos-app/astakos/im/auth_backends.py b/snf-astakos-app/astakos/im/auth_backends.py index 1409e12ef91a05979f8207f6cf1a4d186b32310e..4d0d494dcddcfc9ec32fb19c7c753fe1ffbc0afa 100644 --- a/snf-astakos-app/astakos/im/auth_backends.py +++ b/snf-astakos-app/astakos/im/auth_backends.py @@ -1,35 +1,17 @@ -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.contrib.auth.backends import ModelBackend diff --git a/snf-astakos-app/astakos/im/auth_providers.py b/snf-astakos-app/astakos/im/auth_providers.py index ac63b7525bbc689e4ff83c7fbe204ef889ec60a4..c2b4ae4bdd9dbec08c858e37dc84c346b794a5e7 100644 --- a/snf-astakos-app/astakos/im/auth_providers.py +++ b/snf-astakos-app/astakos/im/auth_providers.py @@ -1,42 +1,27 @@ -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import copy import json +from datetime import datetime + from synnefo.lib.ordereddict import OrderedDict from django.core.urlresolvers import reverse, NoReverseMatch +from django.utils.encoding import smart_unicode from django.utils.translation import ugettext as _ from django.contrib.auth.models import Group from django import template @@ -204,6 +189,14 @@ class AuthProvider(object): from astakos.im.models import AstakosUserAuthProvider as AuthProvider return AuthProvider + def update_last_login_at(self): + instance = self._instance + user = instance.user.__class__.objects.get(pk=instance.user.pk) + date = datetime.now() + instance.last_login_at = user.last_login = date + instance.save() + user.save() + def remove_from_user(self): if not self.get_remove_policy: raise Exception("Provider cannot be removed") @@ -252,6 +245,7 @@ class AuthProvider(object): user = pending._instance.user logger.info("Removing existing unverified user (%r)", user.log_display) + user.base_project and user.base_project.delete() user.delete() create_params = { @@ -318,7 +312,8 @@ class AuthProvider(object): if override_in_settings is not None: msg = override_in_settings try: - self.message_tpls_compiled[key] = msg.format(**params) + tpl = smart_unicode(msg) + self.message_tpls_compiled[key] = tpl.format(**params) params.update(self.message_tpls_compiled) except KeyError, e: continue diff --git a/snf-astakos-app/astakos/im/context_processors.py b/snf-astakos-app/astakos/im/context_processors.py index 3bc86ab7945fe341cc2b052a0637e2d074567e16..af37741c222fb6b11c39d98921bfb309d3bc63d2 100644 --- a/snf-astakos-app/astakos/im/context_processors.py +++ b/snf-astakos-app/astakos/im/context_processors.py @@ -1,35 +1,17 @@ -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from astakos.im import settings from astakos.im import presentation diff --git a/snf-astakos-app/astakos/im/cookie.py b/snf-astakos-app/astakos/im/cookie.py index b61e81a746e8facb6e7e87b97b020b74ea50686d..0af7ef1e7ba726e2b2af31e8fdb497ac3556c353 100644 --- a/snf-astakos-app/astakos/im/cookie.py +++ b/snf-astakos-app/astakos/im/cookie.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import logging diff --git a/snf-astakos-app/astakos/im/fields.py b/snf-astakos-app/astakos/im/fields.py index bdba1016da91fb1c084dbb15a11e481f4082f3de..f2bd641a94535b1948a2ff3550a8614dda571249 100644 --- a/snf-astakos-app/astakos/im/fields.py +++ b/snf-astakos-app/astakos/im/fields.py @@ -1,42 +1,29 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import re from django.utils.translation import ugettext as _ from django.utils.encoding import smart_str from django.utils.encoding import force_unicode as force_text +from django.utils.safestring import mark_safe from django import forms +from django.forms import widgets +from django.core import validators + +from synnefo.util import units class EmailValidator(object): @@ -91,3 +78,107 @@ class EmailValidator(object): class EmailField(forms.EmailField): default_validators = [EmailValidator()] + + +class CustomChoiceWidget(forms.MultiWidget): + + def __init__(self, attrs=None, **kwargs): + _widgets = ( + widgets.Select(attrs=attrs, **kwargs), + widgets.TextInput(attrs=attrs) + ) + super(CustomChoiceWidget, self).__init__(_widgets, attrs) + + def render(self, *args, **kwargs): + attrs = kwargs.get("attrs", {}) + css_class = attrs.get("class", "") + " custom-select" + attrs['class'] = css_class + kwargs['attrs'] = attrs + out = super(CustomChoiceWidget, self).render(*args, **kwargs) + return mark_safe(""" +%(html)s +<script> +$(document).ready(function() { + var select = $("#%(id)s_0"); + var input = $("#%(id)s_1"); + input.hide(); + var check_custom = function() { + var val = select.val(); + if (val == "custom") { + input.show().focus(); + } else { + input.hide().val(''); + } + } + select.bind("change", check_custom); + check_custom(); +}); +</script> +""" % ({ + 'id': attrs.get("id"), + 'html': out +})) + + def decompress(self, value): + if not value: + return ['custom', ''] + if value == 'Unlimited': + return ['Unlimited', ''] + + try: + value = int(value) + except ValueError: + return ['custom', value] + + values = dict(self.choices).values() + + if value in values: + return [str(value), ''] + else: + return ['custom', str(value)] + + def value_from_datadict(self, *args, **kwargs): + value = super(CustomChoiceWidget, self).value_from_datadict(*args, + **kwargs) + if value[0] == "custom": + return value[1] + return value[0] + + +class InfiniteChoiceField(forms.ChoiceField): + """ + A custom integer choice field which allows user to set a custom value. + """ + + INFINITE_VALUES = ['Unlimited'] + widget = CustomChoiceWidget + default_validators=[validators.MinValueValidator(0)] + + def _get_choices(self): + return self._choices + + def _set_choices(self, value): + self._choices = self.widget.choices = \ + self.widget.widgets[0].choices = \ + list(value) + [("custom", "Other")] + + choices = property(_get_choices, _set_choices) + + def to_python(self, value): + """ + Handle infinite values. + """ + if value in self.INFINITE_VALUES: + value = units.PRACTICALLY_INFINITE + value = super(InfiniteChoiceField, self).to_python(value) + try: + value = int(str(value)) + except (ValueError, TypeError): + raise forms.ValidationError(self.error_messages['invalid']) + return value + + def validate(self, value): + try: + value = int(str(value)) + except (ValueError, TypeError): + raise forms.ValidationError(self.error_messages['invalid']) diff --git a/snf-astakos-app/astakos/im/forms.py b/snf-astakos-app/astakos/im/forms.py index e119f5063feefe8bd4f1e5967d807e4fdf0c1cd0..f589924be4b5fca198a3038ad08a10edbcaddb8b 100644 --- a/snf-astakos-app/astakos/im/forms.py +++ b/snf-astakos-app/astakos/im/forms.py @@ -1,35 +1,20 @@ -# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +import re +import synnefo.util.date as date_util + from random import random from datetime import datetime @@ -42,19 +27,20 @@ from django.contrib.auth.tokens import default_token_generator from django.core.urlresolvers import reverse from django.utils.safestring import mark_safe from django.utils.encoding import smart_str -from django.db import transaction +from astakos.im import transaction from django.core import validators from synnefo.util import units from synnefo_branding.utils import render_to_string from synnefo.lib import join_urls -from astakos.im.fields import EmailField +from astakos.im.fields import EmailField, InfiniteChoiceField from astakos.im.models import AstakosUser, EmailChange, Invitation, Resource, \ PendingThirdPartyUser, get_latest_terms, ProjectApplication, Project from astakos.im import presentation from astakos.im.widgets import DummyWidget, RecaptchaWidget -from astakos.im.functions import send_change_email, submit_application, \ +from astakos.im.functions import submit_application, \ accept_membership_project_checks, ProjectError +from astakos.im.user_utils import send_change_email from astakos.im.util import reserved_verified_email, model_to_dict from astakos.im import auth_providers @@ -70,6 +56,9 @@ import re logger = logging.getLogger(__name__) +BASE_PROJECT_NAME_REGEX = re.compile( + r'^system:[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-' + '[a-f0-9]{12}$') DOMAIN_VALUE_REGEX = re.compile( r'^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}$', re.IGNORECASE) @@ -499,6 +488,7 @@ class SignApprovalTermsForm(forms.ModelForm): def __init__(self, *args, **kwargs): super(SignApprovalTermsForm, self).__init__(*args, **kwargs) + self.fields['has_signed_terms'].label = _("I agree with the terms") def clean_has_signed_terms(self): has_signed_terms = self.cleaned_data['has_signed_terms'] @@ -603,6 +593,10 @@ app_name_validator = validators.RegexValidator( DOMAIN_VALUE_REGEX, _(astakos_messages.DOMAIN_VALUE_ERR), 'invalid') +base_app_name_validator = validators.RegexValidator( + BASE_PROJECT_NAME_REGEX, + _(astakos_messages.BASE_PROJECT_NAME_ERR), + 'invalid') app_name_help = _(""" The project's name should be in a domain format. The domain shouldn't neccessarily exist in the real @@ -656,7 +650,7 @@ leave_policy_label = _("Leaving policy") app_member_leave_policy_help = _(""" Select how new members can leave the project.""") -max_members_label = _("Maximum member count") +max_members_label = _("Max members") max_members_help = _(""" Specify the maximum number of members this project may have, including the owner. Beyond this number, no new members @@ -716,10 +710,11 @@ class ProjectApplicationForm(forms.ModelForm): coerce=int, choices=leave_policies) - limit_on_members_number = forms.IntegerField( + limit_on_members_number = InfiniteChoiceField( + choices=settings.PROJECT_MEMBERS_LIMIT_CHOICES, label=max_members_label, help_text=max_members_help, - min_value=0, + initial="Unlimited", required=True) class Meta: @@ -731,17 +726,30 @@ class ProjectApplicationForm(forms.ModelForm): def __init__(self, *args, **kwargs): instance = kwargs.get('instance') - self.precursor_application = instance + super(ProjectApplicationForm, self).__init__(*args, **kwargs) # in case of new application remove closed join policy if not instance: policies = presentation.PROJECT_MEMBER_JOIN_POLICIES.copy() policies.pop(3) self.fields['member_join_policy'].choices = policies.iteritems() + else: + if instance.is_base: + name_field = self.fields['name'] + name_field.validators = [base_app_name_validator] + if self.initial['limit_on_members_number'] == \ + units.PRACTICALLY_INFINITE: + self.initial['limit_on_members_number'] = 'Unlimited' + + def clean_limit_on_members_number(self): + value = self.cleaned_data.get('limit_on_members_number') + if value in ["inf", "Unlimited"]: + return units.PRACTICALLY_INFINITE + return value def clean_start_date(self): start_date = self.cleaned_data.get('start_date') - if not self.precursor_application: + if not self.instance: today = datetime.now() today = datetime(today.year, today.month, today.day) if start_date and (start_date - today).days < 0: @@ -764,7 +772,7 @@ class ProjectApplicationForm(forms.ModelForm): def clean(self): userid = self.data.get('user', None) - self.resource_policies + policies = self.resource_policies self.user = None if userid: try: @@ -773,28 +781,73 @@ class ProjectApplicationForm(forms.ModelForm): pass if not self.user: raise forms.ValidationError(_(astakos_messages.NO_APPLICANT)) - super(ProjectApplicationForm, self).clean() - return self.cleaned_data + cleaned_data = super(ProjectApplicationForm, self).clean() + return cleaned_data @property def resource_policies(self): policies = [] append = policies.append - for name, value in self.data.iteritems(): + resource_indexes = {} + include_diffs = False + is_new = self.instance and self.instance.id is None + + existing_policies = [] + existing_data = {} + + # normalize to single values dict + data = dict() + for key, value in self.data.iteritems(): + data[key] = value + + if not is_new: + # User may have emptied some fields. Empty values are not handled + # below. Fill data as if user typed "0" in field, but only + # for resources which exist in application project and have + # non-zero capacity (either for member or project). + include_diffs = True + existing_policies = self.instance.resource_set + append_groups = set() + for policy in existing_policies: + cap_set = max(policy.project_capacity, policy.member_capacity) + + if not policy.resource.ui_visible: + continue + + rname = policy.resource.name + group = policy.resource.group + existing_data["%s_p_uplimit" % rname] = "0" + existing_data["%s_m_uplimit" % rname] = "0" + append_groups.add(group) + + for key, value in existing_data.iteritems(): + if not key in data or data.get(key, '') == '': + data[key] = value + for group in append_groups: + data["is_selected_%s" % group] = "1" + + for name, value in data.iteritems(): + if not value: continue - uplimit = value + if name.endswith('_uplimit'): - subs = name.split('_uplimit') - prefix, suffix = subs + is_project_limit = name.endswith('_p_uplimit') + suffix = '_p_uplimit' if is_project_limit else '_m_uplimit' + if value == 'inf' or value == 'Unlimited': + value = units.PRACTICALLY_INFINITE + uplimit = value + prefix, _suffix = name.split(suffix) + try: resource = Resource.objects.get(name=prefix) except Resource.DoesNotExist: raise forms.ValidationError("Resource %s does not exist" % resource.name) + # keep only resource limits for selected resource groups - if self.data.get('is_selected_%s' % - resource.group, "0") == "1": + if data.get('is_selected_%s' % \ + resource.group, "0") == "1": if not resource.ui_visible: raise forms.ValidationError("Invalid resource %s" % resource.name) @@ -804,10 +857,85 @@ class ProjectApplicationForm(forms.ModelForm): except ValueError: m = "Limit should be an integer" raise forms.ValidationError(m) + display = units.show(uplimit, resource.unit) - d.update(dict(resource=prefix, uplimit=uplimit, - display_uplimit=display)) - append(d) + if display == "inf": + display = "Unlimited" + + handled = resource_indexes.get(prefix) + + diff_data = None + if include_diffs: + try: + policy = existing_policies.get(resource=resource) + if is_project_limit: + pval = policy.project_capacity + else: + pval = policy.member_capacity + + if pval != uplimit: + diff = pval - uplimit + + diff_display = units.show(abs(diff), + resource.unit, + inf="Unlimited") + diff_is_inf = False + prev_is_inf = False + if uplimit == units.PRACTICALLY_INFINITE: + diff_display = "Unlimited" + diff_is_inf = True + if pval == units.PRACTICALLY_INFINITE: + diff_display = "Unlimited" + prev_is_inf = True + + prev_display = units.show(pval, resource.unit, + inf="Unlimited") + + diff_data = { + 'prev': pval, + 'prev_display': prev_display, + 'diff': diff, + 'diff_display': diff_display, + 'increased': diff < 0, + 'diff_is_inf': diff_is_inf, + 'prev_is_inf': prev_is_inf, + 'operator': '+' if diff < 0 else '-' + } + + except: + pass + + if is_project_limit: + d.update(dict(resource=prefix, + p_uplimit=uplimit, + display_p_uplimit=display)) + + if diff_data: + d.update(dict(resource=prefix, p_diff=diff_data)) + + if not handled: + d.update(dict(resource=prefix, m_uplimit=0, + display_m_uplimit=units.show(0, + resource.unit))) + else: + d.update(dict(resource=prefix, m_uplimit=uplimit, + display_m_uplimit=display)) + + if diff_data: + d.update(dict(resource=prefix, m_diff=diff_data)) + + if not handled: + d.update(dict(resource=prefix, p_uplimit=0, + display_p_uplimit=units.show(0, + resource.unit))) + + if resource_indexes.get(prefix, None) is not None: + # already included in policies + handled.update(d) + else: + # keep track of resource dicts + append(d) + resource_indexes[prefix] = d ordered_keys = presentation.RESOURCES['resources_order'] @@ -823,21 +951,93 @@ class ProjectApplicationForm(forms.ModelForm): def cleaned_resource_policies(self): policies = {} for d in self.resource_policies: + if self.instance.pk: + if not d.get('p_diff', None) and not d.get('m_diff', None): + continue + policies[d["name"]] = { - "project_capacity": None, - "member_capacity": d["uplimit"] + "project_capacity": d.get("p_uplimit", 0), + "member_capacity": d.get("m_uplimit", 0) } + if len(policies.keys()) == 0: + return {} + return policies - def save(self, commit=True, **kwargs): + def get_api_data(self): data = dict(self.cleaned_data) is_new = self.instance.id is None - data['project_id'] = self.instance.chain.id if not is_new else None - data['owner'] = self.user if is_new else self.instance.owner - data['resources'] = self.cleaned_resource_policies() + if isinstance(self.instance, Project): + data['project_id'] = self.instance.id + else: + data['project_id'] = self.instance.chain.id if not is_new else None + + owner_uuid = None + if self.instance.owner: + owner_uuid = self.instance.owner.uuid + + user_uuid = self.user.uuid if is_new else owner_uuid + try: + object_owner = AstakosUser.objects.get(uuid=user_uuid) + data['owner'] = object_owner + except AstakosUser.DoesNotExist: + pass + + exclude_keys = ['owner', 'comments', 'project_id', 'start_date'] + + # is_valid changes instance attributes + instance = self.instance + if not is_new: + instance = Project.objects.get(pk=self.instance.pk) + + for key in [dkey for dkey in data.keys() if not dkey in exclude_keys]: + if not is_new and \ + (getattr(instance, key) == data.get(key)): + del data[key] + + resources = self.cleaned_resource_policies() + if resources: + data['resources'] = resources + + if data.get('start_date', None): + data['start_date'] = date_util.isoformat(data.get('start_date')) + else: + del data['start_date'] + + if data.get('end_date', None): + data['end_date'] = date_util.isoformat(data.get('end_date')) + + limit = data.get('limit_on_members_number', None) + if limit: + data['max_members'] = data.get('limit_on_members_number') + else: + data['max_members'] = units.PRACTICALLY_INFINITE + data['request_user'] = self.user - submit_application(**data) + if 'owner' in data: + data['owner'] = data['owner'].uuid + + return data + + def save(self, commit=True, **kwargs): + from astakos.api import projects as api + data = self.get_api_data() + return api.submit_new_project(data, self.user) + + +class ProjectModificationForm(ProjectApplicationForm): + + class Meta: + model = Project + fields = ('name', 'homepage', 'description', + 'end_date', 'comments', 'member_join_policy', + 'member_leave_policy', 'limit_on_members_number') + + def save(self, commit=True, **kwargs): + from astakos.api import projects as api + data = self.get_api_data() + return api.submit_modification(data, self.user, self.instance.uuid) class ProjectSortForm(forms.Form): @@ -879,9 +1079,9 @@ class AddProjectMembersForm(forms.Form): required=True,) def __init__(self, *args, **kwargs): - chain_id = kwargs.pop('chain_id', None) - if chain_id: - self.project = Project.objects.get(id=chain_id) + project_id = kwargs.pop('project_id', None) + if project_id: + self.project = Project.objects.get(id=project_id) self.request_user = kwargs.pop('request_user', None) super(AddProjectMembersForm, self).__init__(*args, **kwargs) @@ -892,7 +1092,7 @@ class AddProjectMembersForm(forms.Form): raise forms.ValidationError(e) q = self.cleaned_data.get('q') or '' - users = q.split(',') + users = re.split("\r\n|\n|,", q) users = list(u.strip() for u in users if u) db_entries = AstakosUser.objects.accepted().filter(email__in=users) unknown = list(set(users) - set(u.email for u in db_entries)) @@ -904,10 +1104,7 @@ class AddProjectMembersForm(forms.Form): def get_valid_users(self): """Should be called after form cleaning""" - try: - return self.valid_users - except: - return () + return self.valid_users class ProjectMembersSortForm(forms.Form): diff --git a/snf-astakos-app/astakos/im/functions.py b/snf-astakos-app/astakos/im/functions.py index 4cf24e629df8b23de5833439a28938fab8c3f5a2..4926ef48b92d1ef6c4c315d3badaa9a7b82e0577 100644 --- a/snf-astakos-app/astakos/im/functions.py +++ b/snf-astakos-app/astakos/im/functions.py @@ -1,251 +1,42 @@ -# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +import re import logging +from datetime import datetime +from dateutil.relativedelta import relativedelta +import uuid from django.utils.translation import ugettext as _ -from django.core.mail import send_mail, get_connection -from django.core.urlresolvers import reverse -from django.contrib.auth import login as auth_login, logout as auth_logout from django.db.models import Q +from django.db.utils import IntegrityError + +from snf_django.lib.api import faults -from synnefo_branding.utils import render_to_string - -from synnefo.lib import join_urls -from astakos.im.models import AstakosUser, Invitation, ProjectMembership, \ - ProjectApplication, Project, new_chain, Resource, ProjectLock -from astakos.im.quotas import qh_sync_user, get_pending_app_quota, \ - register_pending_apps, qh_sync_project, qh_sync_locked_users, \ - get_users_for_update, members_to_sync -from astakos.im.project_notif import membership_change_notify, \ - membership_enroll_notify, membership_request_notify, \ - membership_leave_request_notify, application_submit_notify, \ - application_approve_notify, application_deny_notify, \ - project_termination_notify, project_suspension_notify, \ - project_unsuspension_notify, project_reinstatement_notify -from astakos.im import settings +import synnefo.util.date as date_util +from astakos.im.models import AstakosUser, ProjectMembership, \ + ProjectApplication, Project, new_chain, Resource, ProjectLock, \ + create_project, ProjectResourceQuota, ProjectResourceGrant +from astakos.im import quotas +from astakos.im import project_notif import astakos.im.messages as astakos_messages logger = logging.getLogger(__name__) -def login(request, user): - auth_login(request, user) - from astakos.im.models import SessionCatalog - SessionCatalog( - session_key=request.session.session_key, - user=user - ).save() - logger.info('%s logged in.', user.log_display) - - -def logout(request, *args, **kwargs): - user = request.user - auth_logout(request, *args, **kwargs) - user.delete_online_access_tokens() - logger.info('%s logged out.', user.log_display) - - -def send_verification(user, template_name='im/activation_email.txt'): - """ - Send email to user to verify his/her email and activate his/her account. - """ - url = join_urls(settings.BASE_HOST, - user.get_activation_url(nxt=reverse('index'))) - message = render_to_string(template_name, { - 'user': user, - 'url': url, - 'baseurl': settings.BASE_URL, - 'site_name': settings.SITENAME, - 'support': settings.CONTACT_EMAIL}) - sender = settings.SERVER_EMAIL - send_mail(_(astakos_messages.VERIFICATION_EMAIL_SUBJECT), message, sender, - [user.email], - connection=get_connection()) - logger.info("Sent user verirfication email: %s", user.log_display) - - -def _send_admin_notification(template_name, - context=None, - user=None, - msg="", - subject='alpha2 testing notification',): - """ - Send notification email to settings.HELPDESK + settings.MANAGERS + - settings.ADMINS. - """ - if context is None: - context = {} - if not 'user' in context: - context['user'] = user - - message = render_to_string(template_name, context) - sender = settings.SERVER_EMAIL - recipient_list = [e[1] for e in settings.HELPDESK + - settings.MANAGERS + settings.ADMINS] - send_mail(subject, message, sender, recipient_list, - connection=get_connection()) - if user: - msg = 'Sent admin notification (%s) for user %s' % (msg, - user.log_display) - else: - msg = 'Sent admin notification (%s)' % msg - - logger.log(settings.LOGGING_LEVEL, msg) - - -def send_account_pending_moderation_notification( - user, - template_name='im/account_pending_moderation_notification.txt'): - """ - Notify admins that a new user has verified his email address and moderation - step is required to activate his account. - """ - subject = (_(astakos_messages.ACCOUNT_CREATION_SUBJECT) % - {'user': user.email}) - return _send_admin_notification(template_name, {}, subject=subject, - user=user, msg="account creation") - - -def send_account_activated_notification( - user, - template_name='im/account_activated_notification.txt'): - """ - Send email to settings.HELPDESK + settings.MANAGERES + settings.ADMINS - lists to notify that a new account has been accepted and activated. - """ - message = render_to_string( - template_name, - {'user': user} - ) - sender = settings.SERVER_EMAIL - recipient_list = [e[1] for e in settings.HELPDESK + - settings.MANAGERS + settings.ADMINS] - send_mail(_(astakos_messages.HELPDESK_NOTIFICATION_EMAIL_SUBJECT) % - {'user': user.email}, - message, sender, recipient_list, connection=get_connection()) - msg = 'Sent helpdesk admin notification for %s' - logger.log(settings.LOGGING_LEVEL, msg, user.email) - - -def send_invitation(invitation, template_name='im/invitation.txt'): - """ - Send invitation email. - """ - subject = _(astakos_messages.INVITATION_EMAIL_SUBJECT) - url = '%s?code=%d' % (join_urls(settings.BASE_HOST, - reverse('index')), invitation.code) - message = render_to_string(template_name, { - 'invitation': invitation, - 'url': url, - 'baseurl': settings.BASE_URL, - 'site_name': settings.SITENAME, - 'support': settings.CONTACT_EMAIL}) - sender = settings.SERVER_EMAIL - send_mail(subject, message, sender, [invitation.username], - connection=get_connection()) - msg = 'Sent invitation %s' - logger.log(settings.LOGGING_LEVEL, msg, invitation) - inviter_invitations = invitation.inviter.invitations - invitation.inviter.invitations = max(0, inviter_invitations - 1) - invitation.inviter.save() - - -def send_greeting(user, email_template_name='im/welcome_email.txt'): - """ - Send welcome email to an accepted/activated user. - - Raises SMTPException, socket.error - """ - subject = _(astakos_messages.GREETING_EMAIL_SUBJECT) - message = render_to_string(email_template_name, { - 'user': user, - 'url': join_urls(settings.BASE_HOST, - reverse('index')), - 'baseurl': settings.BASE_URL, - 'site_name': settings.SITENAME, - 'support': settings.CONTACT_EMAIL}) - sender = settings.SERVER_EMAIL - send_mail(subject, message, sender, [user.email], - connection=get_connection()) - msg = 'Sent greeting %s' - logger.log(settings.LOGGING_LEVEL, msg, user.log_display) - - -def send_feedback(msg, data, user, email_template_name='im/feedback_mail.txt'): - subject = _(astakos_messages.FEEDBACK_EMAIL_SUBJECT) - from_email = settings.SERVER_EMAIL - recipient_list = [e[1] for e in settings.HELPDESK] - content = render_to_string(email_template_name, { - 'message': msg, - 'data': data, - 'user': user}) - send_mail(subject, content, from_email, recipient_list, - connection=get_connection()) - msg = 'Sent feedback from %s' - logger.log(settings.LOGGING_LEVEL, msg, user.log_display) - - -def send_change_email(ec, request, - email_template_name= - 'registration/email_change_email.txt'): - url = ec.get_url() - url = request.build_absolute_uri(url) - c = {'url': url, - 'site_name': settings.SITENAME, - 'support': settings.CONTACT_EMAIL, - 'ec': ec} - message = render_to_string(email_template_name, c) - from_email = settings.SERVER_EMAIL - send_mail(_(astakos_messages.EMAIL_CHANGE_EMAIL_SUBJECT), message, - from_email, - [ec.new_email_address], connection=get_connection()) - msg = 'Sent change email for %s' - logger.log(settings.LOGGING_LEVEL, msg, ec.user.log_display) - - -def invite(inviter, email, realname): - inv = Invitation(inviter=inviter, username=email, realname=realname) - inv.save() - send_invitation(inv) - inviter.invitations = max(0, inviter.invitations - 1) - inviter.save() - - -### PROJECT FUNCTIONS ### - - class ProjectError(Exception): pass @@ -290,9 +81,21 @@ def get_project_by_id(project_id): raise ProjectNotFound(m) +def get_project_by_uuid(uuid): + try: + return Project.objects.get(uuid=uuid) + except Project.DoesNotExist: + m = _(astakos_messages.UNKNOWN_PROJECT_ID) % uuid + raise ProjectNotFound(m) + + def get_project_for_update(project_id): try: - return Project.objects.select_for_update().get(id=project_id) + try: + project_id = int(project_id) + return Project.objects.select_for_update().get(id=project_id) + except ValueError: + return Project.objects.select_for_update().get(uuid=project_id) except Project.DoesNotExist: m = _(astakos_messages.UNKNOWN_PROJECT_ID) % project_id raise ProjectNotFound(m) @@ -346,24 +149,17 @@ def get_membership_by_id(memb_id): raise ProjectNotFound(m) -ALLOWED_CHECKS = [ - (lambda u, a: not u or u.is_project_admin()), - (lambda u, a: a.owner == u), - (lambda u, a: a.applicant == u), - (lambda u, a: a.chain.overall_state() == Project.O_ACTIVE - or bool(a.chain.projectmembership_set.any_accepted().filter(person=u))), -] - ADMIN_LEVEL = 0 OWNER_LEVEL = 1 -APPLICANT_LEVEL = 2 -ANY_LEVEL = 3 +APPLICANT_LEVEL = 1 +ANY_LEVEL = 2 -def _check_yield(b, silent=False): - if b: - return True +def is_admin(user): + return not user or user.is_project_admin() + +def _failure(silent=False): if silent: return False @@ -376,31 +172,60 @@ def membership_check_allowed(membership, request_user, r = project_check_allowed( membership.project, request_user, level, silent=True) - return _check_yield(r or membership.person == request_user, silent) + if r or membership.person == request_user: + return True + return _failure(silent) def project_check_allowed(project, request_user, level=OWNER_LEVEL, silent=False): - return app_check_allowed(project.application, request_user, level, silent) + if is_admin(request_user): + return True + if level <= ADMIN_LEVEL: + return _failure(silent) + + if project.owner == request_user: + return True + if level <= OWNER_LEVEL: + return _failure(silent) + + if project.state == Project.NORMAL and not project.private \ + or bool(project.projectmembership_set.any_accepted(). + filter(person=request_user)): + return True + return _failure(silent) def app_check_allowed(application, request_user, level=OWNER_LEVEL, silent=False): - checks = (f(request_user, application) for f in ALLOWED_CHECKS[:level+1]) - return _check_yield(any(checks), silent) + if is_admin(request_user): + return True + if level <= ADMIN_LEVEL: + return _failure(silent) + if application.applicant == request_user: + return True + return _failure(silent) + + +def checkAlive(project, silent=False): + def fail(msg): + if silent: + return False, msg + else: + raise ProjectConflict(msg) -def checkAlive(project): if not project.is_alive: - m = _(astakos_messages.NOT_ALIVE_PROJECT) % project.id - raise ProjectConflict(m) + m = _(astakos_messages.NOT_ALIVE_PROJECT) % project.uuid + return fail(m) + return True, None def accept_membership_project_checks(project, request_user): project_check_allowed(project, request_user) checkAlive(project) - join_policy = project.application.member_join_policy + join_policy = project.member_join_policy if join_policy == CLOSED_POLICY: m = _(astakos_messages.MEMBER_JOIN_POLICY_CLOSED) raise ProjectConflict(m) @@ -425,11 +250,11 @@ def accept_membership(memb_id, request_user=None, reason=None): accept_membership_checks(membership, request_user) user = membership.person membership.perform_action("accept", actor=request_user, reason=reason) - qh_sync_user(user) + quotas.qh_sync_membership(membership) logger.info("User %s has been accepted in %s." % (user.log_display, project)) - membership_change_notify(project, user, 'accepted') + project_notif.membership_change_notify(project, user, 'accepted') return membership @@ -452,7 +277,7 @@ def reject_membership(memb_id, request_user=None, reason=None): logger.info("Request of user %s for %s has been rejected." % (user.log_display, project)) - membership_change_notify(project, user, 'rejected') + project_notif.membership_change_notify(project, user, 'rejected') return membership @@ -484,7 +309,7 @@ def remove_membership_checks(membership, request_user=None): project_check_allowed(project, request_user) checkAlive(project) - leave_policy = project.application.member_leave_policy + leave_policy = project.member_leave_policy if leave_policy == CLOSED_POLICY: m = _(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED) raise ProjectConflict(m) @@ -496,11 +321,11 @@ def remove_membership(memb_id, request_user=None, reason=None): remove_membership_checks(membership, request_user) user = membership.person membership.perform_action("remove", actor=request_user, reason=reason) - qh_sync_user(user) + quotas.qh_sync_membership(membership) logger.info("User %s has been removed from %s." % (user.log_display, project)) - membership_change_notify(project, user, 'removed') + project_notif.membership_change_notify(project, user, 'removed') return membership @@ -520,7 +345,7 @@ def enroll_member(project_id, user, request_user=None, reason=None): accept_membership_project_checks(project, request_user) try: - membership = get_membership(project_id, user.id) + membership = get_membership(project.id, user.id) if not membership.check_action("enroll"): m = _(astakos_messages.MEMBERSHIP_ACCEPTED) raise ProjectConflict(m) @@ -529,11 +354,11 @@ def enroll_member(project_id, user, request_user=None, reason=None): membership = new_membership(project, user, actor=request_user, enroll=True) - qh_sync_user(user) + quotas.qh_sync_membership(membership) logger.info("User %s has been enrolled in %s." % (membership.person.log_display, project)) - membership_enroll_notify(project, membership.person) + project_notif.membership_enroll_notify(project, membership.person) return membership @@ -546,12 +371,19 @@ def leave_project_checks(membership, request_user): project = membership.project checkAlive(project) - leave_policy = project.application.member_leave_policy + leave_policy = project.member_leave_policy if leave_policy == CLOSED_POLICY: m = _(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED) raise ProjectConflict(m) +def can_cancel_join_request(project, user): + m = user.get_membership(project) + if m is None: + return False + return m.state in [m.REQUESTED] + + def can_leave_request(project, user): m = user.get_membership(project) if m is None: @@ -569,10 +401,10 @@ def leave_project(memb_id, request_user, reason=None): leave_project_checks(membership, request_user) auto_accepted = False - leave_policy = project.application.member_leave_policy + leave_policy = project.member_leave_policy if leave_policy == AUTO_ACCEPT_POLICY: membership.perform_action("remove", actor=request_user, reason=reason) - qh_sync_user(request_user) + quotas.qh_sync_membership(membership) logger.info("User %s has left %s." % (request_user.log_display, project)) auto_accepted = True @@ -581,14 +413,15 @@ def leave_project(memb_id, request_user, reason=None): reason=reason) logger.info("User %s requested to leave %s." % (request_user.log_display, project)) - membership_leave_request_notify(project, membership.person) + project_notif.membership_request_notify( + project, membership.person, "leave") return auto_accepted def join_project_checks(project): checkAlive(project) - join_policy = project.application.member_join_policy + join_policy = project.member_join_policy if join_policy == CLOSED_POLICY: m = _(astakos_messages.MEMBER_JOIN_POLICY_CLOSED) raise ProjectConflict(m) @@ -614,7 +447,7 @@ def new_membership(project, user, actor=None, reason=None, enroll=False): state = (ProjectMembership.ACCEPTED if enroll else ProjectMembership.REQUESTED) m = ProjectMembership.objects.create( - project=project, person=user, state=state) + project=project, person=user, state=state, initialized=enroll) m._log_create(None, state, actor=actor, reason=reason) return m @@ -633,15 +466,16 @@ def join_project(project_id, request_user, reason=None): membership = new_membership(project, request_user, actor=request_user, reason=reason) - join_policy = project.application.member_join_policy + join_policy = project.member_join_policy if (join_policy == AUTO_ACCEPT_POLICY and ( not project.violates_members_limit(adding=1))): membership.perform_action("accept", actor=request_user, reason=reason) - qh_sync_user(request_user) + quotas.qh_sync_membership(membership) logger.info("User %s joined %s." % (request_user.log_display, project)) else: - membership_request_notify(project, membership.person) + project_notif.membership_request_notify( + project, membership.person, "join") logger.info("User %s requested to join %s." % (request_user.log_display, project)) return membership @@ -667,6 +501,142 @@ def membership_allowed_actions(membership, request_user): return allowed +def new_uuid(): + return str(uuid.uuid4()) + + +def make_base_project(user): + chain = new_chain() + try: + proj = create_project( + id=chain.chain, + uuid=user.uuid, + last_application=None, + owner=None, + realname="system:" + user.uuid, + homepage="", + description=("system project for user " + user.username), + end_date=(datetime.now() + relativedelta(years=100)), + member_join_policy=CLOSED_POLICY, + member_leave_policy=CLOSED_POLICY, + limit_on_members_number=1, + private=True, + is_base=True) + except IntegrityError as e: + if 'uuid' in str(e): + m = (("The impossible happened: " + "User UUID '%s' collides with an existing project. " + "To resolve the issue, delete the user " + "and create a new one.") + % user.uuid) + logger.warning(m) + raise ProjectConflict(m) + raise + user.base_project = proj + user.save() + return proj + + +def enable_base_project(user): + project = make_base_project(user) + _fill_from_skeleton(project) + project.activate() + new_membership(project, user, enroll=True) + quotas.qh_sync_project(project) + + +MODIFY_KEYS_MAIN = ["owner", "realname", "homepage", "description"] +MODIFY_KEYS_EXTRA = ["end_date", "member_join_policy", "member_leave_policy", + "limit_on_members_number", "private"] +MODIFY_KEYS = MODIFY_KEYS_MAIN + MODIFY_KEYS_EXTRA + + +def modifies_main_fields(request): + return set(request.keys()).intersection(MODIFY_KEYS_MAIN) + + +def modify_project(project_id, request): + project = get_project_for_update(project_id) + if project.state not in Project.INITIALIZED_STATES: + m = _(astakos_messages.UNINITIALIZED_NO_MODIFY) % project.uuid + raise ProjectConflict(m) + + if project.is_base: + main_fields = modifies_main_fields(request) + if main_fields: + m = (_(astakos_messages.BASE_NO_MODIFY_FIELDS) + % ", ".join(map(unicode, main_fields))) + raise ProjectBadRequest(m) + + new_name = request.get("realname") + if new_name is not None and project.is_alive: + check_conflicting_projects(project, new_name) + project.realname = new_name + project.name = new_name + project.save() + + _modify_projects(Project.objects.filter(id=project.id), request) + + +def modify_projects_in_bulk(flt, request): + main_fields = modifies_main_fields(request) + if main_fields: + raise ProjectBadRequest("Cannot modify field(s) '%s' in bulk" % + ", ".join(map(unicode, main_fields))) + + projects = Project.objects.initialized(flt).select_for_update() + _modify_projects(projects, request) + + +def _modify_projects(projects, request): + upds = {} + for key in MODIFY_KEYS: + value = request.get(key) + if value is not None: + upds[key] = value + projects.update(**upds) + + changed_resources = set() + pquotas = [] + req_policies = request.get("resources", {}) + req_policies = validate_resource_policies(req_policies, admin=True) + for project in projects: + for resource, m_capacity, p_capacity in req_policies: + changed_resources.add(resource) + pquotas.append( + ProjectResourceQuota( + project=project, + resource=resource, + member_capacity=m_capacity, + project_capacity=p_capacity)) + ProjectResourceQuota.objects.\ + filter(project__in=projects, resource__in=changed_resources).delete() + ProjectResourceQuota.objects.bulk_create(pquotas) + quotas.qh_sync_projects(projects) + + +MAX_TEXT_INPUT = 4096 +MAX_BIGINT = 2**63 - 1 + + +DOMAIN_VALUE_REGEX = re.compile( + r'^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}$', + re.IGNORECASE) + + +def valid_project_name(name): + return DOMAIN_VALUE_REGEX.match(name) is not None + + +def get_date(date, key): + if isinstance(date, datetime): + return date + try: + return date_util.isoparse(date) + except ValueError: + raise ProjectBadRequest("Invalid %s" % key) + + def submit_application(owner=None, name=None, project_id=None, @@ -677,6 +647,7 @@ def submit_application(owner=None, member_join_policy=None, member_leave_policy=None, limit_on_members_number=None, + private=False, comments=None, resources=None, request_user=None): @@ -685,17 +656,84 @@ def submit_application(owner=None, if project_id is not None: project = get_project_for_update(project_id) project_check_allowed(project, request_user, level=APPLICANT_LEVEL) + if project.state not in Project.INITIALIZED_STATES: + raise ProjectConflict("Cannot modify an uninitialized project.") policies = validate_resource_policies(resources) + if name is not None: + maxlen = ProjectApplication.MAX_NAME_LENGTH + if len(name) > maxlen: + raise ProjectBadRequest( + "'name' value exceeds max length %s" % maxlen) + if not valid_project_name(name): + raise ProjectBadRequest("Project name should be in domain format") + + if member_join_policy is not None: + if member_join_policy not in POLICIES: + raise ProjectBadRequest("Invalid join policy") + + if member_leave_policy is not None: + if member_leave_policy not in POLICIES: + raise ProjectBadRequest("Invalid join policy") + + if homepage is not None: + maxlen = ProjectApplication.MAX_HOMEPAGE_LENGTH + if len(homepage) > maxlen: + raise ProjectBadRequest( + "'homepage' value exceeds max length %s" % maxlen) + if description is not None: + maxlen = MAX_TEXT_INPUT + if len(description) > maxlen: + raise ProjectBadRequest( + "'description' value exceeds max length %s" % maxlen) + if comments is not None: + maxlen = MAX_TEXT_INPUT + if len(comments) > maxlen: + raise ProjectBadRequest( + "'comments' value exceeds max length %s" % maxlen) + if limit_on_members_number is not None: + if not 0 <= limit_on_members_number <= MAX_BIGINT: + raise ProjectBadRequest("max_members out of range") + + if start_date is not None: + start_date = get_date(start_date, "start_date") + if end_date is not None: + end_date = get_date(end_date, "end_date") + if end_date < datetime.now(): + raise ProjectBadRequest( + "'end_date' must be in the future") + force = request_user.is_project_admin() - ok, limit = qh_add_pending_app(owner, project, force) + ok, limit = qh_add_pending_app(request_user, project, force) if not ok: m = _(astakos_messages.REACHED_PENDING_APPLICATION_LIMIT) % limit raise ProjectConflict(m) - application = ProjectApplication( + if project is None: + chain = new_chain() + project = create_project( + id=chain.chain, + owner=owner, + realname=name, + homepage=homepage, + description=description, + end_date=end_date, + member_join_policy=member_join_policy, + member_leave_policy=member_leave_policy, + limit_on_members_number=limit_on_members_number, + private=private) + if policies is not None: + set_project_resources(project, policies) + elif project.is_base: + if [x for x in [owner, name, homepage, description] if x is not None]: + raise ProjectConflict( + "Cannot modify fields 'owner', 'name', 'homepage', and " + "'description' of a system project.") + + application = ProjectApplication.objects.create( applicant=request_user, + chain=project, owner=owner, name=name, homepage=homepage, @@ -705,42 +743,35 @@ def submit_application(owner=None, member_join_policy=member_join_policy, member_leave_policy=member_leave_policy, limit_on_members_number=limit_on_members_number, + private=private, comments=comments) + if policies is not None: + set_application_resources(application, policies) - if project is None: - chain = new_chain() - application.chain_id = chain.chain - application.save() - Project.objects.create(id=chain.chain, application=application) - else: - application.chain = project - application.save() - if project.application.state != ProjectApplication.APPROVED: - project.application = application - project.save() - - pending = ProjectApplication.objects.filter( - chain=project, - state=ProjectApplication.PENDING).exclude(id=application.id) - for app in pending: - app.state = ProjectApplication.REPLACED - app.save() + project.last_application = application + project.save() + + ProjectApplication.objects.\ + filter(chain=project, state=ProjectApplication.PENDING).\ + exclude(id=application.id).\ + update(state=ProjectApplication.REPLACED) - if policies is not None: - set_resource_policies(application, policies) logger.info("User %s submitted %s." % (request_user.log_display, application.log_display)) - application_submit_notify(application) + action = "submit_new" if project_id is None else "submit_modification" + project_notif.application_notify(application, action) return application -def validate_resource_policies(policies): +def validate_resource_policies(policies, admin=False): if not isinstance(policies, dict): raise ProjectBadRequest("Malformed resource policies") resource_names = policies.keys() - resources = Resource.objects.filter(name__in=resource_names, - api_visible=True) + resources = Resource.objects.filter(name__in=resource_names) + if not admin: + resources = resources.filter(api_visible=True) + resource_d = {} for resource in resources: resource_d[resource.name] = resource @@ -755,25 +786,61 @@ def validate_resource_policies(policies): p_capacity = specs.get("project_capacity") m_capacity = specs.get("member_capacity") - if p_capacity is not None and not isinstance(p_capacity, (int, long)): - raise ProjectBadRequest("Malformed resource policies") - if not isinstance(m_capacity, (int, long)): + if not isinstance(p_capacity, (int, long)) or \ + not isinstance(m_capacity, (int, long)): raise ProjectBadRequest("Malformed resource policies") + if p_capacity > MAX_BIGINT or m_capacity > MAX_BIGINT: + raise ProjectBadRequest( + "Quota limit exceeds max value %s" % MAX_BIGINT) + if p_capacity < 0 or m_capacity < 0: + raise ProjectBadRequest( + "Negative quota limit is not allowed") + if p_capacity < m_capacity: + raise ProjectBadRequest( + "Project quota limit is less than member limit for " + "resource '%s'" % resource_name) pols.append((resource_d[resource_name], m_capacity, p_capacity)) return pols -def set_resource_policies(application, policies): +def set_application_resources(application, policies): + grants = [] for resource, m_capacity, p_capacity in policies: - g = application.projectresourcegrant_set - g.create(resource=resource, - member_capacity=m_capacity, - project_capacity=p_capacity) + grants.append( + ProjectResourceGrant( + project_application=application, + resource=resource, + member_capacity=m_capacity, + project_capacity=p_capacity)) + ProjectResourceGrant.objects.bulk_create(grants) + + +def set_project_resources(project, policies): + grants = [] + for resource, m_capacity, p_capacity in policies: + grants.append( + ProjectResourceQuota( + project=project, + resource=resource, + member_capacity=m_capacity, + project_capacity=p_capacity)) + ProjectResourceQuota.objects.bulk_create(grants) + + +def check_app_relevant(application, project, project_id): + if project_id is not None and project.uuid != project_id or \ + project.last_application != application: + pid = project_id if project_id is not None else project.uuid + m = (_("%s is not a pending application for project %s.") % + (application.id, pid)) + raise ProjectConflict(m) -def cancel_application(application_id, request_user=None, reason=""): - get_project_of_application_for_update(application_id) +def cancel_application(application_id, project_id=None, request_user=None, + reason=""): + project = get_project_of_application_for_update(application_id) application = get_application(application_id) + check_app_relevant(application, project, project_id) app_check_allowed(application, request_user, level=APPLICANT_LEVEL) if not application.can_cancel(): @@ -781,15 +848,19 @@ def cancel_application(application_id, request_user=None, reason=""): (application.id, application.state_display())) raise ProjectConflict(m) - qh_release_pending_app(application.owner) + qh_release_pending_app(application.applicant) application.cancel(actor=request_user, reason=reason) + if project.state == Project.UNINITIALIZED: + project.set_deleted() logger.info("%s has been cancelled." % (application.log_display)) -def dismiss_application(application_id, request_user=None, reason=""): - get_project_of_application_for_update(application_id) +def dismiss_application(application_id, project_id=None, request_user=None, + reason=""): + project = get_project_of_application_for_update(application_id) application = get_application(application_id) + check_app_relevant(application, project, project_id) app_check_allowed(application, request_user, level=APPLICANT_LEVEL) if not application.can_dismiss(): @@ -798,13 +869,16 @@ def dismiss_application(application_id, request_user=None, reason=""): raise ProjectConflict(m) application.dismiss(actor=request_user, reason=reason) + if project.state == Project.UNINITIALIZED: + project.set_deleted() logger.info("%s has been dismissed." % (application.log_display)) -def deny_application(application_id, request_user=None, reason=""): - get_project_of_application_for_update(application_id) +def deny_application(application_id, project_id=None, request_user=None, + reason=""): + project = get_project_of_application_for_update(application_id) application = get_application(application_id) - + check_app_relevant(application, project, project_id) app_check_allowed(application, request_user, level=ADMIN_LEVEL) if not application.can_deny(): @@ -812,34 +886,38 @@ def deny_application(application_id, request_user=None, reason=""): (application.id, application.state_display())) raise ProjectConflict(m) - qh_release_pending_app(application.owner) + qh_release_pending_app(application.applicant) application.deny(actor=request_user, reason=reason) logger.info("%s has been denied with reason \"%s\"." % (application.log_display, reason)) - application_deny_notify(application) + project_notif.application_notify(application, "deny") + +def check_conflicting_projects(project, new_project_name, silent=False): + def fail(msg): + if silent: + return False, msg + else: + raise ProjectConflict(msg) -def check_conflicting_projects(application): - project = application.chain - new_project_name = application.name try: - q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED) + q = Q(name=new_project_name) & ~Q(id=project.id) conflicting_project = Project.objects.get(q) - if (conflicting_project != project): - m = (_("cannot approve: project with name '%s' " - "already exists (id: %s)") % - (new_project_name, conflicting_project.id)) - raise ProjectConflict(m) # invalid argument + m = (_("cannot approve: project with name '%s' " + "already exists (id: %s)") % + (new_project_name, conflicting_project.uuid)) + return fail(m) except Project.DoesNotExist: - pass + return True, None -def approve_application(app_id, request_user=None, reason=""): +def approve_application(application_id, project_id=None, request_user=None, + reason=""): get_project_lock() - project = get_project_of_application_for_update(app_id) - application = get_application(app_id) - + project = get_project_of_application_for_update(application_id) + application = get_application(application_id) + check_app_relevant(application, project, project_id) app_check_allowed(application, request_user, level=ADMIN_LEVEL) if not application.can_approve(): @@ -847,26 +925,78 @@ def approve_application(app_id, request_user=None, reason=""): (application.id, application.state_display())) raise ProjectConflict(m) - check_conflicting_projects(application) + if application.name: + check_conflicting_projects(project, application.name) - # Pre-lock members and owner together in order to impose an ordering - # on locking users - members = members_to_sync(project) - uids_to_sync = [member.id for member in members] - owner = application.owner - uids_to_sync.append(owner.id) - get_users_for_update(uids_to_sync) - - qh_release_pending_app(owner, locked=True) + qh_release_pending_app(application.applicant) application.approve(actor=request_user, reason=reason) - project.application = application - project.name = application.name - project.save() - if project.is_deactivated(): - project.resume(actor=request_user, reason="APPROVE") - qh_sync_locked_users(members) + + if project.state == Project.UNINITIALIZED: + _fill_from_skeleton(project) + else: + _apply_modifications(project, application) + project.activate(actor=request_user, reason=reason) + + quotas.qh_sync_project(project) logger.info("%s has been approved." % (application.log_display)) - application_approve_notify(application) + project_notif.application_notify(application, "approve") + return project + + +def _fill_from_skeleton(project): + current_resources = set(ProjectResourceQuota.objects. + filter(project=project). + values_list("resource_id", flat=True)) + resources = Resource.objects.all() + new_quotas = [] + for resource in resources: + if resource.id not in current_resources: + limit = quotas.pick_limit_scheme(project, resource) + new_quotas.append( + ProjectResourceQuota( + project=project, + resource=resource, + member_capacity=limit, + project_capacity=limit)) + ProjectResourceQuota.objects.bulk_create(new_quotas) + + +def _apply_modifications(project, application): + FIELDS = [ + ("owner", "owner"), + ("name", "realname"), + ("homepage", "homepage"), + ("description", "description"), + ("end_date", "end_date"), + ("member_join_policy", "member_join_policy"), + ("member_leave_policy", "member_leave_policy"), + ("limit_on_members_number", "limit_on_members_number"), + ("private", "private"), + ] + + changed = False + for appfield, projectfield in FIELDS: + value = getattr(application, appfield) + if value is not None: + changed = True + setattr(project, projectfield, value) + if changed: + project.save() + + grants = application.projectresourcegrant_set.all() + pquotas = [] + resources = [] + for grant in grants: + resources.append(grant.resource) + pquotas.append( + ProjectResourceQuota( + project=project, + resource=grant.resource, + member_capacity=grant.member_capacity, + project_capacity=grant.project_capacity)) + ProjectResourceQuota.objects.\ + filter(project=project, resource__in=resources).delete() + ProjectResourceQuota.objects.bulk_create(pquotas) def check_expiration(execute=False): @@ -879,58 +1009,135 @@ def check_expiration(execute=False): return [project.expiration_info() for project in expired] +def validate_project_action(project, action, request_user=None, silent=True): + """Check if an action can apply on a project. + + Arguments: + project: The target project. + action: The name of the action (in capital letters). + request_user: The user that requests the action. + silent: If set to True, suppress exceptions. + + Returns: + A `(success, message)` tuple. `success` is a boolean value that + shows if the action can apply on a project, and `message` explains + why the action cannot apply on a project. + + If an action can apply on a project, this function will always return + `(True, None)`. + + Exceptions: + ProjectConflict: When the action cannot apply on a project due to a + conflict. + ProjectForbidden: When a user is not allowed to apply an action on a + project. + faults.BadRequest: When the action is unknown/malformed. + """ + def fail(e=Exception, msg=""): + if silent: + return False, msg + else: + raise e(msg) + + if action == "TERMINATE": + ok = project_check_allowed(project, request_user, level=ADMIN_LEVEL, + silent=silent) + if not ok: + return fail(ProjectConflict) + + ok, m = checkAlive(project, silent=silent) + if not ok: + return fail(ProjectConflict, m) + + if project.is_base: + m = _(astakos_messages.BASE_NO_TERMINATE) % project.uuid + return fail(ProjectConflict, m) + + elif action == "SUSPEND": + ok = project_check_allowed(project, request_user, level=ADMIN_LEVEL, + silent=silent) + if not ok: + return fail(ProjectConflict) + + ok, m = checkAlive(project, silent=silent) + if not ok: + return fail(ProjectConflict, m) + + if project.is_suspended: + m = _(astakos_messages.SUSPENDED_PROJECT) % project.uuid + return fail(ProjectConflict, m) + + elif action == "UNSUSPEND": + ok = project_check_allowed(project, request_user, level=ADMIN_LEVEL, + silent=silent) + if not ok: + return fail(ProjectConflict) + + if not project.is_suspended: + m = _(astakos_messages.NOT_SUSPENDED_PROJECT) % project.uuid + return fail(ProjectConflict, m) + + elif action == "REINSTATE": + ok = project_check_allowed(project, request_user, level=ADMIN_LEVEL, + silent=silent) + if not ok: + return fail(ProjectConflict) + + if not project.is_terminated: + m = _(astakos_messages.NOT_TERMINATED_PROJECT) % project.uuid + return fail(ProjectConflict, m) + + ok, m = check_conflicting_projects(project, project.realname, + silent=silent) + if not ok: + return fail(ProjectConflict, m) + + else: + return fail(faults.BadRequest, "Unknown action: {}.".format(action)) + + return True, None + + def terminate(project_id, request_user=None, reason=None): project = get_project_for_update(project_id) - project_check_allowed(project, request_user, level=ADMIN_LEVEL) - checkAlive(project) + validate_project_action(project, "TERMINATE", request_user, silent=False) project.terminate(actor=request_user, reason=reason) - qh_sync_project(project) + quotas.qh_sync_project(project) logger.info("%s has been terminated." % (project)) - project_termination_notify(project) + project_notif.project_notify(project, "terminate") def suspend(project_id, request_user=None, reason=None): project = get_project_for_update(project_id) - project_check_allowed(project, request_user, level=ADMIN_LEVEL) - checkAlive(project) + validate_project_action(project, "SUSPEND", request_user, silent=False) project.suspend(actor=request_user, reason=reason) - qh_sync_project(project) + quotas.qh_sync_project(project) logger.info("%s has been suspended." % (project)) - project_suspension_notify(project) + project_notif.project_notify(project, "suspend") def unsuspend(project_id, request_user=None, reason=None): project = get_project_for_update(project_id) - project_check_allowed(project, request_user, level=ADMIN_LEVEL) - - if not project.is_suspended: - m = _(astakos_messages.NOT_SUSPENDED_PROJECT) % project.id - raise ProjectConflict(m) + validate_project_action(project, "UNSUSPEND", request_user, silent=False) project.resume(actor=request_user, reason=reason) - qh_sync_project(project) + quotas.qh_sync_project(project) logger.info("%s has been unsuspended." % (project)) - project_unsuspension_notify(project) + project_notif.project_notify(project, "unsuspend") def reinstate(project_id, request_user=None, reason=None): get_project_lock() project = get_project_for_update(project_id) - project_check_allowed(project, request_user, level=ADMIN_LEVEL) - - if not project.is_terminated: - m = _(astakos_messages.NOT_TERMINATED_PROJECT) % project.id - raise ProjectConflict(m) - - check_conflicting_projects(project.application) + validate_project_action(project, "REINSTATE", request_user, silent=False) project.resume(actor=request_user, reason=reason) - qh_sync_project(project) + quotas.qh_sync_project(project) logger.info("%s has been reinstated" % (project)) - project_reinstatement_notify(project) + project_notif.project_notify(project, "reinstate") def _partition_by(f, l): @@ -946,36 +1153,40 @@ def _partition_by(f, l): def count_pending_app(users): users = list(users) apps = ProjectApplication.objects.filter(state=ProjectApplication.PENDING, - owner__in=users) - apps_d = _partition_by(lambda a: a.owner.uuid, apps) + applicant__in=users) + apps_d = _partition_by(lambda a: a.applicant.uuid, apps) - usage = {} + usage = quotas.QuotaDict() for user in users: uuid = user.uuid - usage[uuid] = len(apps_d.get(uuid, [])) + base_project = user.get_base_project() + usage[uuid][base_project.uuid][quotas.PENDING_APP_RESOURCE] = \ + len(apps_d.get(uuid, [])) return usage -def get_pending_app_diff(user, project): - if project is None: - diff = 1 - else: - objs = ProjectApplication.objects - q = objs.filter(chain=project, state=ProjectApplication.PENDING) - count = q.count() - diff = 1 - count - return diff +def get_existing_pending_app(project): + objs = ProjectApplication.objects + apps = objs.filter(chain=project, state=ProjectApplication.PENDING) + apps_d = _partition_by(lambda a: a.applicant, apps) + for user, userapps in apps_d.iteritems(): + apps_d[user] = len(userapps) + + return apps_d def qh_add_pending_app(user, project=None, force=False): - user = AstakosUser.objects.select_for_update().get(id=user.id) - diff = get_pending_app_diff(user, project) - return register_pending_apps(user, diff, force) + provisions = [(user, user.get_base_project(), 1)] + existing = get_existing_pending_app(project) + for applicant, value in existing.iteritems(): + provisions.append((applicant, applicant.get_base_project(), -value)) + return quotas.register_pending_apps(provisions, force=force) def check_pending_app_quota(user, project=None): - diff = get_pending_app_diff(user, project) - quota = get_pending_app_quota(user) + existing = get_existing_pending_app(project).get(user, 0) + diff = 1 - existing + quota = quotas.get_pending_app_quota(user) limit = quota['limit'] usage = quota['usage'] if usage + diff > limit: @@ -983,7 +1194,5 @@ def check_pending_app_quota(user, project=None): return True, None -def qh_release_pending_app(user, locked=False): - if not locked: - user = AstakosUser.objects.select_for_update().get(id=user.id) - register_pending_apps(user, -1) +def qh_release_pending_app(user): + quotas.register_pending_apps([(user, user.get_base_project(), -1)]) diff --git a/snf-astakos-app/astakos/im/management/commands/_common.py b/snf-astakos-app/astakos/im/management/commands/_common.py index 14d276cf977b034a59ba4d0ea5389a89a69bb413..18c4e8ddf69ec068a2e51a292d9ebf4d65b56d24 100644 --- a/snf-astakos-app/astakos/im/management/commands/_common.py +++ b/snf-astakos-app/astakos/im/management/commands/_common.py @@ -1,42 +1,24 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import uuid from django.core.validators import validate_email from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType -from django.core.management import CommandError +from snf_django.management.commands import CommandError from synnefo.util import units from astakos.im.models import AstakosUser @@ -197,48 +179,34 @@ def show_resource_value(number, resource, style): return units.show(number, unit, style) -def collect_holder_quotas(holder_quotas, h_initial, style=None): +def collect_holder_quotas(holder_quotas, style=None): print_data = [] for source, source_quotas in holder_quotas.iteritems(): - try: - s_initial = h_initial[source] - except KeyError: - continue for resource, values in source_quotas.iteritems(): - try: - initial = s_initial[resource] - except KeyError: - continue - initial = show_resource_value(initial, resource, style) limit = show_resource_value(values['limit'], resource, style) usage = show_resource_value(values['usage'], resource, style) - fields = (source, resource, initial, limit, usage) + fields = (source, resource, limit, usage) print_data.append(fields) return print_data -def show_user_quotas(holder_quotas, h_initial, style=None): - labels = ('source', 'resource', 'base_quota', 'total_quota', 'usage') - print_data = collect_holder_quotas(holder_quotas, h_initial, style=style) +def show_user_quotas(holder_quotas, style=None): + labels = ('source', 'resource', 'limit', 'usage') + print_data = collect_holder_quotas(holder_quotas, style=style) return print_data, labels -def show_quotas(qh_quotas, astakos_initial, info=None, style=None): - labels = ('user', 'source', 'resource', 'base_quota', 'total_quota', - 'usage') +def show_quotas(qh_quotas, info=None, style=None): + labels = ('holder', 'source', 'resource', 'limit', 'usage') if info is not None: labels = ('displayname',) + labels print_data = [] for holder, holder_quotas in qh_quotas.iteritems(): - h_initial = astakos_initial.get(holder) - if h_initial is None: - continue - if info is not None: email = info.get(holder, "") - h_data = collect_holder_quotas(holder_quotas, h_initial, style=style) + h_data = collect_holder_quotas(holder_quotas, style=style) if info is not None: h_data = [(email, holder) + fields for fields in h_data] else: diff --git a/snf-astakos-app/astakos/im/management/commands/_filtering.py b/snf-astakos-app/astakos/im/management/commands/_filtering.py index 06d5d3585f1019f94f8b7e2911381fb1a764a767..059d395fe98d32c4a8782a3cee6d056468f28791 100644 --- a/snf-astakos-app/astakos/im/management/commands/_filtering.py +++ b/snf-astakos-app/astakos/im/management/commands/_filtering.py @@ -1,38 +1,20 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from synnefo.util import units -from django.core.management import CommandError +from snf_django.management.commands import CommandError from django.db.models import Q diff --git a/snf-astakos-app/astakos/im/management/commands/authpolicy-add.py b/snf-astakos-app/astakos/im/management/commands/authpolicy-add.py index a5469b66cb6535efac3b9e98122b5fa6024030b4..7967b136821ce1e4ffdae044843a5134e4e813ef 100644 --- a/snf-astakos-app/astakos/im/management/commands/authpolicy-add.py +++ b/snf-astakos-app/astakos/im/management/commands/authpolicy-add.py @@ -1,45 +1,27 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import string from optparse import make_option -from django.core.management.base import BaseCommand, CommandError +from snf_django.management.commands import SynnefoCommand, CommandError from astakos.im.models import AuthProviderPolicyProfile as Profile -option_list = list(BaseCommand.option_list) + [ +option_list = list(SynnefoCommand.option_list) + [ make_option('--update', action='store_true', dest='update', @@ -68,7 +50,7 @@ for p in POLICIES: help="%s policy" % p.title())) -class Command(BaseCommand): +class Command(SynnefoCommand): args = "<name> <provider_name>" help = "Create a new authentication provider policy profile" option_list = option_list diff --git a/snf-astakos-app/astakos/im/management/commands/authpolicy-list.py b/snf-astakos-app/astakos/im/management/commands/authpolicy-list.py index fd564903f1854e1036a80b553bb6ad97720d367e..1be5ecb9614af7dc91234f625144a0c7986d1926 100644 --- a/snf-astakos-app/astakos/im/management/commands/authpolicy-list.py +++ b/snf-astakos-app/astakos/im/management/commands/authpolicy-list.py @@ -1,35 +1,17 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from astakos.im.models import AuthProviderPolicyProfile from snf_django.management.commands import ListCommand diff --git a/snf-astakos-app/astakos/im/management/commands/authpolicy-remove.py b/snf-astakos-app/astakos/im/management/commands/authpolicy-remove.py index 8cbe367df6a696ee4162dca0e85702ff7add6e83..f8ebdd647b2db6fe2ef1edcde138741f07a3f291 100644 --- a/snf-astakos-app/astakos/im/management/commands/authpolicy-remove.py +++ b/snf-astakos-app/astakos/im/management/commands/authpolicy-remove.py @@ -1,46 +1,28 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. -from django.core.management.base import BaseCommand, CommandError +from snf_django.management.commands import SynnefoCommand, CommandError from astakos.im.models import AuthProviderPolicyProfile as Profile -class Command(BaseCommand): +class Command(SynnefoCommand): args = "<profile_name>" help = "Remove an authentication provider policy" - option_list = BaseCommand.option_list + () + option_list = SynnefoCommand.option_list + () def handle(self, *args, **options): if len(args) != 1: diff --git a/snf-astakos-app/astakos/im/management/commands/authpolicy-set.py b/snf-astakos-app/astakos/im/management/commands/authpolicy-set.py index b6f88e69e1bb7ede336848fe09f6fd7deef17d8e..ddf655dda9b8fcdd83161b2e9896ead738eb2abd 100644 --- a/snf-astakos-app/astakos/im/management/commands/authpolicy-set.py +++ b/snf-astakos-app/astakos/im/management/commands/authpolicy-set.py @@ -1,45 +1,26 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option -from django.db import transaction -from django.core.management.base import BaseCommand, CommandError - +from astakos.im import transaction +from snf_django.management.commands import SynnefoCommand, CommandError from astakos.im.models import AuthProviderPolicyProfile as Profile from astakos.im.models import AstakosUser, Group -option_list = BaseCommand.option_list + ( +option_list = SynnefoCommand.option_list + ( make_option('--group', action='append', dest='groups', @@ -63,7 +44,7 @@ def update_profile(profile, users, groups): profile.users.add(*users) -class Command(BaseCommand): +class Command(SynnefoCommand): args = "<name> <provider_name>" help = "Assign an existing authentication provider policy profile to " + \ "a user or group. All previously set " diff --git a/snf-astakos-app/astakos/im/management/commands/authpolicy-show.py b/snf-astakos-app/astakos/im/management/commands/authpolicy-show.py index 0948222aad90f47419623cabafaed42092907f90..14c25cd3b2c0381cac5561f967b6e106f9571f59 100644 --- a/snf-astakos-app/astakos/im/management/commands/authpolicy-show.py +++ b/snf-astakos-app/astakos/im/management/commands/authpolicy-show.py @@ -1,41 +1,21 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. - -from django.core.management.base import CommandError +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from astakos.im.models import AuthProviderPolicyProfile as Profile from synnefo.lib.ordereddict import OrderedDict -from snf_django.management.commands import SynnefoCommand +from snf_django.management.commands import SynnefoCommand, CommandError from snf_django.management import utils diff --git a/snf-astakos-app/astakos/im/management/commands/cleanup-full.py b/snf-astakos-app/astakos/im/management/commands/cleanup-full.py index f36dccbf4736191dae44b561f89c4295e144e936..90d688bf74dc443146bb318ddf5559de60211c32 100644 --- a/snf-astakos-app/astakos/im/management/commands/cleanup-full.py +++ b/snf-astakos-app/astakos/im/management/commands/cleanup-full.py @@ -1,49 +1,31 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. -from django.core.management.base import NoArgsCommand from django.core.management import call_command from django.utils.importlib import import_module from django.conf import settings from astakos.im.models import SessionCatalog +from snf_django.management.commands import SynnefoCommand -class Command(NoArgsCommand): +class Command(SynnefoCommand): help = "Cleanup sessions and session catalog" - def handle_noargs(self, **options): + def handle(self, **options): self.stderr.write('Cleanup sessions ...\n') call_command('cleanup') diff --git a/snf-astakos-app/astakos/im/management/commands/component-add.py b/snf-astakos-app/astakos/im/management/commands/component-add.py index 538234f50c161a35d641c98116658bd0722591f7..24f24a392025b2d776bb71544f4978d6b959cc20 100644 --- a/snf-astakos-app/astakos/im/management/commands/component-add.py +++ b/snf-astakos-app/astakos/im/management/commands/component-add.py @@ -1,46 +1,28 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option -from django.core.management.base import BaseCommand, CommandError +from snf_django.management.commands import SynnefoCommand, CommandError from astakos.im.models import Component -class Command(BaseCommand): +class Command(SynnefoCommand): args = "<name>" help = "Register a component" - option_list = BaseCommand.option_list + ( + option_list = SynnefoCommand.option_list + ( make_option('--ui-url', dest='ui_url', default=None, diff --git a/snf-astakos-app/astakos/im/management/commands/component-list.py b/snf-astakos-app/astakos/im/management/commands/component-list.py index 213a82d06b66bfea1b8d0935adf8b00d8b40b839..365b4c21b29a2ad341331811a6d127c424d61341 100644 --- a/snf-astakos-app/astakos/im/management/commands/component-list.py +++ b/snf-astakos-app/astakos/im/management/commands/component-list.py @@ -1,35 +1,17 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from astakos.im.models import Component from snf_django.management.commands import ListCommand diff --git a/snf-astakos-app/astakos/im/management/commands/component-modify.py b/snf-astakos-app/astakos/im/management/commands/component-modify.py index 6443fb4881b269d7be78a429fcd6d4a3bb835b21..d376bba2bcf11b46a8c7bd21f322bb115089d0f6 100644 --- a/snf-astakos-app/astakos/im/management/commands/component-modify.py +++ b/snf-astakos-app/astakos/im/management/commands/component-modify.py @@ -1,48 +1,28 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option - -from django.core.management.base import BaseCommand, CommandError - +from snf_django.management.commands import SynnefoCommand, CommandError from astakos.im.models import Component -class Command(BaseCommand): +class Command(SynnefoCommand): args = "<component ID or name>" help = "Modify component attributes" - option_list = BaseCommand.option_list + ( + option_list = SynnefoCommand.option_list + ( make_option('--ui-url', dest='ui_url', default=None, diff --git a/snf-astakos-app/astakos/im/management/commands/component-remove.py b/snf-astakos-app/astakos/im/management/commands/component-remove.py index b6db6626ee464f746bde95cbaa4145350ec66c8a..89f5a8022e269e3229627b7e39879996b036834e 100644 --- a/snf-astakos-app/astakos/im/management/commands/component-remove.py +++ b/snf-astakos-app/astakos/im/management/commands/component-remove.py @@ -1,42 +1,24 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. -from django.core.management.base import BaseCommand, CommandError -from django.db import transaction +from snf_django.management.commands import SynnefoCommand, CommandError +from astakos.im import transaction from astakos.im.models import Component -class Command(BaseCommand): +class Command(SynnefoCommand): args = "<component ID or name>" help = "Remove a component along with its registered services" diff --git a/snf-astakos-app/astakos/im/management/commands/component-show.py b/snf-astakos-app/astakos/im/management/commands/component-show.py index a0eef4ed399c4a085c879df298ed718be540028c..9bd601233ddec489006c84034451cf7b8a813e6f 100644 --- a/snf-astakos-app/astakos/im/management/commands/component-show.py +++ b/snf-astakos-app/astakos/im/management/commands/component-show.py @@ -1,40 +1,21 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. -from django.core.management.base import CommandError from astakos.im.models import Component from synnefo.lib.ordereddict import OrderedDict -from snf_django.management.commands import SynnefoCommand +from snf_django.management.commands import SynnefoCommand, CommandError from snf_django.management import utils diff --git a/snf-astakos-app/astakos/im/management/commands/fix-superusers.py b/snf-astakos-app/astakos/im/management/commands/fix-superusers.py index 6fda2a47286941dc1e94481473a61af8201fdfce..758a64afc924cd5edce12c4f725919c65ec11b46 100644 --- a/snf-astakos-app/astakos/im/management/commands/fix-superusers.py +++ b/snf-astakos-app/astakos/im/management/commands/fix-superusers.py @@ -1,46 +1,25 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. - -from optparse import make_option -from datetime import datetime +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. -from django.db import transaction -from django.core.management.base import NoArgsCommand, CommandError +from astakos.im import transaction +from snf_django.management.commands import SynnefoCommand, CommandError from astakos.im.auth import fix_superusers -class Command(NoArgsCommand): +class Command(SynnefoCommand): help = "Transform superusers created by syncdb into AstakosUser instances" @transaction.commit_on_success diff --git a/snf-astakos-app/astakos/im/management/commands/group-add.py b/snf-astakos-app/astakos/im/management/commands/group-add.py index 7bbeb16ef29b021d8bbcb746829eded06991b97e..28eb7b02db0999b4db9ff17b782f375a90a06ca8 100644 --- a/snf-astakos-app/astakos/im/management/commands/group-add.py +++ b/snf-astakos-app/astakos/im/management/commands/group-add.py @@ -1,46 +1,27 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. - -from django.core.management.base import BaseCommand, CommandError +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +from snf_django.management.commands import SynnefoCommand, CommandError from astakos.im.models import Group -class Command(BaseCommand): +class Command(SynnefoCommand): args = "<group name>" help = "Create a group with the given name" - option_list = BaseCommand.option_list + () + option_list = SynnefoCommand.option_list + () def handle(self, *args, **options): if len(args) != 1: diff --git a/snf-astakos-app/astakos/im/management/commands/group-list.py b/snf-astakos-app/astakos/im/management/commands/group-list.py index 85707a9935ab4a766c167dd5b50c99d99e6549a0..a95483ba661c7328d68054ceb79760a0eb6ea965 100644 --- a/snf-astakos-app/astakos/im/management/commands/group-list.py +++ b/snf-astakos-app/astakos/im/management/commands/group-list.py @@ -1,35 +1,17 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from astakos.im.models import Group from snf_django.management.commands import ListCommand diff --git a/snf-astakos-app/astakos/im/management/commands/project-control.py b/snf-astakos-app/astakos/im/management/commands/project-control.py index d1672499bf5e637c3f536f4a84427e0a849c0783..c86baa06f1c2d2b66c2815583ea95ba1ca292f0b 100644 --- a/snf-astakos-app/astakos/im/management/commands/project-control.py +++ b/snf-astakos-app/astakos/im/management/commands/project-control.py @@ -1,49 +1,32 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option -from django.db import transaction -from django.core.management.base import BaseCommand, CommandError +from astakos.im import transaction +from snf_django.management import utils +from snf_django.management.commands import SynnefoCommand, CommandError from astakos.im.functions import (terminate, suspend, unsuspend, reinstate, check_expiration, approve_application, deny_application) -class Command(BaseCommand): +class Command(SynnefoCommand): help = "Manage projects and applications" - option_list = BaseCommand.option_list + ( + option_list = SynnefoCommand.option_list + ( make_option('--approve', dest='approve', metavar='<application id>', @@ -89,6 +72,7 @@ class Command(BaseCommand): @transaction.commit_on_success def handle(self, *args, **options): + self.output_format = options["output_format"] message = options['message'] actions = { @@ -120,28 +104,15 @@ class Command(BaseCommand): length = len(projects) if length == 0: s = 'No expired projects.\n' - elif length == 1: - s = '1 expired project:\n' - else: - s = '%d expired projects:\n' % (length,) - self.stderr.write(s) - - if length > 0: - labels = ('Project', 'Name', 'Status', 'Expiration date') - columns = (10, 30, 14, 30) - - line = ' '.join(l.rjust(w) for l, w in zip(labels, columns)) - self.stderr.write(line + '\n') - sep = '-' * len(line) - self.stderr.write(sep + '\n') - - for project in projects: - line = ' '.join(f.rjust(w) for f, w in zip(project, columns)) - self.stderr.write(line + '\n') - - if execute: - self.stderr.write('%d projects have been terminated.\n' % ( - length,)) + self.stderr.write(s) + return + labels = ('Project', 'Name', 'Status', 'Expiration date') + utils.pprint_table(self.stdout, projects, labels, + self.output_format, title="Expired projects") + + if execute: + self.stderr.write('%d projects have been terminated.\n' % + (length,)) def expire(self, execute=False): projects = check_expiration(execute=execute) diff --git a/snf-astakos-app/astakos/im/management/commands/project-list.py b/snf-astakos-app/astakos/im/management/commands/project-list.py index f01d7402f3243bf40e45eb9a68928ae2ad50c7b8..c1b7b1a556a21c1557dffc7965a07882b21c395f 100644 --- a/snf-astakos-app/astakos/im/management/commands/project-list.py +++ b/snf-astakos-app/astakos/im/management/commands/project-list.py @@ -1,50 +1,33 @@ -# Copyright 2012-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option -from snf_django.management.commands import SynnefoCommand, CommandError +from snf_django.management.commands import ListCommand -from astakos.im.models import Project -from django.db.models import Q -from snf_django.management import utils -from ._common import is_uuid, is_email +from astakos.im.models import Project, ProjectApplication +from ._common import is_uuid -class Command(SynnefoCommand): +class Command(ListCommand): help = """List projects and project status. Project status can be one of: + Uninitialized an uninitialized project, + with no pending application + Pending an uninitialized project, pending review Active an active project @@ -59,14 +42,14 @@ class Command(SynnefoCommand): it can later be resumed Terminated a terminated project; its name can be claimed - by a new project""" + by a new project - option_list = SynnefoCommand.option_list + ( - make_option('--all', - action='store_true', - dest='all', - default=False, - help="List all projects (default)"), + Deleted an uninitialized, deleted project""" + + object_class = Project + select_related = ["last_application", "owner"] + + option_list = ListCommand.option_list + ( make_option('--new', action='store_true', dest='new', @@ -83,89 +66,70 @@ class Command(SynnefoCommand): default=False, help=("Show only projects with a pending application " "(equiv. --modified --new)")), - make_option('--skip', + make_option('--deleted', action='store_true', - dest='skip', + dest='deleted', default=False, - help="Skip cancelled and terminated projects"), - make_option('--name', - dest='name', - help='Filter projects by name'), - make_option('--owner', - dest='owner', - help='Filter projects by owner\'s email or uuid'), + help="Also show cancelled/terminated projects"), + make_option('--system-projects', + action='store_true', + default=False, + help="Also show system projects"), ) - def handle(self, *args, **options): - - flt = Q() - owner = options['owner'] - if owner: - flt &= filter_by_owner(owner) - - name = options['name'] - if name: - flt &= filter_by_name(name) - - chains = Project.objects.all_with_pending(flt) - - if not options['all']: - if options['skip']: - pred = lambda c: ( - c[0].overall_state() not in Project.SKIP_STATES - or c[1] is not None) - chains = filter_preds([pred], chains) - - preds = [] - if options['new'] or options['pending']: - preds.append( - lambda c: c[0].overall_state() == Project.O_PENDING) - if options['modified'] or options['pending']: - preds.append( - lambda c: c[0].overall_state() != Project.O_PENDING - and c[1] is not None) - - if preds: - chains = filter_preds(preds, chains) - - labels = ('ProjID', 'Name', 'Owner', 'Email', 'Status', - 'Pending AppID') - - info = chain_info(chains) - utils.pprint_table(self.stdout, info, labels, - options["output_format"]) - - -def filter_preds(preds, chains): - return [c for c in chains - if any(map(lambda f: f(c), preds))] - - -def filter_by_name(name): - return Q(application__name=name) - - -def filter_by_owner(s): - if is_email(s): - return Q(application__owner__email=s) - if is_uuid(s): - return Q(application__owner__uuid=s) - raise CommandError("Expecting either email or uuid.") - - -def chain_info(chains): - l = [] - for project, pending_app in chains: - status = project.state_display() - pending_appid = pending_app.id if pending_app is not None else "" - application = project.application - - t = (project.pk, - application.name, - application.owner.realname, - application.owner.email, - status, - pending_appid, - ) - l.append(t) - return l + def get_owner(project): + return project.owner.email if project.owner else None + + def get_status(project): + return project.state_display() + + def get_pending_app(project): + app = project.last_application + return app.id if app and app.state == app.PENDING else "" + + FIELDS = { + "id": ("uuid", "Project ID"), + "name": ("realname", "Project Name"), + "owner": (get_owner, "Project Owner"), + "status": (get_status, "Project Status"), + "pending_app": (get_pending_app, + "An application pending for the project"), + } + + fields = ["id", "name", "owner", "status", "pending_app"] + + def handle_args(self, *args, **options): + try: + name_filter = self.filters.pop("name") + self.filters["realname"] = name_filter + except KeyError: + pass + + try: + owner_filter = self.filters.pop("owner") + if owner_filter is not None: + if is_uuid(owner_filter): + self.filters["owner__uuid"] = owner_filter + else: + self.filters["owner__email"] = owner_filter + except KeyError: + pass + + if not options['deleted']: + self.excludes["state__in"] = Project.SKIP_STATES + + if not options['system_projects']: + self.excludes["is_base"] = True + + if options["pending"]: + self.filter_pending() + else: + if options['new']: + self.filter_pending() + self.filters["state"] = Project.UNINITIALIZED + if options['modified']: + self.filter_pending() + self.filters["state__in"] = Project.INITIALIZED_STATES + + def filter_pending(self): + self.filters["last_application__state"] = ProjectApplication.PENDING diff --git a/snf-astakos-app/astakos/im/management/commands/project-modify.py b/snf-astakos-app/astakos/im/management/commands/project-modify.py new file mode 100644 index 0000000000000000000000000000000000000000..efbe9ea4f976ab00b8b9a74c0cd0d5f85b70b11e --- /dev/null +++ b/snf-astakos-app/astakos/im/management/commands/project-modify.py @@ -0,0 +1,151 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from optparse import make_option + +from django.db.models import Q +from snf_django.management.commands import SynnefoCommand, CommandError +from astakos.im import transaction +from synnefo.util import units +from astakos.im import functions +from astakos.im import models +import astakos.api.projects as api +import synnefo.util.date as date_util +from snf_django.management import utils +from astakos.im.management.commands import _common + + +def make_policies(limits): + policies = {} + for (name, member_capacity, project_capacity) in limits: + try: + member_capacity = units.parse(member_capacity) + project_capacity = units.parse(project_capacity) + except units.ParseError: + m = "Please specify capacity as a decimal integer" + raise CommandError(m) + policies[name] = {"member_capacity": member_capacity, + "project_capacity": project_capacity} + return policies + +Simple = type('Simple', (), {}) + + +class Param(object): + def __init__(self, key=Simple, mod=Simple, action=Simple, nargs=Simple, + is_main=False, help=""): + self.key = key + self.mod = mod + self.action = action + self.nargs = nargs + self.is_main = is_main + self.help = help + + +PARAMS = { + "name": Param(key="realname", help="Set project name"), + "owner": Param(mod=_common.get_accepted_user, help="Set project owner"), + "homepage": Param(help="Set project homepage"), + "description": Param(help="Set project description"), + "end_date": Param(mod=date_util.isoparse, is_main=True, + help=("Set project end date in ISO format " + "(e.g. 2014-01-01T00:00Z)")), + "join_policy": Param(key="member_join_policy", is_main=True, + mod=(lambda x: api.MEMBERSHIP_POLICY[x]), + help="Set join policy (auto, moderated, or closed)"), + "leave_policy": Param(key="member_leave_policy", is_main=True, + mod=(lambda x: api.MEMBERSHIP_POLICY[x]), + help=("Set leave policy " + "(auto, moderated, or closed)")), + "max_members": Param(key="limit_on_members_number", mod=int, is_main=True, + help="Set maximum members limit"), + "private": Param(mod=utils.parse_bool, is_main=True, + help="Set project private"), + "limit": Param(key="resources", mod=make_policies, is_main=True, + nargs=3, action="append", + help=("Set resource limits: " + "resource_name member_capacity project_capacity")), +} + + +def make_options(): + options = [] + for key, param in PARAMS.iteritems(): + opt = "--" + key.replace('_', '-') + kwargs = {} + if param.action is not Simple: + kwargs["action"] = param.action + if param.nargs is not Simple: + kwargs["nargs"] = param.nargs + kwargs["help"] = param.help + options.append(make_option(opt, **kwargs)) + return tuple(options) + + +class Command(SynnefoCommand): + args = "<project id> (or --all-system-projects)" + help = "Modify an already initialized project" + option_list = SynnefoCommand.option_list + make_options() + ( + make_option('--all-system-projects', + action='store_true', + default=False, + help="Modify in bulk all initialized system projects"), + make_option('--exclude', + help=("If `--all-system-projects' is given, exclude projects" + " given as a list of uuids: uuid1,uuid2,uuid3")), + ) + + def check_args(self, args, all_base, exclude): + if all_base and args or not all_base and len(args) != 1: + m = "Please provide a project ID or --all-system-projects" + raise CommandError(m) + if not all_base and exclude: + m = ("Option --exclude is meaningful only combined with " + " --all-system-projects.") + raise CommandError(m) + + def mk_all_base_filter(self, all_base, exclude): + flt = Q(state__in=models.Project.INITIALIZED_STATES, is_base=True) + if exclude: + exclude = exclude.split(',') + flt &= ~Q(uuid__in=exclude) + return flt + + @transaction.commit_on_success + def handle(self, *args, **options): + all_base = options["all_system_projects"] + exclude = options["exclude"] + self.check_args(args, all_base, exclude) + + try: + changes = {} + for key, value in options.iteritems(): + param = PARAMS.get(key) + if param is None or value is None: + continue + if all_base and not param.is_main: + m = "Cannot modify field '%s' in bulk" % key + raise CommandError(m) + k = key if param.key is Simple else param.key + v = value if param.mod is Simple else param.mod(value) + changes[k] = v + + if all_base: + flt = self.mk_all_base_filter(all_base, exclude) + functions.modify_projects_in_bulk(flt, changes) + else: + functions.modify_project(args[0], changes) + except BaseException as e: + raise CommandError(e) diff --git a/snf-astakos-app/astakos/im/management/commands/project-show.py b/snf-astakos-app/astakos/im/management/commands/project-show.py index e15e742024aaa5dd5b931051cedde29a31216016..caed84e60d180b80d09cd45ed858f408dd19968a 100644 --- a/snf-astakos-app/astakos/im/management/commands/project-show.py +++ b/snf-astakos-app/astakos/im/management/commands/project-show.py @@ -1,63 +1,40 @@ -# Copyright 2012-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option -from django.core.management.base import CommandError from synnefo.lib.ordereddict import OrderedDict -from snf_django.management.commands import SynnefoCommand +from snf_django.management.commands import SynnefoCommand, CommandError from snf_django.management import utils from astakos.im.models import ProjectApplication, Project +from astakos.im import quotas from ._common import show_resource_value, style_options, check_style +from synnefo.util import units class Command(SynnefoCommand): args = "<id>" - help = "Show details for project (or application) <id>" + help = "Show details for project <id>" option_list = SynnefoCommand.option_list + ( - make_option('--app', - action='store_true', - dest='app', - default=False, - help="Show details of applications instead of projects" - ), - make_option('--pending', + make_option('--pending-app', action='store_true', dest='pending', default=False, help=("For a given project, show also pending " - "modifications (applications), if any") + "application, if any") ), make_option('--members', action='store_true', @@ -65,6 +42,11 @@ class Command(SynnefoCommand): default=False, help=("Show a list of project memberships") ), + make_option('--quota', + action='store_true', + dest='list_quotas', + default=False, + help="List project quota"), make_option('--unit-style', default='mb', help=("Specify display unit for resource values " @@ -80,32 +62,26 @@ class Command(SynnefoCommand): show_pending = bool(options['pending']) show_members = bool(options['members']) - search_apps = options['app'] + show_quota = bool(options['list_quotas']) self.output_format = options['output_format'] id_ = args[0] - try: - id_ = int(id_) - except ValueError: - raise CommandError("id should be an integer value.") - - if search_apps: - app = get_app(id_) - self.print_app(app) - else: - project, pending_app = get_chain_state(id_) - self.print_project(project, pending_app) + if True: + project = get_chain_state(id_) + self.print_project(project, show_quota) if show_members and project is not None: self.stdout.write("\n") fields, labels = members_fields(project) self.pprint_table(fields, labels, title="Members") - if show_pending and pending_app is not None: - self.stdout.write("\n") - self.print_app(pending_app) + if show_pending: + app = project.last_application + if app and app.state == ProjectApplication.PENDING: + self.stdout.write("\n") + self.print_app(app) - def pprint_dict(self, d, vertical=True): + def pprint_dict(self, d, vertical=True, title=None): utils.pprint_table(self.stdout, [d.values()], d.keys(), - self.output_format, vertical=vertical) + self.output_format, vertical=vertical, title=title) def pprint_table(self, tbl, labels, title=None): utils.pprint_table(self.stdout, tbl, labels, @@ -113,110 +89,113 @@ class Command(SynnefoCommand): def print_app(self, app): app_info = app_fields(app) - self.pprint_dict(app_info) - self.print_resources(app) - - def print_project(self, project, app): - if project is None: - self.print_app(app) - else: - self.pprint_dict(project_fields(project, app)) - self.print_resources(project.application) - - def print_resources(self, app): - fields, labels = resource_fields(app, self.unit_style) + self.pprint_dict(app_info, title="Pending Application") + self.print_app_resources(app) + + def print_project(self, project, show_quota=False): + self.pprint_dict(project_fields(project)) + quota = (quotas.get_project_quota(project) + if show_quota else None) + self.print_resources(project, quota=quota) + + def print_resources(self, project, quota=None): + policies = project.projectresourcequota_set.all() + fields, labels = resource_fields(policies, quota, self.unit_style) if fields: self.stdout.write("\n") self.pprint_table(fields, labels, title="Resource limits") - -def get_app(app_id): - try: - return ProjectApplication.objects.get(id=app_id) - except ProjectApplication.DoesNotExist: - raise CommandError("Application with id %s not found." % app_id) + def print_app_resources(self, app): + policies = app.projectresourcegrant_set.all() + fields, labels = resource_fields(policies, None, self.unit_style) + if fields: + self.stdout.write("\n") + self.pprint_table(fields, labels, title="Resource limits") def get_chain_state(project_id): try: - chain = Project.objects.get(id=project_id) - return chain, chain.last_pending_application() + return Project.objects.get(uuid=project_id) except Project.DoesNotExist: raise CommandError("Project with id %s not found." % project_id) -def resource_fields(app, style): - labels = ('name', 'description', 'max per member') - policies = app.projectresourcegrant_set.all() +def resource_fields(policies, quota, style): + labels = ('name', 'max per member', 'max per project') + if quota: + labels += ('usage',) collect = [] for policy in policies: name = policy.resource.name - desc = policy.resource.desc capacity = policy.member_capacity - collect.append((name, desc, - show_resource_value(capacity, name, style))) + p_capacity = policy.project_capacity + row = (name, + show_resource_value(capacity, name, style), + show_resource_value(p_capacity, name, style)) + if quota: + r_quota = quota.get(name) + usage = r_quota.get('project_usage') + row += (show_resource_value(usage, name, style),) + collect.append(row) return collect, labels def app_fields(app): - mem_limit = app.limit_on_members_number - mem_limit_show = mem_limit if mem_limit is not None else "unlimited" - d = OrderedDict([ - ('project id', app.chain_id), + ('project id', app.chain.uuid), ('application id', app.id), - ('name', app.name), ('status', app.state_display()), - ('owner', app.owner), ('applicant', app.applicant), - ('homepage', app.homepage), - ('description', app.description), ('comments for review', app.comments), ('request issue date', app.issue_date), - ('request start date', app.start_date), - ('request end date', app.end_date), - ('join policy', app.member_join_policy_display), - ('leave policy', app.member_leave_policy_display), - ('max members', mem_limit_show), - ]) + ]) + if app.name: + d['name'] = app.name + if app.owner: + d['owner'] = app.owner + if app.homepage: + d['homepage'] = app.homepage + if app.description: + d['description'] = app.description + if app.start_date: + d['request start date'] = app.start_date + if app.end_date: + d['request end date'] = app.end_date + if app.member_join_policy: + d['join policy'] = app.member_join_policy_display + if app.member_leave_policy: + d['leave policy'] = app.member_leave_policy_display + if app.limit_on_members_number: + d['max members'] = units.show(app.limit_on_members_number, None) return d -def project_fields(project, pending_app): - app = project.application +def project_fields(project): + app = project.last_application + pending_app = (app.id if app and app.state == app.PENDING + else None) d = OrderedDict([ - ('project id', project.id), - ('application id', app.id), - ('name', app.name), + ('project id', project.uuid), + ('name', project.realname), ('status', project.state_display()), - ]) - if pending_app is not None: - d.update([('pending application', pending_app.id)]) - - d.update([('owner', app.owner), - ('applicant', app.applicant), - ('homepage', app.homepage), - ('description', app.description), - ('comments for review', app.comments), - ('request issue date', app.issue_date), - ('request start date', app.start_date), - ('creation date', project.creation_date), - ('request end date', app.end_date), - ]) + ('pending_app', pending_app), + ('owner', project.owner), + ('homepage', project.homepage), + ('description', project.description), + ('creation date', project.creation_date), + ('request end date', project.end_date), + ]) deact = project.last_deactivation() if deact is not None: d['deactivation date'] = deact.date - mem_limit = app.limit_on_members_number - mem_limit_show = mem_limit if mem_limit is not None else "unlimited" - d.update([ - ('join policy', app.member_join_policy_display), - ('leave policy', app.member_leave_policy_display), - ('max members', mem_limit_show), + ('join policy', project.member_join_policy_display), + ('leave policy', project.member_leave_policy_display), + ('max members', units.show(project.limit_on_members_number, None)), ('total members', project.members_count()), ]) diff --git a/snf-astakos-app/astakos/im/management/commands/quota-list.py b/snf-astakos-app/astakos/im/management/commands/quota-list.py index c7f9362e0598d614d17f545f82edcc11660197d4..d28117ceaf84f613f7649f9e2d20287515b56b0d 100644 --- a/snf-astakos-app/astakos/im/management/commands/quota-list.py +++ b/snf-astakos-app/astakos/im/management/commands/quota-list.py @@ -1,38 +1,20 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option -from django.db import transaction +from astakos.im import transaction from astakos.im.models import AstakosUser from astakos.im.quotas import list_user_quotas @@ -58,34 +40,27 @@ class Command(SynnefoCommand): make_option('--overlimit', action='store_true', help="Show quota that is over limit"), - make_option('--with-custom', - metavar='True|False', - help=("Filter quota different from the default or " - "equal to it")), make_option('--filter-by', help="Filter by field; " "e.g. \"user=uuid,usage>=10M,base_quota<inf\""), - make_option('--displayname', + make_option('--display-mails', action='store_true', + dest="display-mails", help="Show user display name"), ) QHFLT = { - "total_quota": ("limit", filtering.parse_with_unit), + "limit": ("limit", filtering.parse_with_unit), "usage": ("usage_max", filtering.parse_with_unit), "user": ("holder", lambda x: x), "resource": ("resource", lambda x: x), "source": ("source", lambda x: x), } - INITFLT = { - "base_quota": ("capacity", filtering.parse_with_unit), - } - @transaction.commit_on_success def handle(self, *args, **options): output_format = options["output_format"] - displayname = bool(options["displayname"]) + displayname = bool(options["display-mails"]) unit_style = options["unit_style"] common.check_style(unit_style) @@ -95,30 +70,19 @@ class Command(SynnefoCommand): else: filters = [] - QHQ, INITQ = Q(), Q() + QHQ = Q() for flt in filters: q = filtering.make_query(flt, self.QHFLT) if q is not None: QHQ &= q - q = filtering.make_query(flt, self.INITFLT) - if q is not None: - INITQ &= q overlimit = bool(options["overlimit"]) if overlimit: QHQ &= Q(usage_max__gt=F("limit")) - with_custom = options["with_custom"] - if with_custom is not None: - qeq = Q(capacity=F("resource__uplimit")) - try: - INITQ &= ~qeq if utils.parse_bool(with_custom) else qeq - except ValueError as e: - raise CommandError(e) - users = AstakosUser.objects.accepted() - qh_quotas, astakos_i = list_user_quotas( - users, qhflt=QHQ, initflt=INITQ) + qh_quotas = list_user_quotas( + users, qhflt=QHQ) if displayname: info = {} @@ -128,5 +92,5 @@ class Command(SynnefoCommand): info = None print_data, labels = common.show_quotas( - qh_quotas, astakos_i, info, style=unit_style) + qh_quotas, info, style=unit_style) utils.pprint_table(self.stdout, print_data, labels, output_format) diff --git a/snf-astakos-app/astakos/im/management/commands/quota-verify.py b/snf-astakos-app/astakos/im/management/commands/quota-verify.py index d595afde220e1b75a55197e81e6733a1e08c4aca..4b87cbfd29eb1ee22f94c583f16c5118aa84e9cb 100644 --- a/snf-astakos-app/astakos/im/management/commands/quota-verify.py +++ b/snf-astakos-app/astakos/im/management/commands/quota-verify.py @@ -1,108 +1,119 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option -from django.db import transaction +from astakos.im import transaction -from astakos.im.models import AstakosUser -from astakos.im.quotas import ( - qh_sync_users_diffs,) -from astakos.im.functions import get_user_by_uuid +from astakos.im.models import Project +from astakos.im import quotas +from snf_django.management.utils import pprint_table from snf_django.management.commands import SynnefoCommand -from astakos.im.management.commands import _common as common import logging logger = logging.getLogger(__name__) +def differences(local_quotas, qh_quotas): + unsynced = [] + unexpected = [] + for holder, h_quotas in local_quotas.iteritems(): + qh_h_quotas = qh_quotas.pop(holder, {}) + for source, s_quotas in h_quotas.iteritems(): + qh_s_quotas = qh_h_quotas.pop(source, {}) + for resource, value in s_quotas.iteritems(): + qh_value = qh_s_quotas.pop(resource, None) + if value != qh_value: + data = (holder, source, resource, value, qh_value) + unsynced.append(data) + unexpected += unexpected_resources(holder, source, qh_s_quotas) + unexpected += unexpected_sources(holder, qh_h_quotas) + unexpected += unexpected_holders(qh_quotas) + return unsynced, unexpected + + +def unexpected_holders(qh_quotas): + unexpected = [] + for holder, qh_h_quotas in qh_quotas.iteritems(): + unexpected += unexpected_sources(holder, qh_h_quotas) + return unexpected + + +def unexpected_sources(holder, qh_h_quotas): + unexpected = [] + for source, qh_s_quotas in qh_h_quotas.iteritems(): + unexpected += unexpected_resources(holder, source, qh_s_quotas) + return unexpected + + +def unexpected_resources(holder, source, qh_s_quotas): + unexpected = [] + for resource, qh_value in qh_s_quotas.iteritems(): + data = (holder, source, resource, None, qh_value) + unexpected.append(data) + return unexpected + + class Command(SynnefoCommand): - help = "Check the integrity of user quota" + help = "Check the integrity of user and project quota" option_list = SynnefoCommand.option_list + ( - make_option('--sync', - action='store_true', - dest='sync', + make_option("--include-unexpected-holdings", default=False, - help="Sync quotaholder"), - make_option('--user', - metavar='<uuid or email>', - dest='user', - help="Check for a specified user"), + action="store_true", + help=("Also check for holdings that do not correspond " + "to Astakos projects or user. Note that fixing such " + "inconsistencies will permanently delete these " + "holdings.")), + make_option("--fix", dest="fix", + default=False, + action="store_true", + help="Synchronize Quotaholder with Astakos DB."), ) @transaction.commit_on_success def handle(self, *args, **options): - sync = options['sync'] - user_ident = options['user'] - - if user_ident is not None: - users = [common.get_accepted_user(user_ident)] - else: - users = AstakosUser.objects.accepted() - - qh_limits, diff_q = qh_sync_users_diffs(users, sync=sync) - if sync: - self.print_sync(diff_q) - else: - self.print_verify(qh_limits, diff_q) - - def print_sync(self, diff_quotas): - size = len(diff_quotas) - if size == 0: - self.stderr.write("No sync needed.\n") - else: - self.stderr.write("Synced %s users:\n" % size) - uuids = diff_quotas.keys() - users = AstakosUser.objects.filter(uuid__in=uuids) - for user in users: - self.stderr.write("%s (%s)\n" % (user.uuid, user.username)) - - def print_verify(self, qh_limits, diff_quotas): - for holder, local in diff_quotas.iteritems(): - registered = qh_limits.pop(holder, None) - user = get_user_by_uuid(holder) - if registered is None: - self.stderr.write( - "No quota for %s (%s) in quotaholder.\n" % - (holder, user.username)) - else: - self.stdout.write("Quota differ for %s (%s):\n" % - (holder, user.username)) - self.stdout.write("Quota according to quotaholder:\n") - self.stdout.write("%s\n" % (registered)) - self.stdout.write("Quota according to astakos:\n") - self.stdout.write("%s\n\n" % (local)) - - diffs = len(diff_quotas) - if diffs: - self.stderr.write("Quota differ for %d users.\n" % (diffs)) + write = self.stderr.write + fix = options['fix'] + check_unexpected = options["include_unexpected_holdings"] + + projects = Project.objects.all() + local_proj_quotas, local_user_quotas = \ + quotas.astakos_project_quotas(projects) + qh_proj_quotas, qh_user_quotas = \ + quotas.get_projects_quota_limits() + unsynced, unexpected = differences(local_proj_quotas, qh_proj_quotas) + unsync_u, unexpect_u = differences(local_user_quotas, qh_user_quotas) + unsynced += unsync_u + unexpected += unexpect_u + + headers = ("Holder", "Source", "Resource", "Astakos", "Quotaholder") + if not unsynced and (not check_unexpected or not unexpected): + write("Everything in sync.\n") + return + + printable = (unsynced if not check_unexpected + else unsynced + unexpected) + pprint_table(self.stdout, printable, headers, title="Inconsistencies") + if fix: + to_sync = [] + for holder, source, resource, value, qh_value in unsynced: + to_sync.append(((holder, source, resource), value)) + quotas.qh.set_quota(to_sync) + + if check_unexpected: + to_del = [] + for holder, source, resource, value, qh_value in unexpected: + to_del.append((holder, source, resource)) + quotas.qh.delete_quota(to_del) diff --git a/snf-astakos-app/astakos/im/management/commands/reconcile-resources-astakos.py b/snf-astakos-app/astakos/im/management/commands/reconcile-resources-astakos.py index 52db8359b4dd07a2af464aeb5792caedb3496872..64ab091eaa72ea52d2bcbfda3b2ed291f63288b4 100644 --- a/snf-astakos-app/astakos/im/management/commands/reconcile-resources-astakos.py +++ b/snf-astakos-app/astakos/im/management/commands/reconcile-resources-astakos.py @@ -1,50 +1,33 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option +from datetime import datetime -from django.core.management.base import BaseCommand, CommandError -from django.db import transaction - +from snf_django.management.commands import SynnefoCommand, CommandError +from astakos.im import transaction +from snf_django.utils import reconcile from snf_django.management.utils import pprint_table from astakos.im.models import Component, AstakosUser -from astakos.im.quotas import service_get_quotas, SYSTEM +from astakos.im import quotas from astakos.im.functions import count_pending_app import astakos.quotaholder_app.callpoint as qh import astakos.quotaholder_app.exception as qh_exception -class Command(BaseCommand): +class Command(SynnefoCommand): help = """Reconcile resource usage of Quotaholder with Astakos DB. Detect unsynchronized usage between Quotaholder and Astakos DB resources @@ -52,10 +35,12 @@ class Command(BaseCommand): """ - option_list = BaseCommand.option_list + ( - make_option("--userid", dest="userid", + option_list = SynnefoCommand.option_list + ( + make_option("--user", dest="userid", default=None, help="Reconcile resources only for this user"), + make_option("--project", + help="Reconcile resources only for this project"), make_option("--fix", dest="fix", default=False, action="store_true", @@ -72,6 +57,9 @@ class Command(BaseCommand): write = self.stderr.write force = options['force'] userid = options['userid'] + project = options['project'] + + resources = [quotas.PENDING_APP_RESOURCE] try: astakos = Component.objects.get(name="astakos") @@ -79,10 +67,13 @@ class Command(BaseCommand): raise CommandError("Component 'astakos' not found.") query = [userid] if userid is not None else None - qh_holdings = service_get_quotas(astakos, query) + qh_holdings = quotas.service_get_quotas(astakos, query) + query = [project] if project is not None else None + qh_project_holdings = quotas.service_get_project_quotas(astakos, query) if userid is None: - users = AstakosUser.objects.accepted() + users = AstakosUser.objects.accepted().select_related( + 'base_project') else: try: user = AstakosUser.objects.get(uuid=userid) @@ -94,48 +85,35 @@ class Command(BaseCommand): db_holdings = count_pending_app(users) - pending_exists = False - unknown_user_exists = False - unsynced = [] - for user in users: - uuid = user.uuid - db_value = db_holdings.get(uuid, 0) - try: - qh_all = qh_holdings[uuid] - except KeyError: - write("User '%s' does not exist in Quotaholder!\n" % uuid) - unknown_user_exists = True - continue - - # Assuming only one source - system_qh = qh_all.get(SYSTEM, {}) - # Assuming only one resource - resource = 'astakos.pending_app' - try: - qh_values = system_qh[resource] - qh_value = qh_values['usage'] - qh_pending = qh_values['pending'] - except KeyError: - write("Resource '%s' does not exist in Quotaholder" - " for user '%s'!\n" % (resource, uuid)) - continue - if qh_pending: - write("Pending commission. User '%s', resource '%s'.\n" % - (uuid, resource)) - pending_exists = True - continue - if db_value != qh_value: - data = (uuid, resource, db_value, qh_value) - unsynced.append(data) - - headers = ("User", "Resource", "Astakos", "Quotaholder") + db_project_holdings = {} + for user, user_holdings in db_holdings.iteritems(): + db_project_holdings.update(user_holdings) + + unsynced_users, users_pending, users_unknown =\ + reconcile.check_users(self.stderr, resources, + db_holdings, qh_holdings) + + unsynced_projects, projects_pending, projects_unknown =\ + reconcile.check_projects(self.stderr, resources, + db_project_holdings, qh_project_holdings) + pending_exists = users_pending or projects_pending + unknown_exists = users_unknown or projects_unknown + + headers = ("Type", "Holder", "Source", "Resource", + "Astakos", "Quotaholder") + unsynced = unsynced_users + unsynced_projects if unsynced: pprint_table(self.stdout, unsynced, headers) if options["fix"]: - provisions = map(create_provision, unsynced) + user_provisions = create_user_provisions(unsynced_users) + project_provisions = create_project_provisions( + unsynced_projects) + provisions = user_provisions + project_provisions + name = ("client: reconcile-resources-astakos, time: %s" + % datetime.now()) try: s = qh.issue_commission('astakos', provisions, - name='RECONCILE', force=force) + name=name, force=force) except qh_exception.NoCapacityError: write("Reconciling failed because a limit has been " "reached. Use --force to ignore the check.\n") @@ -147,10 +125,23 @@ class Command(BaseCommand): if pending_exists: write("Found pending commissions. " "This is probably a bug. Please report.\n") - elif not (unsynced or unknown_user_exists): + elif not (unsynced or unknown_exists): write("Everything in sync.\n") -def create_provision(provision_info): - user, resource, db_value, qh_value = provision_info - return (user, SYSTEM, resource), (db_value - qh_value) +def create_user_provisions(provision_list): + provisions = [] + for _, holder, source, resource, db_value, qh_value in provision_list: + value = db_value - qh_value + provisions.append( + quotas.mk_user_provision(holder, source, resource, value)) + return provisions + + +def create_project_provisions(provision_list): + provisions = [] + for _, holder, _, resource, db_value, qh_value in provision_list: + value = db_value - qh_value + provisions.append( + quotas.mk_project_provision(holder, resource, value)) + return provisions diff --git a/snf-astakos-app/astakos/im/management/commands/resource-list.py b/snf-astakos-app/astakos/im/management/commands/resource-list.py index 101129699123ef969d4967358e6707d3ab51fdb4..5e6a50d80431479a59b1be7615f8e3d3c2794064 100644 --- a/snf-astakos-app/astakos/im/management/commands/resource-list.py +++ b/snf-astakos-app/astakos/im/management/commands/resource-list.py @@ -1,35 +1,17 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option from astakos.im.models import Resource @@ -54,7 +36,9 @@ class Command(ListCommand): "service_type": ("service_type", "Service type"), "service_origin": ("service_origin", "Service"), "unit": ("unit", "Unit of measurement"), - "default_quota": ("limit_with_unit", "Default Quota"), + "system_default": ("limit_with_unit", "System project default quota"), + "project_default": ("project_limit_with_unit", + "Project default quota"), "description": ("desc", "Description"), "api_visible": ("api_visible", "Resource accessibility through the API"), @@ -62,11 +46,8 @@ class Command(ListCommand): "Resource accessibility through the UI"), } - fields = ["id", "name", "default_quota", "api_visible", "ui_visible"] - - def show_limit(self, resource): - limit = resource.uplimit - return show_resource_value(limit, resource.name, self.unit_style) + fields = ["id", "name", "system_default", "project_default", + "api_visible", "ui_visible"] def handle_args(self, *args, **options): self.unit_style = options['unit_style'] @@ -74,4 +55,7 @@ class Command(ListCommand): def handle_db_objects(self, rows, *args, **kwargs): for resource in rows: - resource.limit_with_unit = self.show_limit(resource) + resource.limit_with_unit = show_resource_value( + resource.uplimit, resource.name, self.unit_style) + resource.project_limit_with_unit = show_resource_value( + resource.project_default, resource.name, self.unit_style) diff --git a/snf-astakos-app/astakos/im/management/commands/resource-modify.py b/snf-astakos-app/astakos/im/management/commands/resource-modify.py index 8abe2f5ee4156ac98d4067d4a637f4f22c679f62..1beae63f061a9bcce2ea7536f5c918701a4cc986 100644 --- a/snf-astakos-app/astakos/im/management/commands/resource-modify.py +++ b/snf-astakos-app/astakos/im/management/commands/resource-modify.py @@ -1,65 +1,38 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option -from django.core.management.base import BaseCommand, CommandError -from django.utils import simplejson as json +from snf_django.management.commands import SynnefoCommand, CommandError from snf_django.management import utils from astakos.im.models import Resource -from astakos.im.register import update_resources -from ._common import show_resource_value, style_options, check_style, units +from astakos.im import register +from ._common import style_options, check_style, units -class Command(BaseCommand): +class Command(SynnefoCommand): args = "<resource name>" - help = "Modify a resource's default base quota and boolean flags." + help = "Modify a resource's quota defaults and boolean flags." - option_list = BaseCommand.option_list + ( - make_option('--default-quota', + option_list = SynnefoCommand.option_list + ( + make_option('--system-default', metavar='<limit>', - help="Specify default base quota"), - make_option('--default-quota-interactive', - action='store_true', - default=None, - help=("Prompt user to change default base quota. " - "If no resource is given, prompts for all " - "resources.")), - make_option('--default-quota-from-file', - metavar='<limits_file.json>', - help=("Read default base quota from a file. " - "File should contain a json dict mapping resource " - "names to limits")), + help="Specify default quota for system projects"), + make_option('--project-default', + metavar='<limit>', + help="Specify default quota for non-system projects"), make_option('--unit-style', default='mb', help=("Specify display unit for resource values " @@ -74,11 +47,13 @@ class Command(BaseCommand): def handle(self, *args, **options): resource_name = args[0] if len(args) > 0 else None + if resource_name is None: + raise CommandError("Please provide a resource name.") + resource = self.get_resource(resource_name) actions = { - 'default_quota': self.change_limit, - 'default_quota_interactive': self.change_interactive, - 'default_quota_from_file': self.change_from_file, + 'system_default': self.change_base_default, + 'project_default': self.change_project_default, 'api_visible': self.set_api_visible, 'ui_visible': self.set_ui_visible, } @@ -87,44 +62,29 @@ class Command(BaseCommand): for (key, value) in options.items() if key in actions and value is not None] - if len(opts) != 1: - raise CommandError("Please provide exactly one of the options: " - "--default-quota, --default-quota-interactive, " - "--default-quota-from-file, " - "--api-visible, --ui-visible.") - self.unit_style = options['unit_style'] check_style(self.unit_style) - key, value = opts[0] - action = actions[key] - action(resource_name, value) - - def set_api_visible(self, resource_name, allow): - if resource_name is None: - raise CommandError("Please provide a resource name.") + for key, value in opts: + action = actions[key] + action(resource, value) + def set_api_visible(self, resource, allow): try: allow = utils.parse_bool(allow) except ValueError: raise CommandError("Expecting a boolean value.") - resource = self.get_resource(resource_name) resource.api_visible = allow if not allow and resource.ui_visible: self.stderr.write("Also resetting 'ui_visible' for consistency.\n") resource.ui_visible = False resource.save() - def set_ui_visible(self, resource_name, allow): - if resource_name is None: - raise CommandError("Please provide a resource name.") - + def set_ui_visible(self, resource, allow): try: allow = utils.parse_bool(allow) except ValueError: raise CommandError("Expecting a boolean value.") - resource = self.get_resource(resource_name) - resource.ui_visible = allow if allow and not resource.api_visible: self.stderr.write("Also setting 'api_visible' for consistency.\n") @@ -138,83 +98,18 @@ class Command(BaseCommand): raise CommandError("Resource %s does not exist." % resource_name) - def change_limit(self, resource_name, limit): - if resource_name is None: - raise CommandError("Please provide a resource name.") + def change_base_default(self, resource, limit): + limit = self.parse_limit(limit) + register.update_base_default(resource, limit) - resource = self.get_resource(resource_name) - self.change_resource_limit(resource, limit) - - def change_from_file(self, resource_name, filename): - with open(filename) as file_data: - try: - config = json.load(file_data) - except json.JSONDecodeError: - raise CommandError("Malformed JSON file.") - if not isinstance(config, dict): - raise CommandError("Malformed JSON file.") - self.change_with_conf(resource_name, config) - - def change_with_conf(self, resource_name, config): - if resource_name is None: - resources = Resource.objects.all().select_for_update() - else: - resources = [self.get_resource(resource_name)] - - updates = [] - for resource in resources: - limit = config.get(resource.name) - if limit is not None: - limit = self.parse_limit(limit) - updates.append((resource, limit)) - if updates: - update_resources(updates) - - def change_interactive(self, resource_name, _placeholder): - if resource_name is None: - resources = Resource.objects.all().select_for_update() - else: - resources = [self.get_resource(resource_name)] - - updates = [] - for resource in resources: - self.stdout.write("Resource '%s' (%s)\n" % - (resource.name, resource.desc)) - value = show_resource_value(resource.uplimit, resource.name, - self.unit_style) - self.stdout.write("Current limit: %s\n" % value) - while True: - self.stdout.write("New limit (leave blank to keep current): ") - try: - response = raw_input() - except EOFError: - self.stderr.write("Aborted.\n") - exit() - if response == "": - break - else: - try: - value = units.parse(response) - except units.ParseError: - continue - updates.append((resource, value)) - break - if updates: - self.stderr.write("Updating...\n") - update_resources(updates) + def change_project_default(self, resource, limit): + limit = self.parse_limit(limit) + register.update_project_default(resource, limit) def parse_limit(self, limit): try: - if isinstance(limit, (int, long)): - return limit - if isinstance(limit, basestring): - return units.parse(limit) - raise units.ParseError() + return units.parse(limit) except units.ParseError: - m = ("Limit should be an integer, optionally followed by a unit," - " or 'inf'.") + m = ("Quota limit should be an integer, " + "optionally followed by a unit, or 'inf'.") raise CommandError(m) - - def change_resource_limit(self, resource, limit): - limit = self.parse_limit(limit) - update_resources([(resource, limit)]) diff --git a/snf-astakos-app/astakos/im/management/commands/service-export-astakos.py b/snf-astakos-app/astakos/im/management/commands/service-export-astakos.py index 96cb2ce2dd5a997bfee2fa3cb208888ec0f15da6..94ff94e5495c3058cf3ea078860ed38fc7ace8dc 100644 --- a/snf-astakos-app/astakos/im/management/commands/service-export-astakos.py +++ b/snf-astakos-app/astakos/im/management/commands/service-export-astakos.py @@ -1,44 +1,27 @@ -# Copyright 2012-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.utils import simplejson as json -from django.core.management.base import NoArgsCommand # import from settings, after any post-processing + from astakos.im.settings import astakos_services from synnefo.lib.services import filter_public +from snf_django.management.commands import SynnefoCommand -class Command(NoArgsCommand): +class Command(SynnefoCommand): help = "Export Astakos services in JSON format." def handle(self, *args, **options): diff --git a/snf-astakos-app/astakos/im/management/commands/service-import.py b/snf-astakos-app/astakos/im/management/commands/service-import.py index d2a1b3d932d234c17218ab1a0f4d5a0815b4fce0..aa4ecb1c086f028a0186175f0eb58f9d8b24880f 100644 --- a/snf-astakos-app/astakos/im/management/commands/service-import.py +++ b/snf-astakos-app/astakos/im/management/commands/service-import.py @@ -1,40 +1,22 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option -from django.db import transaction -from django.core.management.base import BaseCommand, CommandError +from astakos.im import transaction +from snf_django.management.commands import SynnefoCommand, CommandError from django.utils import simplejson as json from astakos.im.register import add_service, add_resource, RegisterException @@ -42,10 +24,10 @@ from astakos.im.models import Component from ._common import read_from_file -class Command(BaseCommand): +class Command(SynnefoCommand): help = "Register services" - option_list = BaseCommand.option_list + ( + option_list = SynnefoCommand.option_list + ( make_option('--json', dest='json', metavar='<json.file>', diff --git a/snf-astakos-app/astakos/im/management/commands/service-list.py b/snf-astakos-app/astakos/im/management/commands/service-list.py index 8793aefb054db562b1f2049564f230491b06c8ec..edeca45806b1943e73368f70612c4c5edc3d3d4e 100644 --- a/snf-astakos-app/astakos/im/management/commands/service-list.py +++ b/snf-astakos-app/astakos/im/management/commands/service-list.py @@ -1,35 +1,17 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from astakos.im.models import Service from snf_django.management.commands import ListCommand diff --git a/snf-astakos-app/astakos/im/management/commands/service-show.py b/snf-astakos-app/astakos/im/management/commands/service-show.py index 3cb9a041d82517fad851d72988246f1901f60e12..334d40ca54e410397d5a76717bf3cff2a48b9262 100644 --- a/snf-astakos-app/astakos/im/management/commands/service-show.py +++ b/snf-astakos-app/astakos/im/management/commands/service-show.py @@ -1,40 +1,21 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. -from django.core.management.base import CommandError from astakos.im.models import Service, EndpointData from synnefo.lib.ordereddict import OrderedDict -from snf_django.management.commands import SynnefoCommand +from snf_django.management.commands import SynnefoCommand, CommandError from snf_django.management import utils diff --git a/snf-astakos-app/astakos/im/management/commands/stats-astakos.py b/snf-astakos-app/astakos/im/management/commands/stats-astakos.py index 3f811d7e0580385e6f3ad68630905cc859eac2f3..1d86d01d4dbed7096645d2cac90eee8c767f5d6f 100644 --- a/snf-astakos-app/astakos/im/management/commands/stats-astakos.py +++ b/snf-astakos-app/astakos/im/management/commands/stats-astakos.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from __future__ import division import json import string diff --git a/snf-astakos-app/astakos/im/management/commands/term-add.py b/snf-astakos-app/astakos/im/management/commands/term-add.py index 89e6842238d3080ca051ed9d0f4036944b56b619..a484e2ea478d85e3e54fa48977ff13aec09c84b1 100644 --- a/snf-astakos-app/astakos/im/management/commands/term-add.py +++ b/snf-astakos-app/astakos/im/management/commands/term-add.py @@ -1,45 +1,25 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from os.path import abspath - -from django.core.management.base import BaseCommand, CommandError -from django.db import transaction - +from snf_django.management.commands import SynnefoCommand, CommandError +from astakos.im import transaction from astakos.im.models import ApprovalTerms, AstakosUser -class Command(BaseCommand): +class Command(SynnefoCommand): args = "<location>" help = "Insert approval terms" diff --git a/snf-astakos-app/astakos/im/management/commands/user-activation-send.py b/snf-astakos-app/astakos/im/management/commands/user-activation-send.py index b7b06ed95aa0303f2cb963554ad1f5ca88096309..3ee9ab0b0b41f3371580aa0316a6bb95c62cda54 100644 --- a/snf-astakos-app/astakos/im/management/commands/user-activation-send.py +++ b/snf-astakos-app/astakos/im/management/commands/user-activation-send.py @@ -1,45 +1,25 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. - -from django.core.management.base import BaseCommand, CommandError +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. -from astakos.im import activation_backends -activation_backend = activation_backends.get_backend() +from snf_django.management.commands import SynnefoCommand, CommandError +from astakos.im.user_logic import send_verification_mail from ._common import get_user -class Command(BaseCommand): +class Command(SynnefoCommand): args = "<user ID or email> [user ID or email] ..." help = "Sends an activation email to one or more users" @@ -57,6 +37,6 @@ class Command(BaseCommand): "User email already verified '%s'\n" % (user.email,)) continue - activation_backend.send_user_verification_email(user) + send_verification_mail(user) self.stderr.write("Activation sent to '%s'\n" % (user.email,)) diff --git a/snf-astakos-app/astakos/im/management/commands/user-add.py b/snf-astakos-app/astakos/im/management/commands/user-add.py index 16ca7cd4654cbe647f7e10a4294868be9da577c5..2c8c58e656406b4cabfd864feb106658ba43103e 100644 --- a/snf-astakos-app/astakos/im/management/commands/user-add.py +++ b/snf-astakos-app/astakos/im/management/commands/user-add.py @@ -1,41 +1,22 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option -from datetime import datetime -from django.db import transaction -from django.core.management.base import BaseCommand, CommandError +from astakos.im import transaction +from snf_django.management.commands import SynnefoCommand, CommandError from django.core.validators import validate_email from django.core.exceptions import ValidationError @@ -43,11 +24,11 @@ from astakos.im.models import AstakosUser, get_latest_terms from astakos.im.auth import make_local_user -class Command(BaseCommand): +class Command(SynnefoCommand): args = "<email> <first name> <last name>" help = "Create a user" - option_list = BaseCommand.option_list + ( + option_list = SynnefoCommand.option_list + ( make_option('--password', dest='password', metavar='PASSWORD', @@ -74,8 +55,7 @@ class Command(BaseCommand): if len(args) != 3: raise CommandError("Invalid number of arguments") - email, first_name, last_name = map(lambda arg: arg.decode('utf8'), - args[:3]) + email, first_name, last_name = args[:3] password = options['password'] or \ AstakosUser.objects.make_random_password() @@ -98,9 +78,10 @@ class Command(BaseCommand): except BaseException, e: raise CommandError(e) else: - self.stdout.write('User created successfully ') + self.stdout.write('User created successfully with UUID: %s' + % user.uuid) if not options.get('password'): - self.stdout.write('with password: %s\n' % password) + self.stdout.write(' and password: %s\n' % password) else: self.stdout.write('\n') diff --git a/snf-astakos-app/astakos/im/management/commands/user-list.py b/snf-astakos-app/astakos/im/management/commands/user-list.py index 51306b8c61ec04a4b02c05f94c9f1d1c305011d9..4143f600a898459623d8f4a4b156e7cc6792722c 100644 --- a/snf-astakos-app/astakos/im/management/commands/user-list.py +++ b/snf-astakos-app/astakos/im/management/commands/user-list.py @@ -1,35 +1,17 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option @@ -54,7 +36,7 @@ class Command(ListCommand): FIELDS = { 'id': ('id', ('The id of the user')), - 'real name': ('realname', 'The name of the user'), + 'realname': ('realname', 'The name of the user'), 'active': ('is_active', 'Whether the user is active or not'), 'verified': ('email_verified', 'Whether the user has a verified email address'), @@ -67,11 +49,13 @@ class Command(ListCommand): 'activation_sent': ('activation_sent', 'The date activation sent to the user'), 'displayname': ('username', 'The display name of the user'), - 'groups': (get_groups, 'The groups of the user') + 'groups': (get_groups, 'The groups of the user'), + 'last_login_details': ('last_login_info_display', + 'User last login dates for each login method'), + 'last_login': ('last_login', 'User last login date') } - fields = ['id', 'real name', 'active', 'verified', 'moderated', 'admin', - 'uuid'] + fields = ['id', 'displayname', 'realname', 'uuid', 'active', 'admin'] option_list = ListCommand.option_list + ( make_option('--auth-providers', @@ -101,11 +85,11 @@ class Command(ListCommand): dest='pending_verification', default=False, help="Display unverified users"), - make_option("--displayname", + make_option("--display-mails", dest="displayname", action="store_true", default=False, - help="Display user displayname") + help="Display user email (enabled by default)") ) def handle_args(self, *args, **options): @@ -125,5 +109,6 @@ class Command(ListCommand): if options['auth_providers']: self.fields.extend(['providers']) - if options['displayname']: - self.fields.extend(['displayname']) + DISPLAYNAME = 'displayname' + if options[DISPLAYNAME] and DISPLAYNAME not in self.fields: + self.fields.extend([DISPLAYNAME]) diff --git a/snf-astakos-app/astakos/im/management/commands/user-modify.py b/snf-astakos-app/astakos/im/management/commands/user-modify.py index 12b444e588a3e86a101129b00a7572dea732a54b..2f3a42ea5d85681bbf4d6253604f784c02dc4371 100644 --- a/snf-astakos-app/astakos/im/management/commands/user-modify.py +++ b/snf-astakos-app/astakos/im/management/commands/user-modify.py @@ -1,35 +1,17 @@ -# Copyright 2012, 2013, 2014 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import string from datetime import datetime @@ -37,36 +19,22 @@ from datetime import datetime from optparse import make_option from django.core import management -from django.db import transaction -from django.core.management.base import BaseCommand, CommandError +from astakos.im import transaction +from snf_django.management.commands import SynnefoCommand, CommandError from django.contrib.auth.models import Group from django.core.exceptions import ValidationError from django.core.validators import validate_email -from synnefo.util import units -from astakos.im.models import AstakosUser, Resource -from astakos.im import quotas -from astakos.im import activation_backends -from ._common import (remove_user_permission, add_user_permission, is_uuid, - show_resource_value) - -activation_backend = activation_backends.get_backend() +from astakos.im.models import AstakosUser +from ._common import (remove_user_permission, add_user_permission, is_uuid) +from astakos.im import user_logic as user_action -class Command(BaseCommand): - args = "<user ID> (or --all)" +class Command(SynnefoCommand): + args = "<user ID>" help = "Modify a user's attributes" - option_list = BaseCommand.option_list + ( - make_option('--all', - action='store_true', - default=False, - help=("Operate on all users. Currently only setting " - "base quota is supported in this mode. Can be " - "combined with `--exclude'.")), - make_option('--exclude', - help=("If `--all' is given, exclude users given as a " - "list of uuids: uuid1,uuid2,uuid3")), + option_list = SynnefoCommand.option_list + ( make_option('--invitations', dest='invitations', metavar='NUM', @@ -143,14 +111,6 @@ class Command(BaseCommand): default=False, action='store_true', help="Sign terms"), - make_option('--base-quota', - dest='set_base_quota', - metavar='<resource> <capacity>', - nargs=2, - help=("Set base quota for a specified resource. " - "The special value 'default' sets the user base " - "quota to the default value.") - ), make_option('-f', '--no-confirm', action='store_true', default=False, @@ -167,18 +127,8 @@ class Command(BaseCommand): @transaction.commit_on_success def handle(self, *args, **options): - if options['all']: - if not args: - return self.handle_all_users(*args, **options) - else: - raise CommandError("Please provide a user ID or --all") - if len(args) != 1: - raise CommandError("Please provide a user ID or --all") - - if options["exclude"] is not None: - m = "Option --exclude is meaningful only combined with --all." - raise CommandError(m) + raise CommandError("Please provide a user ID") if args[0].isdigit(): try: @@ -195,7 +145,12 @@ class Command(BaseCommand): raise CommandError(("Invalid user identification: " "you should provide a valid user ID " "or a valid user UUID")) + try: + self.apply_actions(user, options) + except BaseException as e: + raise CommandError(e) + def apply_actions(self, user, options): if options.get('admin'): user.is_superuser = True elif options.get('noadmin'): @@ -203,49 +158,41 @@ class Command(BaseCommand): if options.get('reject'): reject_reason = options.get('reject_reason', None) - res = activation_backend.handle_moderation( - user, - accept=False, - reject_reason=reject_reason) - activation_backend.send_result_notifications(res, user) + res = user_action.reject(user, reject_reason) if res.is_error(): - print "Failed to reject.", res.message + self.stderr.write("Failed to reject: %s\n" % res.message) else: - print "Account rejected" + self.stderr.write("Account rejected\n") if options.get('verify'): - res = activation_backend.handle_verification( - user, - user.verification_code) - #activation_backend.send_result_notifications(res, user) + res = user_action.verify(user, user.verification_code) if res.is_error(): - print "Failed to verify.", res.message + self.stderr.write("Failed to verify: %s\n" % res.message) else: - print "Account verified (%s)" % res.status_display() + self.stderr.write("Account verified (%s)\n" + % res.status_display()) if options.get('accept'): - res = activation_backend.handle_moderation(user, accept=True) - activation_backend.send_result_notifications(res, user) + res = user_action.accept(user) if res.is_error(): - print "Failed to accept.", res.message + self.stderr.write("Failed to accept: %s\n" % res.message) else: - print "Account accepted and activated" + self.stderr.write("Account accepted and activated\n") if options.get('active'): - res = activation_backend.activate_user(user) + res = user_action.activate(user) if res.is_error(): - print "Failed to activate.", res.message + self.stderr.write("Failed to activate: %s\n" % res.message) else: - print "Account %s activated" % user.username + self.stderr.write("Account %s activated\n" % user.username) elif options.get('inactive'): - res = activation_backend.deactivate_user( - user, - reason=options.get('inactive_reason', None)) + inactive_reason = options.get('inactive_reason', None) + res = user_action.deactivate(user, inactive_reason) if res.is_error(): - print "Failed to deactivate,", res.message + self.stderr.write("Failed to deactivate: %s\n" % res.message) else: - print "Account %s deactivated" % user.username + self.stderr.write("Account %s deactivated\n" % user.username) invitations = options.get('invitations') if invitations is not None: @@ -329,15 +276,6 @@ class Command(BaseCommand): self.stdout.write('User\'s new password: %s\n' % password) force = options['force'] - - set_base_quota = options.get('set_base_quota') - if set_base_quota is not None: - if not user.is_accepted(): - m = "%s is not an accepted user." % user - raise CommandError(m) - resource, capacity = set_base_quota - self.set_limits([user], resource, capacity, force) - delete = options.get('delete') if delete: if user.is_accepted(): @@ -349,7 +287,9 @@ class Command(BaseCommand): if not force: self.stdout.write("About to delete user %s. " % user.uuid) self.confirm() + user.delete() + user.base_project and user.base_project.delete() # Change users email address newemail = options.get('set-email', None) @@ -377,67 +317,3 @@ class Command(BaseCommand): if string.lower(response) not in ['y', 'yes']: self.stderr.write("Aborted.\n") exit() - - def handle_limits_user(self, user, res, capacity, style): - default_capacity = res.uplimit - resource = res.name - quota = user.get_resource_policy(resource) - s_default = show_resource_value(default_capacity, resource, style) - s_current = show_resource_value(quota.capacity, resource, style) - s_capacity = (show_resource_value(capacity, resource, style) - if capacity != 'default' else capacity) - self.stdout.write("user: %s (%s)\n" % (user.uuid, user.username)) - self.stdout.write("default capacity: %s\n" % s_default) - self.stdout.write("current capacity: %s\n" % s_current) - self.stdout.write("new capacity: %s\n" % s_capacity) - self.confirm() - - def handle_limits_all(self, res, capacity, exclude, style): - m = "This will set base quota for all users" - app = (" except %s" % ", ".join(exclude)) if exclude else "" - self.stdout.write(m+app+".\n") - resource = res.name - self.stdout.write("resource: %s\n" % resource) - s_capacity = (show_resource_value(capacity, resource, style) - if capacity != 'default' else capacity) - self.stdout.write("capacity: %s\n" % s_capacity) - self.confirm() - - def set_limits(self, users, resource, capacity, force=False, exclude=None): - try: - r = Resource.objects.get(name=resource) - except Resource.DoesNotExist: - raise CommandError("No such resource '%s'." % resource) - - style = None - if capacity != "default": - try: - capacity, style = units.parse_with_style(capacity) - except: - m = ("Please specify capacity as a decimal integer or " - "'default'") - raise CommandError(m) - - if not force: - if len(users) == 1: - self.handle_limits_user(users[0], r, capacity, style) - else: - self.handle_limits_all(r, capacity, exclude, style) - - if capacity == "default": - capacity = r.uplimit - quotas.update_base_quota(users, resource, capacity) - - def handle_all_users(self, *args, **options): - force = options["force"] - exclude = options["exclude"] - if exclude is not None: - exclude = exclude.split(',') - - set_base_quota = options.get('set_base_quota') - if set_base_quota is not None: - users = AstakosUser.objects.accepted().select_for_update() - if exclude: - users = users.exclude(uuid__in=exclude) - resource, capacity = set_base_quota - self.set_limits(users, resource, capacity, force, exclude) diff --git a/snf-astakos-app/astakos/im/management/commands/user-show.py b/snf-astakos-app/astakos/im/management/commands/user-show.py index ba6778b4bf7db1264bef4f5cf406ed55362a7fa7..e400468be30cf9dc8abeab7be7a4e0ec68a674e8 100644 --- a/snf-astakos-app/astakos/im/management/commands/user-show.py +++ b/snf-astakos-app/astakos/im/management/commands/user-show.py @@ -1,45 +1,24 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. -from django.core.management.base import CommandError from optparse import make_option - -from django.db.models import Q from astakos.im.models import AstakosUser, get_latest_terms, Project -from astakos.im.quotas import list_user_quotas +from astakos.im.quotas import get_user_quotas from synnefo.lib.ordereddict import OrderedDict -from snf_django.management.commands import SynnefoCommand +from snf_django.management.commands import SynnefoCommand, CommandError from snf_django.management import utils from ._common import show_user_quotas, style_options, check_style @@ -96,7 +75,6 @@ class Command(SynnefoCommand): ('email', user.email), ('first name', user.first_name), ('last name', user.last_name), - ('active', user.is_active), ('admin', user.is_superuser), ('last login', user.last_login), ('date joined', user.date_joined), @@ -104,14 +82,17 @@ class Command(SynnefoCommand): #('token', user.auth_token), ('token expiration', user.auth_token_expires), ('providers', user.auth_providers_display), - ('verified', user.is_verified), ('groups', [elem.name for elem in user.groups.all()]), ('permissions', [elem.codename for elem in user.user_permissions.all()]), ('group permissions', user.get_group_permissions()), - ('email verified', user.email_verified), + ('email_verified', user.email_verified), + ('moderated', user.moderated), + ('rejected', user.is_rejected), + ('active', user.is_active), ('username', user.username), ('activation_sent_date', user.activation_sent), + ('last_login_details', user.last_login_info_display), ]) if get_latest_terms(): @@ -127,12 +108,10 @@ class Command(SynnefoCommand): unit_style = options["unit_style"] check_style(unit_style) - quotas, initial = list_user_quotas([user]) - h_quotas = quotas[user.uuid] - h_initial = initial[user.uuid] + quotas = get_user_quotas(user) if quotas: self.stdout.write("\n") - print_data, labels = show_user_quotas(h_quotas, h_initial, + print_data, labels = show_user_quotas(quotas, style=unit_style) utils.pprint_table(self.stdout, print_data, labels, options["output_format"], @@ -161,28 +140,29 @@ def memberships(user): for m in ms: project = m.project - print_data.append((project.id, - project.application.name, + print_data.append((project.uuid, + project.realname, m.state_display(), )) return print_data, labels def ownerships(user): - chains = Project.objects.all_with_pending(Q(application__owner=user)) + chains = Project.objects.select_related("last_application").\ + filter(owner=user) return chain_info(chains) def chain_info(chains): labels = ('project id', 'project name', 'status', 'pending app id') l = [] - for project, pending_app in chains: + for project in chains: status = project.state_display() - pending_appid = pending_app.id if pending_app is not None else "" - application = project.application + app = project.last_application + pending_appid = app.id if app and app.state == app.PENDING else "" - t = (project.pk, - application.name, + t = (project.uuid, + project.realname, status, pending_appid, ) diff --git a/snf-astakos-app/astakos/im/messages.py b/snf-astakos-app/astakos/im/messages.py index a165cf060568fc4523b156586a227404e084f1c8..380c355db9f241309b06733dc17d04165745fa2a 100644 --- a/snf-astakos-app/astakos/im/messages.py +++ b/snf-astakos-app/astakos/im/messages.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.conf import settings import astakos.im.settings as astakos_settings @@ -221,13 +203,16 @@ NOT_ALLOWED_NEXT_PARAM = 'Not allowed next parameter.' MISSING_KEY_PARAMETER = 'Missing key parameter.' INVALID_KEY_PARAMETER = 'Invalid key.' DOMAIN_VALUE_ERR = 'Enter a valid domain.' +BASE_PROJECT_NAME_ERR = 'Enter a valid system project name.' QH_SYNC_ERROR = 'Failed to get synchronized with quotaholder.' UNIQUE_PROJECT_NAME_CONSTRAIN_ERR = ( 'The project name (as specified in its application\'s definition) must ' 'be unique among all active projects.') NOT_ALIVE_PROJECT = 'Project %s is not alive.' +SUSPENDED_PROJECT = 'Project %s is suspended.' NOT_SUSPENDED_PROJECT = 'Project %s is not suspended.' NOT_TERMINATED_PROJECT = 'Project %s is not terminated.' +BASE_NO_TERMINATE = "Cannot terminate: %s is a system project." NOT_ALLOWED = 'You do not have the permissions to perform this action.' MEMBER_NUMBER_LIMIT_REACHED = ( 'You have reached the maximum number of members for this Project.') @@ -271,8 +256,13 @@ APPLICATION_CANNOT_DENY = "Cannot deny application %s in state '%s'" APPLICATION_CANNOT_DISMISS = "Cannot dismiss application %s in state '%s'" APPLICATION_CANNOT_CANCEL = "Cannot cancel application %s in state '%s'" APPLICATION_CANCELLED = "Your project application has been cancelled." +APPLICATION_APPROVED = "Project application has been approved." +APPLICATION_DENIED = "Project application has been denied." +APPLICATION_DISMISSED = "Project application has been dismissed." REACHED_PENDING_APPLICATION_LIMIT = ("You have reached the maximum number " "of pending project applications: %s.") +UNINITIALIZED_NO_MODIFY = "Cannot modify: project %s is not initialized." +BASE_NO_MODIFY_FIELDS = "Cannot modify field(s) '%s' of system projects." PENDING_APPLICATION_LIMIT_ADD = \ ("You are not allowed to create a new project " @@ -342,6 +332,7 @@ AUTH_PROVIDER_ADD_TO_EXISTING_ACCOUNT = ( # Email subjects _SITENAME = astakos_settings.SITENAME +PLAIN_EMAIL_SUBJECT = 'New email from %s' % _SITENAME INVITATION_EMAIL_SUBJECT = 'Invitation to %s' % _SITENAME GREETING_EMAIL_SUBJECT = 'Welcome to %s' % _SITENAME FEEDBACK_EMAIL_SUBJECT = 'Feedback from %s' % _SITENAME @@ -352,19 +343,21 @@ HELPDESK_NOTIFICATION_EMAIL_SUBJECT = \ EMAIL_CHANGE_EMAIL_SUBJECT = 'Email change on %s ' % _SITENAME PASSWORD_RESET_EMAIL_SUBJECT = 'Password reset on %s ' % _SITENAME PROJECT_CREATION_SUBJECT = \ - '%s project application created (%%(name)s)' % _SITENAME + '%s application for a new project created (%%s)' % _SITENAME +PROJECT_MODIFICATION_SUBJECT = \ + '%s application for a project modification created (%%s)' % _SITENAME PROJECT_APPROVED_SUBJECT = \ - '%s project application approved (%%(name)s)' % _SITENAME + '%s project application approved (%%s)' % _SITENAME PROJECT_DENIED_SUBJECT = \ - '%s project application denied (%%(name)s)' % _SITENAME + '%s project application denied (%%s)' % _SITENAME PROJECT_TERMINATION_SUBJECT = \ - '%s project terminated (%%(name)s)' % _SITENAME + '%s project terminated (%%s)' % _SITENAME PROJECT_SUSPENSION_SUBJECT = \ - '%s project suspended (%%(name)s)' % _SITENAME + '%s project suspended (%%s)' % _SITENAME PROJECT_UNSUSPENSION_SUBJECT = \ - '%s project resumed (%%(name)s)' % _SITENAME + '%s project resumed (%%s)' % _SITENAME PROJECT_REINSTATEMENT_SUBJECT = \ - '%s project reinstated (%%(name)s)' % _SITENAME + '%s project reinstated (%%s)' % _SITENAME PROJECT_MEMBERSHIP_CHANGE_SUBJECT = \ '%s project membership changed (%%(name)s)' % _SITENAME PROJECT_MEMBERSHIP_ENROLL_SUBJECT = \ diff --git a/snf-astakos-app/astakos/im/migrations/0063_auto__add_field_projectapplication_private.py b/snf-astakos-app/astakos/im/migrations/0063_auto__add_field_projectapplication_private.py new file mode 100644 index 0000000000000000000000000000000000000000..4fe8d705a5f6f106cebc02b922037d1fb7459fa1 --- /dev/null +++ b/snf-astakos-app/astakos/im/migrations/0063_auto__add_field_projectapplication_private.py @@ -0,0 +1,309 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'ProjectApplication.private' + db.add_column('im_projectapplication', 'private', + self.gf('django.db.models.fields.BooleanField')(default=False), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'ProjectApplication.private' + db.delete_column('im_projectapplication', 'private') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'im.additionalmail': { + 'Meta': {'object_name': 'AdditionalMail'}, + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.approvalterms': { + 'Meta': {'object_name': 'ApprovalTerms'}, + 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.astakosuser': { + 'Meta': {'object_name': 'AstakosUser', '_ormbases': ['auth.User']}, + 'accepted_email': ('django.db.models.fields.EmailField', [], {'default': 'None', 'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'accepted_policy': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'activation_sent': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date_signed_terms': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_reason': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}), + 'disturbed_quota': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'email_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_credits': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_signed_terms': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'invitations': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'is_rejected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'level': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'moderated_at': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'moderated_data': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.AstakosUserQuota']", 'symmetrical': 'False'}), + 'rejected_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {}), + 'user_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'primary_key': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'verification_code': ('django.db.models.fields.CharField', [], {'max_length': '255', 'unique': 'True', 'null': 'True'}), + 'verified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.astakosuserauthprovider': { + 'Meta': {'ordering': "('module', 'created')", 'unique_together': "(('identifier', 'module', 'user'),)", 'object_name': 'AstakosUserAuthProvider'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_backend': ('django.db.models.fields.CharField', [], {'default': "'astakos'", 'max_length': '255'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'info_data': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'module': ('django.db.models.fields.CharField', [], {'default': "'local'", 'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'auth_providers'", 'to': "orm['im.AstakosUser']"}) + }, + 'im.astakosuserquota': { + 'Meta': {'unique_together': "(('resource', 'user'),)", 'object_name': 'AstakosUserQuota'}, + 'capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.authproviderpolicyprofile': { + 'Meta': {'ordering': "['priority']", 'object_name': 'AuthProviderPolicyProfile'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['auth.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_exclusive': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'policy_add': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_automoderate': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_create': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_limit': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}), + 'policy_login': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_remove': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_required': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_switch': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'priority': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['im.AstakosUser']"}) + }, + 'im.chain': { + 'Meta': {'object_name': 'Chain'}, + 'chain': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.component': { + 'Meta': {'object_name': 'Component'}, + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'base_url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}) + }, + 'im.emailchange': { + 'Meta': {'object_name': 'EmailChange'}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'requested_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'emailchanges'", 'unique': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.endpoint': { + 'Meta': {'object_name': 'Endpoint'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'service': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'endpoints'", 'to': "orm['im.Service']"}) + }, + 'im.endpointdata': { + 'Meta': {'unique_together': "(('endpoint', 'key'),)", 'object_name': 'EndpointData'}, + 'endpoint': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'data'", 'to': "orm['im.Endpoint']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '1024'}) + }, + 'im.invitation': { + 'Meta': {'object_name': 'Invitation'}, + 'code': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'consumed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inviter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invitations_sent'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'is_consumed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'realname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.pendingthirdpartyuser': { + 'Meta': {'unique_together': "(('provider', 'third_party_identifier'),)", 'object_name': 'PendingThirdPartyUser'}, + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'info': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'third_party_identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'token': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'im.project': { + 'Meta': {'object_name': 'Project'}, + 'application': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'project'", 'unique': 'True', 'to': "orm['im.ProjectApplication']"}), + 'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_column': "'id'"}), + 'members': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.AstakosUser']", 'through': "orm['im.ProjectMembership']", 'symmetrical': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True', 'null': 'True', 'db_index': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '1', 'db_index': 'True'}) + }, + 'im.projectapplication': { + 'Meta': {'unique_together': "(('chain', 'id'),)", 'object_name': 'ProjectApplication'}, + 'applicant': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_applied'", 'to': "orm['im.AstakosUser']"}), + 'chain': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'chained_apps'", 'db_column': "'chain'", 'to': "orm['im.Project']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'end_date': ('django.db.models.fields.DateTimeField', [], {}), + 'homepage': ('django.db.models.fields.URLField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'issue_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'limit_on_members_number': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'member_join_policy': ('django.db.models.fields.IntegerField', [], {}), + 'member_leave_policy': ('django.db.models.fields.IntegerField', [], {}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_owned'", 'to': "orm['im.AstakosUser']"}), + 'private': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'resource_grants': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.ProjectResourceGrant']", 'blank': 'True'}), + 'response': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'response_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'responded_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'response_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'start_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}), + 'waive_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'waived_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'waive_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'waive_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.projectlock': { + 'Meta': {'object_name': 'ProjectLock'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.projectlog': { + 'Meta': {'object_name': 'ProjectLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.Project']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectmembership': { + 'Meta': {'unique_together': "(('person', 'project'),)", 'object_name': 'ProjectMembership'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Project']"}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}) + }, + 'im.projectmembershiplog': { + 'Meta': {'object_name': 'ProjectMembershipLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'membership': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.ProjectMembership']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectresourcegrant': { + 'Meta': {'unique_together': "(('resource', 'project_application'),)", 'object_name': 'ProjectResourceGrant'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'member_capacity': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'project_application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.ProjectApplication']", 'null': 'True'}), + 'project_capacity': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}) + }, + 'im.resource': { + 'Meta': {'object_name': 'Resource'}, + 'api_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'desc': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'service_origin': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'service_type': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'ui_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'unit': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'uplimit': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) + }, + 'im.service': { + 'Meta': {'object_name': 'Service'}, + 'component': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Component']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.sessioncatalog': { + 'Meta': {'object_name': 'SessionCatalog'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.usersetting': { + 'Meta': {'unique_together': "(('user', 'setting'),)", 'object_name': 'UserSetting'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'setting': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'value': ('django.db.models.fields.IntegerField', [], {}) + } + } + + complete_apps = ['im'] \ No newline at end of file diff --git a/snf-astakos-app/astakos/im/migrations/0064_auto__add_field_project_uuid.py b/snf-astakos-app/astakos/im/migrations/0064_auto__add_field_project_uuid.py new file mode 100644 index 0000000000000000000000000000000000000000..40b760740806a5bfcf3ef54e3b62605df0171012 --- /dev/null +++ b/snf-astakos-app/astakos/im/migrations/0064_auto__add_field_project_uuid.py @@ -0,0 +1,310 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'Project.uuid' + db.add_column('im_project', 'uuid', + self.gf('django.db.models.fields.CharField')(max_length=255, unique=True, null=True), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'Project.uuid' + db.delete_column('im_project', 'uuid') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'im.additionalmail': { + 'Meta': {'object_name': 'AdditionalMail'}, + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.approvalterms': { + 'Meta': {'object_name': 'ApprovalTerms'}, + 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.astakosuser': { + 'Meta': {'object_name': 'AstakosUser', '_ormbases': ['auth.User']}, + 'accepted_email': ('django.db.models.fields.EmailField', [], {'default': 'None', 'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'accepted_policy': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'activation_sent': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date_signed_terms': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_reason': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}), + 'disturbed_quota': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'email_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_credits': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_signed_terms': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'invitations': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'is_rejected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'level': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'moderated_at': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'moderated_data': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.AstakosUserQuota']", 'symmetrical': 'False'}), + 'rejected_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {}), + 'user_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'primary_key': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'verification_code': ('django.db.models.fields.CharField', [], {'max_length': '255', 'unique': 'True', 'null': 'True'}), + 'verified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.astakosuserauthprovider': { + 'Meta': {'ordering': "('module', 'created')", 'unique_together': "(('identifier', 'module', 'user'),)", 'object_name': 'AstakosUserAuthProvider'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_backend': ('django.db.models.fields.CharField', [], {'default': "'astakos'", 'max_length': '255'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'info_data': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'module': ('django.db.models.fields.CharField', [], {'default': "'local'", 'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'auth_providers'", 'to': "orm['im.AstakosUser']"}) + }, + 'im.astakosuserquota': { + 'Meta': {'unique_together': "(('resource', 'user'),)", 'object_name': 'AstakosUserQuota'}, + 'capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.authproviderpolicyprofile': { + 'Meta': {'ordering': "['priority']", 'object_name': 'AuthProviderPolicyProfile'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['auth.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_exclusive': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'policy_add': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_automoderate': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_create': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_limit': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}), + 'policy_login': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_remove': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_required': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_switch': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'priority': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['im.AstakosUser']"}) + }, + 'im.chain': { + 'Meta': {'object_name': 'Chain'}, + 'chain': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.component': { + 'Meta': {'object_name': 'Component'}, + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'base_url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}) + }, + 'im.emailchange': { + 'Meta': {'object_name': 'EmailChange'}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'requested_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'emailchanges'", 'unique': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.endpoint': { + 'Meta': {'object_name': 'Endpoint'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'service': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'endpoints'", 'to': "orm['im.Service']"}) + }, + 'im.endpointdata': { + 'Meta': {'unique_together': "(('endpoint', 'key'),)", 'object_name': 'EndpointData'}, + 'endpoint': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'data'", 'to': "orm['im.Endpoint']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '1024'}) + }, + 'im.invitation': { + 'Meta': {'object_name': 'Invitation'}, + 'code': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'consumed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inviter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invitations_sent'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'is_consumed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'realname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.pendingthirdpartyuser': { + 'Meta': {'unique_together': "(('provider', 'third_party_identifier'),)", 'object_name': 'PendingThirdPartyUser'}, + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'info': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'third_party_identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'token': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'im.project': { + 'Meta': {'object_name': 'Project'}, + 'application': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'project'", 'unique': 'True', 'to': "orm['im.ProjectApplication']"}), + 'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_column': "'id'"}), + 'members': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.AstakosUser']", 'through': "orm['im.ProjectMembership']", 'symmetrical': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True', 'null': 'True', 'db_index': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '1', 'db_index': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'max_length': '255', 'unique': 'True', 'null': 'True'}) + }, + 'im.projectapplication': { + 'Meta': {'unique_together': "(('chain', 'id'),)", 'object_name': 'ProjectApplication'}, + 'applicant': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_applied'", 'to': "orm['im.AstakosUser']"}), + 'chain': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'chained_apps'", 'db_column': "'chain'", 'to': "orm['im.Project']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'end_date': ('django.db.models.fields.DateTimeField', [], {}), + 'homepage': ('django.db.models.fields.URLField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'issue_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'limit_on_members_number': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'member_join_policy': ('django.db.models.fields.IntegerField', [], {}), + 'member_leave_policy': ('django.db.models.fields.IntegerField', [], {}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_owned'", 'to': "orm['im.AstakosUser']"}), + 'private': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'resource_grants': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.ProjectResourceGrant']", 'blank': 'True'}), + 'response': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'response_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'responded_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'response_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'start_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}), + 'waive_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'waived_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'waive_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'waive_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.projectlock': { + 'Meta': {'object_name': 'ProjectLock'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.projectlog': { + 'Meta': {'object_name': 'ProjectLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.Project']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectmembership': { + 'Meta': {'unique_together': "(('person', 'project'),)", 'object_name': 'ProjectMembership'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Project']"}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}) + }, + 'im.projectmembershiplog': { + 'Meta': {'object_name': 'ProjectMembershipLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'membership': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.ProjectMembership']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectresourcegrant': { + 'Meta': {'unique_together': "(('resource', 'project_application'),)", 'object_name': 'ProjectResourceGrant'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'member_capacity': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'project_application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.ProjectApplication']", 'null': 'True'}), + 'project_capacity': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}) + }, + 'im.resource': { + 'Meta': {'object_name': 'Resource'}, + 'api_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'desc': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'service_origin': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'service_type': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'ui_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'unit': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'uplimit': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) + }, + 'im.service': { + 'Meta': {'object_name': 'Service'}, + 'component': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Component']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.sessioncatalog': { + 'Meta': {'object_name': 'SessionCatalog'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.usersetting': { + 'Meta': {'unique_together': "(('user', 'setting'),)", 'object_name': 'UserSetting'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'setting': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'value': ('django.db.models.fields.IntegerField', [], {}) + } + } + + complete_apps = ['im'] \ No newline at end of file diff --git a/snf-astakos-app/astakos/im/migrations/0065_project_uuid.py b/snf-astakos-app/astakos/im/migrations/0065_project_uuid.py new file mode 100644 index 0000000000000000000000000000000000000000..0a649cc428f6cc735442c7a259a077f142f9aa98 --- /dev/null +++ b/snf-astakos-app/astakos/im/migrations/0065_project_uuid.py @@ -0,0 +1,317 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models +import uuid +import itertools + + +class Migration(DataMigration): + + def forwards(self, orm): + users = orm.AstakosUser.objects.all() + current_uuids = set(u.uuid for u in users) + projects = orm.Project.objects.all() + for project in projects: + while True: + u = str(uuid.uuid4()) + if not u in current_uuids: + current_uuids.add(u) + project.uuid = u + project.save() + break + + def backwards(self, orm): + "Write your backwards methods here." + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'im.additionalmail': { + 'Meta': {'object_name': 'AdditionalMail'}, + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.approvalterms': { + 'Meta': {'object_name': 'ApprovalTerms'}, + 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.astakosuser': { + 'Meta': {'object_name': 'AstakosUser', '_ormbases': ['auth.User']}, + 'accepted_email': ('django.db.models.fields.EmailField', [], {'default': 'None', 'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'accepted_policy': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'activation_sent': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date_signed_terms': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_reason': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}), + 'disturbed_quota': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'email_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_credits': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_signed_terms': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'invitations': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'is_rejected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'level': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'moderated_at': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'moderated_data': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.AstakosUserQuota']", 'symmetrical': 'False'}), + 'rejected_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {}), + 'user_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'primary_key': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'verification_code': ('django.db.models.fields.CharField', [], {'max_length': '255', 'unique': 'True', 'null': 'True'}), + 'verified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.astakosuserauthprovider': { + 'Meta': {'ordering': "('module', 'created')", 'unique_together': "(('identifier', 'module', 'user'),)", 'object_name': 'AstakosUserAuthProvider'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_backend': ('django.db.models.fields.CharField', [], {'default': "'astakos'", 'max_length': '255'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'info_data': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'module': ('django.db.models.fields.CharField', [], {'default': "'local'", 'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'auth_providers'", 'to': "orm['im.AstakosUser']"}) + }, + 'im.astakosuserquota': { + 'Meta': {'unique_together': "(('resource', 'user'),)", 'object_name': 'AstakosUserQuota'}, + 'capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.authproviderpolicyprofile': { + 'Meta': {'ordering': "['priority']", 'object_name': 'AuthProviderPolicyProfile'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['auth.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_exclusive': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'policy_add': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_automoderate': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_create': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_limit': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}), + 'policy_login': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_remove': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_required': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_switch': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'priority': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['im.AstakosUser']"}) + }, + 'im.chain': { + 'Meta': {'object_name': 'Chain'}, + 'chain': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.component': { + 'Meta': {'object_name': 'Component'}, + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'base_url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}) + }, + 'im.emailchange': { + 'Meta': {'object_name': 'EmailChange'}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'requested_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'emailchanges'", 'unique': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.endpoint': { + 'Meta': {'object_name': 'Endpoint'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'service': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'endpoints'", 'to': "orm['im.Service']"}) + }, + 'im.endpointdata': { + 'Meta': {'unique_together': "(('endpoint', 'key'),)", 'object_name': 'EndpointData'}, + 'endpoint': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'data'", 'to': "orm['im.Endpoint']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '1024'}) + }, + 'im.invitation': { + 'Meta': {'object_name': 'Invitation'}, + 'code': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'consumed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inviter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invitations_sent'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'is_consumed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'realname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.pendingthirdpartyuser': { + 'Meta': {'unique_together': "(('provider', 'third_party_identifier'),)", 'object_name': 'PendingThirdPartyUser'}, + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'info': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'third_party_identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'token': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'im.project': { + 'Meta': {'object_name': 'Project'}, + 'application': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'project'", 'unique': 'True', 'to': "orm['im.ProjectApplication']"}), + 'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_column': "'id'"}), + 'members': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.AstakosUser']", 'through': "orm['im.ProjectMembership']", 'symmetrical': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True', 'null': 'True', 'db_index': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '1', 'db_index': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'max_length': '255', 'unique': 'True', 'null': 'True'}) + }, + 'im.projectapplication': { + 'Meta': {'unique_together': "(('chain', 'id'),)", 'object_name': 'ProjectApplication'}, + 'applicant': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_applied'", 'to': "orm['im.AstakosUser']"}), + 'chain': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'chained_apps'", 'db_column': "'chain'", 'to': "orm['im.Project']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'end_date': ('django.db.models.fields.DateTimeField', [], {}), + 'homepage': ('django.db.models.fields.URLField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'issue_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'limit_on_members_number': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'member_join_policy': ('django.db.models.fields.IntegerField', [], {}), + 'member_leave_policy': ('django.db.models.fields.IntegerField', [], {}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_owned'", 'to': "orm['im.AstakosUser']"}), + 'private': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'resource_grants': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.ProjectResourceGrant']", 'blank': 'True'}), + 'response': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'response_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'responded_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'response_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'start_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}), + 'waive_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'waived_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'waive_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'waive_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.projectlock': { + 'Meta': {'object_name': 'ProjectLock'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.projectlog': { + 'Meta': {'object_name': 'ProjectLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.Project']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectmembership': { + 'Meta': {'unique_together': "(('person', 'project'),)", 'object_name': 'ProjectMembership'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Project']"}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}) + }, + 'im.projectmembershiplog': { + 'Meta': {'object_name': 'ProjectMembershipLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'membership': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.ProjectMembership']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectresourcegrant': { + 'Meta': {'unique_together': "(('resource', 'project_application'),)", 'object_name': 'ProjectResourceGrant'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'member_capacity': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'project_application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.ProjectApplication']", 'null': 'True'}), + 'project_capacity': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}) + }, + 'im.resource': { + 'Meta': {'object_name': 'Resource'}, + 'api_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'desc': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'service_origin': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'service_type': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'ui_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'unit': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'uplimit': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) + }, + 'im.service': { + 'Meta': {'object_name': 'Service'}, + 'component': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Component']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.sessioncatalog': { + 'Meta': {'object_name': 'SessionCatalog'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.usersetting': { + 'Meta': {'unique_together': "(('user', 'setting'),)", 'object_name': 'UserSetting'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'setting': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'value': ('django.db.models.fields.IntegerField', [], {}) + } + } + + complete_apps = ['im'] + symmetrical = True diff --git a/snf-astakos-app/astakos/im/migrations/0066_auto__chg_field_project_uuid.py b/snf-astakos-app/astakos/im/migrations/0066_auto__chg_field_project_uuid.py new file mode 100644 index 0000000000000000000000000000000000000000..1373cfbf0f25a6421c4bcde988bacf43277dbcce --- /dev/null +++ b/snf-astakos-app/astakos/im/migrations/0066_auto__chg_field_project_uuid.py @@ -0,0 +1,308 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Changing field 'Project.uuid' + db.alter_column('im_project', 'uuid', self.gf('django.db.models.fields.CharField')(default=None, unique=True, max_length=255)) + + def backwards(self, orm): + + # Changing field 'Project.uuid' + db.alter_column('im_project', 'uuid', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255, null=True)) + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'im.additionalmail': { + 'Meta': {'object_name': 'AdditionalMail'}, + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.approvalterms': { + 'Meta': {'object_name': 'ApprovalTerms'}, + 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.astakosuser': { + 'Meta': {'object_name': 'AstakosUser', '_ormbases': ['auth.User']}, + 'accepted_email': ('django.db.models.fields.EmailField', [], {'default': 'None', 'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'accepted_policy': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'activation_sent': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date_signed_terms': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_reason': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}), + 'disturbed_quota': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'email_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_credits': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_signed_terms': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'invitations': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'is_rejected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'level': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'moderated_at': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'moderated_data': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.AstakosUserQuota']", 'symmetrical': 'False'}), + 'rejected_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {}), + 'user_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'primary_key': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'verification_code': ('django.db.models.fields.CharField', [], {'max_length': '255', 'unique': 'True', 'null': 'True'}), + 'verified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.astakosuserauthprovider': { + 'Meta': {'ordering': "('module', 'created')", 'unique_together': "(('identifier', 'module', 'user'),)", 'object_name': 'AstakosUserAuthProvider'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_backend': ('django.db.models.fields.CharField', [], {'default': "'astakos'", 'max_length': '255'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'info_data': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'module': ('django.db.models.fields.CharField', [], {'default': "'local'", 'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'auth_providers'", 'to': "orm['im.AstakosUser']"}) + }, + 'im.astakosuserquota': { + 'Meta': {'unique_together': "(('resource', 'user'),)", 'object_name': 'AstakosUserQuota'}, + 'capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.authproviderpolicyprofile': { + 'Meta': {'ordering': "['priority']", 'object_name': 'AuthProviderPolicyProfile'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['auth.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_exclusive': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'policy_add': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_automoderate': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_create': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_limit': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}), + 'policy_login': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_remove': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_required': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_switch': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'priority': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['im.AstakosUser']"}) + }, + 'im.chain': { + 'Meta': {'object_name': 'Chain'}, + 'chain': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.component': { + 'Meta': {'object_name': 'Component'}, + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'base_url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}) + }, + 'im.emailchange': { + 'Meta': {'object_name': 'EmailChange'}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'requested_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'emailchanges'", 'unique': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.endpoint': { + 'Meta': {'object_name': 'Endpoint'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'service': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'endpoints'", 'to': "orm['im.Service']"}) + }, + 'im.endpointdata': { + 'Meta': {'unique_together': "(('endpoint', 'key'),)", 'object_name': 'EndpointData'}, + 'endpoint': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'data'", 'to': "orm['im.Endpoint']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '1024'}) + }, + 'im.invitation': { + 'Meta': {'object_name': 'Invitation'}, + 'code': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'consumed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inviter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invitations_sent'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'is_consumed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'realname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.pendingthirdpartyuser': { + 'Meta': {'unique_together': "(('provider', 'third_party_identifier'),)", 'object_name': 'PendingThirdPartyUser'}, + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'info': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'third_party_identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'token': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'im.project': { + 'Meta': {'object_name': 'Project'}, + 'application': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'project'", 'unique': 'True', 'to': "orm['im.ProjectApplication']"}), + 'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_column': "'id'"}), + 'members': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.AstakosUser']", 'through': "orm['im.ProjectMembership']", 'symmetrical': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True', 'null': 'True', 'db_index': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '1', 'db_index': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.projectapplication': { + 'Meta': {'unique_together': "(('chain', 'id'),)", 'object_name': 'ProjectApplication'}, + 'applicant': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_applied'", 'to': "orm['im.AstakosUser']"}), + 'chain': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'chained_apps'", 'db_column': "'chain'", 'to': "orm['im.Project']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'end_date': ('django.db.models.fields.DateTimeField', [], {}), + 'homepage': ('django.db.models.fields.URLField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'issue_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'limit_on_members_number': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'member_join_policy': ('django.db.models.fields.IntegerField', [], {}), + 'member_leave_policy': ('django.db.models.fields.IntegerField', [], {}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_owned'", 'to': "orm['im.AstakosUser']"}), + 'private': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'resource_grants': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.ProjectResourceGrant']", 'blank': 'True'}), + 'response': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'response_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'responded_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'response_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'start_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}), + 'waive_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'waived_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'waive_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'waive_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.projectlock': { + 'Meta': {'object_name': 'ProjectLock'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.projectlog': { + 'Meta': {'object_name': 'ProjectLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.Project']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectmembership': { + 'Meta': {'unique_together': "(('person', 'project'),)", 'object_name': 'ProjectMembership'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Project']"}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}) + }, + 'im.projectmembershiplog': { + 'Meta': {'object_name': 'ProjectMembershipLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'membership': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.ProjectMembership']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectresourcegrant': { + 'Meta': {'unique_together': "(('resource', 'project_application'),)", 'object_name': 'ProjectResourceGrant'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'member_capacity': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'project_application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.ProjectApplication']", 'null': 'True'}), + 'project_capacity': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}) + }, + 'im.resource': { + 'Meta': {'object_name': 'Resource'}, + 'api_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'desc': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'service_origin': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'service_type': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'ui_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'unit': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'uplimit': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) + }, + 'im.service': { + 'Meta': {'object_name': 'Service'}, + 'component': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Component']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.sessioncatalog': { + 'Meta': {'object_name': 'SessionCatalog'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.usersetting': { + 'Meta': {'unique_together': "(('user', 'setting'),)", 'object_name': 'UserSetting'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'setting': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'value': ('django.db.models.fields.IntegerField', [], {}) + } + } + + complete_apps = ['im'] \ No newline at end of file diff --git a/snf-astakos-app/astakos/im/migrations/0067_set_project_capacity.py b/snf-astakos-app/astakos/im/migrations/0067_set_project_capacity.py new file mode 100644 index 0000000000000000000000000000000000000000..3f4732d80f2341c2ad83afd58abb6bfaea8e9dcd --- /dev/null +++ b/snf-astakos-app/astakos/im/migrations/0067_set_project_capacity.py @@ -0,0 +1,332 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +APPROVED = 1 +ACTUALLY_ACCEPTED = [1, 5] +MAX = 2**63 - 1 + + +class Migration(DataMigration): + + def forwards(self, orm): + objs = orm.ProjectResourceGrant.objects + grants = objs.select_related("project_application").all() + new_grants = [] + for grant in grants: + application = grant.project_application + max_members = application.limit_on_members_number + if max_members is None: + if application.state != APPROVED: + max_members = 1 + else: + objs = orm.ProjectMembership.objects + members = objs.filter(project=application.chain, + state__in=ACTUALLY_ACCEPTED).count() + max_members = max(2 * members, 1) + + project_capacity = min(max_members * grant.member_capacity, MAX) + new_grant = orm.ProjectResourceGrant( + resource=grant.resource, + project_application=grant.project_application, + member_capacity=grant.member_capacity, + project_capacity=project_capacity) + new_grants.append(new_grant) + orm.ProjectResourceGrant.objects.all().delete() + orm.ProjectResourceGrant.objects.bulk_create(new_grants) + + def backwards(self, orm): + "Write your backwards methods here." + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'im.additionalmail': { + 'Meta': {'object_name': 'AdditionalMail'}, + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.approvalterms': { + 'Meta': {'object_name': 'ApprovalTerms'}, + 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.astakosuser': { + 'Meta': {'object_name': 'AstakosUser', '_ormbases': ['auth.User']}, + 'accepted_email': ('django.db.models.fields.EmailField', [], {'default': 'None', 'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'accepted_policy': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'activation_sent': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date_signed_terms': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_reason': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}), + 'disturbed_quota': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'email_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_credits': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_signed_terms': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'invitations': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'is_rejected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'level': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'moderated_at': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'moderated_data': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.AstakosUserQuota']", 'symmetrical': 'False'}), + 'rejected_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {}), + 'user_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'primary_key': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'verification_code': ('django.db.models.fields.CharField', [], {'max_length': '255', 'unique': 'True', 'null': 'True'}), + 'verified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.astakosuserauthprovider': { + 'Meta': {'ordering': "('module', 'created')", 'unique_together': "(('identifier', 'module', 'user'),)", 'object_name': 'AstakosUserAuthProvider'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_backend': ('django.db.models.fields.CharField', [], {'default': "'astakos'", 'max_length': '255'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'info_data': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'module': ('django.db.models.fields.CharField', [], {'default': "'local'", 'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'auth_providers'", 'to': "orm['im.AstakosUser']"}) + }, + 'im.astakosuserquota': { + 'Meta': {'unique_together': "(('resource', 'user'),)", 'object_name': 'AstakosUserQuota'}, + 'capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.authproviderpolicyprofile': { + 'Meta': {'ordering': "['priority']", 'object_name': 'AuthProviderPolicyProfile'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['auth.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_exclusive': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'policy_add': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_automoderate': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_create': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_limit': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}), + 'policy_login': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_remove': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_required': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_switch': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'priority': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['im.AstakosUser']"}) + }, + 'im.chain': { + 'Meta': {'object_name': 'Chain'}, + 'chain': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.component': { + 'Meta': {'object_name': 'Component'}, + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'base_url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}) + }, + 'im.emailchange': { + 'Meta': {'object_name': 'EmailChange'}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'requested_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'emailchanges'", 'unique': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.endpoint': { + 'Meta': {'object_name': 'Endpoint'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'service': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'endpoints'", 'to': "orm['im.Service']"}) + }, + 'im.endpointdata': { + 'Meta': {'unique_together': "(('endpoint', 'key'),)", 'object_name': 'EndpointData'}, + 'endpoint': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'data'", 'to': "orm['im.Endpoint']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '1024'}) + }, + 'im.invitation': { + 'Meta': {'object_name': 'Invitation'}, + 'code': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'consumed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inviter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invitations_sent'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'is_consumed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'realname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.pendingthirdpartyuser': { + 'Meta': {'unique_together': "(('provider', 'third_party_identifier'),)", 'object_name': 'PendingThirdPartyUser'}, + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'info': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'third_party_identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'token': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'im.project': { + 'Meta': {'object_name': 'Project'}, + 'application': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'project'", 'unique': 'True', 'to': "orm['im.ProjectApplication']"}), + 'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_column': "'id'"}), + 'members': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.AstakosUser']", 'through': "orm['im.ProjectMembership']", 'symmetrical': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True', 'null': 'True', 'db_index': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '1', 'db_index': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.projectapplication': { + 'Meta': {'unique_together': "(('chain', 'id'),)", 'object_name': 'ProjectApplication'}, + 'applicant': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_applied'", 'to': "orm['im.AstakosUser']"}), + 'chain': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'chained_apps'", 'db_column': "'chain'", 'to': "orm['im.Project']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'end_date': ('django.db.models.fields.DateTimeField', [], {}), + 'homepage': ('django.db.models.fields.URLField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'issue_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'limit_on_members_number': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'member_join_policy': ('django.db.models.fields.IntegerField', [], {}), + 'member_leave_policy': ('django.db.models.fields.IntegerField', [], {}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_owned'", 'to': "orm['im.AstakosUser']"}), + 'private': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'resource_grants': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.ProjectResourceGrant']", 'blank': 'True'}), + 'response': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'response_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'responded_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'response_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'start_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}), + 'waive_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'waived_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'waive_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'waive_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.projectlock': { + 'Meta': {'object_name': 'ProjectLock'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.projectlog': { + 'Meta': {'object_name': 'ProjectLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.Project']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectmembership': { + 'Meta': {'unique_together': "(('person', 'project'),)", 'object_name': 'ProjectMembership'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Project']"}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}) + }, + 'im.projectmembershiplog': { + 'Meta': {'object_name': 'ProjectMembershipLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'membership': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.ProjectMembership']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectresourcegrant': { + 'Meta': {'unique_together': "(('resource', 'project_application'),)", 'object_name': 'ProjectResourceGrant'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'member_capacity': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'project_application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.ProjectApplication']", 'null': 'True'}), + 'project_capacity': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}) + }, + 'im.resource': { + 'Meta': {'object_name': 'Resource'}, + 'api_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'desc': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'service_origin': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'service_type': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'ui_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'unit': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'uplimit': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) + }, + 'im.service': { + 'Meta': {'object_name': 'Service'}, + 'component': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Component']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.sessioncatalog': { + 'Meta': {'object_name': 'SessionCatalog'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.usersetting': { + 'Meta': {'unique_together': "(('user', 'setting'),)", 'object_name': 'UserSetting'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'setting': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'value': ('django.db.models.fields.IntegerField', [], {}) + } + } + + complete_apps = ['im'] + symmetrical = True diff --git a/snf-astakos-app/astakos/im/migrations/0068_auto__add_field_resource_project_default.py b/snf-astakos-app/astakos/im/migrations/0068_auto__add_field_resource_project_default.py new file mode 100644 index 0000000000000000000000000000000000000000..fcdb6a79c441898b94a7b89e8bc847d5f0dae653 --- /dev/null +++ b/snf-astakos-app/astakos/im/migrations/0068_auto__add_field_resource_project_default.py @@ -0,0 +1,310 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Adding field 'Resource.project_default' + db.add_column('im_resource', 'project_default', self.gf('django.db.models.fields.BigIntegerField')(default=0), keep_default=False) + + + def backwards(self, orm): + + # Deleting field 'Resource.project_default' + db.delete_column('im_resource', 'project_default') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 11, 28, 11, 54, 41, 792738)'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 11, 28, 11, 54, 41, 792685)'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'im.additionalmail': { + 'Meta': {'object_name': 'AdditionalMail'}, + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.approvalterms': { + 'Meta': {'object_name': 'ApprovalTerms'}, + 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.astakosuser': { + 'Meta': {'object_name': 'AstakosUser', '_ormbases': ['auth.User']}, + 'accepted_email': ('django.db.models.fields.EmailField', [], {'default': 'None', 'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'accepted_policy': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'activation_sent': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date_signed_terms': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_reason': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}), + 'disturbed_quota': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'email_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_credits': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_signed_terms': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'invitations': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'is_rejected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'level': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'moderated_at': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'moderated_data': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.AstakosUserQuota']", 'symmetrical': 'False'}), + 'rejected_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {}), + 'user_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'primary_key': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'verification_code': ('django.db.models.fields.CharField', [], {'max_length': '255', 'unique': 'True', 'null': 'True'}), + 'verified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.astakosuserauthprovider': { + 'Meta': {'ordering': "('module', 'created')", 'unique_together': "(('identifier', 'module', 'user'),)", 'object_name': 'AstakosUserAuthProvider'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_backend': ('django.db.models.fields.CharField', [], {'default': "'astakos'", 'max_length': '255'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'info_data': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'module': ('django.db.models.fields.CharField', [], {'default': "'local'", 'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'auth_providers'", 'to': "orm['im.AstakosUser']"}) + }, + 'im.astakosuserquota': { + 'Meta': {'unique_together': "(('resource', 'user'),)", 'object_name': 'AstakosUserQuota'}, + 'capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.authproviderpolicyprofile': { + 'Meta': {'ordering': "['priority']", 'object_name': 'AuthProviderPolicyProfile'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['auth.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_exclusive': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'policy_add': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_automoderate': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_create': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_limit': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}), + 'policy_login': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_remove': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_required': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_switch': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'priority': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['im.AstakosUser']"}) + }, + 'im.chain': { + 'Meta': {'object_name': 'Chain'}, + 'chain': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.component': { + 'Meta': {'object_name': 'Component'}, + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'base_url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}) + }, + 'im.emailchange': { + 'Meta': {'object_name': 'EmailChange'}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'requested_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'emailchanges'", 'unique': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.endpoint': { + 'Meta': {'object_name': 'Endpoint'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'service': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'endpoints'", 'to': "orm['im.Service']"}) + }, + 'im.endpointdata': { + 'Meta': {'unique_together': "(('endpoint', 'key'),)", 'object_name': 'EndpointData'}, + 'endpoint': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'data'", 'to': "orm['im.Endpoint']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '1024'}) + }, + 'im.invitation': { + 'Meta': {'object_name': 'Invitation'}, + 'code': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'consumed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inviter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invitations_sent'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'is_consumed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'realname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.pendingthirdpartyuser': { + 'Meta': {'unique_together': "(('provider', 'third_party_identifier'),)", 'object_name': 'PendingThirdPartyUser'}, + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'info': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'third_party_identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'token': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'im.project': { + 'Meta': {'object_name': 'Project'}, + 'application': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'project'", 'unique': 'True', 'to': "orm['im.ProjectApplication']"}), + 'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_column': "'id'"}), + 'members': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.AstakosUser']", 'through': "orm['im.ProjectMembership']", 'symmetrical': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True', 'null': 'True', 'db_index': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '1', 'db_index': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.projectapplication': { + 'Meta': {'unique_together': "(('chain', 'id'),)", 'object_name': 'ProjectApplication'}, + 'applicant': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_applied'", 'to': "orm['im.AstakosUser']"}), + 'chain': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'chained_apps'", 'db_column': "'chain'", 'to': "orm['im.Project']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'end_date': ('django.db.models.fields.DateTimeField', [], {}), + 'homepage': ('django.db.models.fields.URLField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'issue_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'limit_on_members_number': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'member_join_policy': ('django.db.models.fields.IntegerField', [], {}), + 'member_leave_policy': ('django.db.models.fields.IntegerField', [], {}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_owned'", 'to': "orm['im.AstakosUser']"}), + 'private': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'resource_grants': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.ProjectResourceGrant']", 'blank': 'True'}), + 'response': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'response_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'responded_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'response_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'start_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}), + 'waive_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'waived_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'waive_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'waive_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.projectlock': { + 'Meta': {'object_name': 'ProjectLock'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.projectlog': { + 'Meta': {'object_name': 'ProjectLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.Project']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectmembership': { + 'Meta': {'unique_together': "(('person', 'project'),)", 'object_name': 'ProjectMembership'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Project']"}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}) + }, + 'im.projectmembershiplog': { + 'Meta': {'object_name': 'ProjectMembershipLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'membership': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.ProjectMembership']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectresourcegrant': { + 'Meta': {'unique_together': "(('resource', 'project_application'),)", 'object_name': 'ProjectResourceGrant'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'member_capacity': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'project_application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.ProjectApplication']", 'null': 'True'}), + 'project_capacity': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}) + }, + 'im.resource': { + 'Meta': {'object_name': 'Resource'}, + 'api_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'desc': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'project_default': ('django.db.models.fields.BigIntegerField', [], {}), + 'service_origin': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'service_type': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'ui_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'unit': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'uplimit': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) + }, + 'im.service': { + 'Meta': {'object_name': 'Service'}, + 'component': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Component']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.sessioncatalog': { + 'Meta': {'object_name': 'SessionCatalog'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.usersetting': { + 'Meta': {'unique_together': "(('user', 'setting'),)", 'object_name': 'UserSetting'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'setting': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'value': ('django.db.models.fields.IntegerField', [], {}) + } + } + + complete_apps = ['im'] diff --git a/snf-astakos-app/astakos/im/migrations/0069_resource_project_defaults.py b/snf-astakos-app/astakos/im/migrations/0069_resource_project_defaults.py new file mode 100644 index 0000000000000000000000000000000000000000..d333118eb901bea4d062d7840f5d27c6573591d5 --- /dev/null +++ b/snf-astakos-app/astakos/im/migrations/0069_resource_project_defaults.py @@ -0,0 +1,312 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +MAX = 2**63 - 1 + + +class Migration(DataMigration): + + def forwards(self, orm): + resources = orm.Resource.objects.all() + for resource in resources: + if resource.uplimit == MAX: + resource.project_default = MAX + resource.save() + + def backwards(self, orm): + "Write your backwards methods here." + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 11, 28, 11, 56, 39, 999402)'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 11, 28, 11, 56, 39, 999350)'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'im.additionalmail': { + 'Meta': {'object_name': 'AdditionalMail'}, + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.approvalterms': { + 'Meta': {'object_name': 'ApprovalTerms'}, + 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.astakosuser': { + 'Meta': {'object_name': 'AstakosUser', '_ormbases': ['auth.User']}, + 'accepted_email': ('django.db.models.fields.EmailField', [], {'default': 'None', 'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'accepted_policy': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'activation_sent': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date_signed_terms': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_reason': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}), + 'disturbed_quota': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'email_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_credits': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_signed_terms': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'invitations': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'is_rejected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'level': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'moderated_at': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'moderated_data': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.AstakosUserQuota']", 'symmetrical': 'False'}), + 'rejected_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {}), + 'user_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'primary_key': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'verification_code': ('django.db.models.fields.CharField', [], {'max_length': '255', 'unique': 'True', 'null': 'True'}), + 'verified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.astakosuserauthprovider': { + 'Meta': {'ordering': "('module', 'created')", 'unique_together': "(('identifier', 'module', 'user'),)", 'object_name': 'AstakosUserAuthProvider'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_backend': ('django.db.models.fields.CharField', [], {'default': "'astakos'", 'max_length': '255'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'info_data': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'module': ('django.db.models.fields.CharField', [], {'default': "'local'", 'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'auth_providers'", 'to': "orm['im.AstakosUser']"}) + }, + 'im.astakosuserquota': { + 'Meta': {'unique_together': "(('resource', 'user'),)", 'object_name': 'AstakosUserQuota'}, + 'capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.authproviderpolicyprofile': { + 'Meta': {'ordering': "['priority']", 'object_name': 'AuthProviderPolicyProfile'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['auth.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_exclusive': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'policy_add': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_automoderate': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_create': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_limit': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}), + 'policy_login': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_remove': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_required': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_switch': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'priority': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['im.AstakosUser']"}) + }, + 'im.chain': { + 'Meta': {'object_name': 'Chain'}, + 'chain': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.component': { + 'Meta': {'object_name': 'Component'}, + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'base_url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}) + }, + 'im.emailchange': { + 'Meta': {'object_name': 'EmailChange'}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'requested_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'emailchanges'", 'unique': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.endpoint': { + 'Meta': {'object_name': 'Endpoint'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'service': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'endpoints'", 'to': "orm['im.Service']"}) + }, + 'im.endpointdata': { + 'Meta': {'unique_together': "(('endpoint', 'key'),)", 'object_name': 'EndpointData'}, + 'endpoint': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'data'", 'to': "orm['im.Endpoint']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '1024'}) + }, + 'im.invitation': { + 'Meta': {'object_name': 'Invitation'}, + 'code': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'consumed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inviter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invitations_sent'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'is_consumed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'realname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.pendingthirdpartyuser': { + 'Meta': {'unique_together': "(('provider', 'third_party_identifier'),)", 'object_name': 'PendingThirdPartyUser'}, + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'info': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'third_party_identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'token': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'im.project': { + 'Meta': {'object_name': 'Project'}, + 'application': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'project'", 'unique': 'True', 'to': "orm['im.ProjectApplication']"}), + 'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_column': "'id'"}), + 'members': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.AstakosUser']", 'through': "orm['im.ProjectMembership']", 'symmetrical': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True', 'null': 'True', 'db_index': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '1', 'db_index': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.projectapplication': { + 'Meta': {'unique_together': "(('chain', 'id'),)", 'object_name': 'ProjectApplication'}, + 'applicant': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_applied'", 'to': "orm['im.AstakosUser']"}), + 'chain': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'chained_apps'", 'db_column': "'chain'", 'to': "orm['im.Project']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'end_date': ('django.db.models.fields.DateTimeField', [], {}), + 'homepage': ('django.db.models.fields.URLField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'issue_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'limit_on_members_number': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'member_join_policy': ('django.db.models.fields.IntegerField', [], {}), + 'member_leave_policy': ('django.db.models.fields.IntegerField', [], {}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_owned'", 'to': "orm['im.AstakosUser']"}), + 'private': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'resource_grants': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.ProjectResourceGrant']", 'blank': 'True'}), + 'response': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'response_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'responded_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'response_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'start_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}), + 'waive_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'waived_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'waive_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'waive_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.projectlock': { + 'Meta': {'object_name': 'ProjectLock'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.projectlog': { + 'Meta': {'object_name': 'ProjectLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.Project']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectmembership': { + 'Meta': {'unique_together': "(('person', 'project'),)", 'object_name': 'ProjectMembership'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Project']"}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}) + }, + 'im.projectmembershiplog': { + 'Meta': {'object_name': 'ProjectMembershipLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'membership': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.ProjectMembership']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectresourcegrant': { + 'Meta': {'unique_together': "(('resource', 'project_application'),)", 'object_name': 'ProjectResourceGrant'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'member_capacity': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'project_application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.ProjectApplication']", 'null': 'True'}), + 'project_capacity': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}) + }, + 'im.resource': { + 'Meta': {'object_name': 'Resource'}, + 'api_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'desc': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'project_default': ('django.db.models.fields.BigIntegerField', [], {}), + 'service_origin': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'service_type': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'ui_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'unit': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'uplimit': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) + }, + 'im.service': { + 'Meta': {'object_name': 'Service'}, + 'component': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Component']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.sessioncatalog': { + 'Meta': {'object_name': 'SessionCatalog'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.usersetting': { + 'Meta': {'unique_together': "(('user', 'setting'),)", 'object_name': 'UserSetting'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'setting': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'value': ('django.db.models.fields.IntegerField', [], {}) + } + } + + complete_apps = ['im'] diff --git a/snf-astakos-app/astakos/im/migrations/0070_auto__chg_field_projectapplication_limit_on_members_number.py b/snf-astakos-app/astakos/im/migrations/0070_auto__chg_field_projectapplication_limit_on_members_number.py new file mode 100644 index 0000000000000000000000000000000000000000..2f51907afb4949bed994730771be9b2afac8aff8 --- /dev/null +++ b/snf-astakos-app/astakos/im/migrations/0070_auto__chg_field_projectapplication_limit_on_members_number.py @@ -0,0 +1,310 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Changing field 'ProjectApplication.limit_on_members_number' + db.alter_column('im_projectapplication', 'limit_on_members_number', self.gf('django.db.models.fields.BigIntegerField')(null=True)) + + + def backwards(self, orm): + + # Changing field 'ProjectApplication.limit_on_members_number' + db.alter_column('im_projectapplication', 'limit_on_members_number', self.gf('django.db.models.fields.PositiveIntegerField')(null=True)) + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 12, 12, 10, 23, 55, 434650)'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 12, 12, 10, 23, 55, 434601)'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'im.additionalmail': { + 'Meta': {'object_name': 'AdditionalMail'}, + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.approvalterms': { + 'Meta': {'object_name': 'ApprovalTerms'}, + 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.astakosuser': { + 'Meta': {'object_name': 'AstakosUser', '_ormbases': ['auth.User']}, + 'accepted_email': ('django.db.models.fields.EmailField', [], {'default': 'None', 'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'accepted_policy': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'activation_sent': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date_signed_terms': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_reason': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}), + 'disturbed_quota': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'email_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_credits': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_signed_terms': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'invitations': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'is_rejected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'level': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'moderated_at': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'moderated_data': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.AstakosUserQuota']", 'symmetrical': 'False'}), + 'rejected_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {}), + 'user_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'primary_key': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'verification_code': ('django.db.models.fields.CharField', [], {'max_length': '255', 'unique': 'True', 'null': 'True'}), + 'verified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.astakosuserauthprovider': { + 'Meta': {'ordering': "('module', 'created')", 'unique_together': "(('identifier', 'module', 'user'),)", 'object_name': 'AstakosUserAuthProvider'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_backend': ('django.db.models.fields.CharField', [], {'default': "'astakos'", 'max_length': '255'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'info_data': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'module': ('django.db.models.fields.CharField', [], {'default': "'local'", 'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'auth_providers'", 'to': "orm['im.AstakosUser']"}) + }, + 'im.astakosuserquota': { + 'Meta': {'unique_together': "(('resource', 'user'),)", 'object_name': 'AstakosUserQuota'}, + 'capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.authproviderpolicyprofile': { + 'Meta': {'ordering': "['priority']", 'object_name': 'AuthProviderPolicyProfile'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['auth.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_exclusive': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'policy_add': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_automoderate': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_create': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_limit': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}), + 'policy_login': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_remove': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_required': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_switch': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'priority': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['im.AstakosUser']"}) + }, + 'im.chain': { + 'Meta': {'object_name': 'Chain'}, + 'chain': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.component': { + 'Meta': {'object_name': 'Component'}, + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'base_url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}) + }, + 'im.emailchange': { + 'Meta': {'object_name': 'EmailChange'}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'requested_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'emailchanges'", 'unique': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.endpoint': { + 'Meta': {'object_name': 'Endpoint'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'service': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'endpoints'", 'to': "orm['im.Service']"}) + }, + 'im.endpointdata': { + 'Meta': {'unique_together': "(('endpoint', 'key'),)", 'object_name': 'EndpointData'}, + 'endpoint': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'data'", 'to': "orm['im.Endpoint']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '1024'}) + }, + 'im.invitation': { + 'Meta': {'object_name': 'Invitation'}, + 'code': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'consumed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inviter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invitations_sent'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'is_consumed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'realname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.pendingthirdpartyuser': { + 'Meta': {'unique_together': "(('provider', 'third_party_identifier'),)", 'object_name': 'PendingThirdPartyUser'}, + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'info': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'third_party_identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'token': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'im.project': { + 'Meta': {'object_name': 'Project'}, + 'application': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'project'", 'unique': 'True', 'to': "orm['im.ProjectApplication']"}), + 'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_column': "'id'"}), + 'members': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.AstakosUser']", 'through': "orm['im.ProjectMembership']", 'symmetrical': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True', 'null': 'True', 'db_index': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '1', 'db_index': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.projectapplication': { + 'Meta': {'unique_together': "(('chain', 'id'),)", 'object_name': 'ProjectApplication'}, + 'applicant': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_applied'", 'to': "orm['im.AstakosUser']"}), + 'chain': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'chained_apps'", 'db_column': "'chain'", 'to': "orm['im.Project']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'end_date': ('django.db.models.fields.DateTimeField', [], {}), + 'homepage': ('django.db.models.fields.URLField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'issue_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'limit_on_members_number': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'member_join_policy': ('django.db.models.fields.IntegerField', [], {}), + 'member_leave_policy': ('django.db.models.fields.IntegerField', [], {}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_owned'", 'to': "orm['im.AstakosUser']"}), + 'private': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'resource_grants': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.ProjectResourceGrant']", 'blank': 'True'}), + 'response': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'response_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'responded_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'response_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'start_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}), + 'waive_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'waived_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'waive_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'waive_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.projectlock': { + 'Meta': {'object_name': 'ProjectLock'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.projectlog': { + 'Meta': {'object_name': 'ProjectLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.Project']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectmembership': { + 'Meta': {'unique_together': "(('person', 'project'),)", 'object_name': 'ProjectMembership'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Project']"}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}) + }, + 'im.projectmembershiplog': { + 'Meta': {'object_name': 'ProjectMembershipLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'membership': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.ProjectMembership']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectresourcegrant': { + 'Meta': {'unique_together': "(('resource', 'project_application'),)", 'object_name': 'ProjectResourceGrant'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'member_capacity': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'project_application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.ProjectApplication']", 'null': 'True'}), + 'project_capacity': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}) + }, + 'im.resource': { + 'Meta': {'object_name': 'Resource'}, + 'api_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'desc': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'project_default': ('django.db.models.fields.BigIntegerField', [], {}), + 'service_origin': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'service_type': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'ui_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'unit': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'uplimit': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) + }, + 'im.service': { + 'Meta': {'object_name': 'Service'}, + 'component': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Component']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.sessioncatalog': { + 'Meta': {'object_name': 'SessionCatalog'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.usersetting': { + 'Meta': {'unique_together': "(('user', 'setting'),)", 'object_name': 'UserSetting'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'setting': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'value': ('django.db.models.fields.IntegerField', [], {}) + } + } + + complete_apps = ['im'] diff --git a/snf-astakos-app/astakos/im/migrations/0071_inf_max_members.py b/snf-astakos-app/astakos/im/migrations/0071_inf_max_members.py new file mode 100644 index 0000000000000000000000000000000000000000..ee1594daa77c05e406feb3d674587ff5f42f6f97 --- /dev/null +++ b/snf-astakos-app/astakos/im/migrations/0071_inf_max_members.py @@ -0,0 +1,314 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +MAX = 2**63 - 1 + + +class Migration(DataMigration): + + def forwards(self, orm): + orm.ProjectApplication.objects.\ + filter(homepage__isnull=True).update(homepage="") + orm.ProjectApplication.objects.\ + filter(description__isnull=True).update(description="") + orm.ProjectApplication.objects.\ + filter(limit_on_members_number__isnull=True).\ + update(limit_on_members_number=MAX) + + def backwards(self, orm): + "Write your backwards methods here." + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 12, 12, 10, 25, 17, 370969)'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 12, 12, 10, 25, 17, 370916)'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'im.additionalmail': { + 'Meta': {'object_name': 'AdditionalMail'}, + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.approvalterms': { + 'Meta': {'object_name': 'ApprovalTerms'}, + 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.astakosuser': { + 'Meta': {'object_name': 'AstakosUser', '_ormbases': ['auth.User']}, + 'accepted_email': ('django.db.models.fields.EmailField', [], {'default': 'None', 'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'accepted_policy': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'activation_sent': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date_signed_terms': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_reason': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}), + 'disturbed_quota': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'email_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_credits': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_signed_terms': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'invitations': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'is_rejected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'level': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'moderated_at': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'moderated_data': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.AstakosUserQuota']", 'symmetrical': 'False'}), + 'rejected_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {}), + 'user_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'primary_key': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'verification_code': ('django.db.models.fields.CharField', [], {'max_length': '255', 'unique': 'True', 'null': 'True'}), + 'verified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.astakosuserauthprovider': { + 'Meta': {'ordering': "('module', 'created')", 'unique_together': "(('identifier', 'module', 'user'),)", 'object_name': 'AstakosUserAuthProvider'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_backend': ('django.db.models.fields.CharField', [], {'default': "'astakos'", 'max_length': '255'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'info_data': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'module': ('django.db.models.fields.CharField', [], {'default': "'local'", 'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'auth_providers'", 'to': "orm['im.AstakosUser']"}) + }, + 'im.astakosuserquota': { + 'Meta': {'unique_together': "(('resource', 'user'),)", 'object_name': 'AstakosUserQuota'}, + 'capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.authproviderpolicyprofile': { + 'Meta': {'ordering': "['priority']", 'object_name': 'AuthProviderPolicyProfile'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['auth.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_exclusive': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'policy_add': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_automoderate': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_create': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_limit': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}), + 'policy_login': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_remove': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_required': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_switch': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'priority': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['im.AstakosUser']"}) + }, + 'im.chain': { + 'Meta': {'object_name': 'Chain'}, + 'chain': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.component': { + 'Meta': {'object_name': 'Component'}, + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'base_url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}) + }, + 'im.emailchange': { + 'Meta': {'object_name': 'EmailChange'}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'requested_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'emailchanges'", 'unique': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.endpoint': { + 'Meta': {'object_name': 'Endpoint'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'service': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'endpoints'", 'to': "orm['im.Service']"}) + }, + 'im.endpointdata': { + 'Meta': {'unique_together': "(('endpoint', 'key'),)", 'object_name': 'EndpointData'}, + 'endpoint': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'data'", 'to': "orm['im.Endpoint']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '1024'}) + }, + 'im.invitation': { + 'Meta': {'object_name': 'Invitation'}, + 'code': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'consumed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inviter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invitations_sent'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'is_consumed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'realname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.pendingthirdpartyuser': { + 'Meta': {'unique_together': "(('provider', 'third_party_identifier'),)", 'object_name': 'PendingThirdPartyUser'}, + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'info': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'third_party_identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'token': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'im.project': { + 'Meta': {'object_name': 'Project'}, + 'application': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'project'", 'unique': 'True', 'to': "orm['im.ProjectApplication']"}), + 'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_column': "'id'"}), + 'members': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.AstakosUser']", 'through': "orm['im.ProjectMembership']", 'symmetrical': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True', 'null': 'True', 'db_index': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '1', 'db_index': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.projectapplication': { + 'Meta': {'unique_together': "(('chain', 'id'),)", 'object_name': 'ProjectApplication'}, + 'applicant': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_applied'", 'to': "orm['im.AstakosUser']"}), + 'chain': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'chained_apps'", 'db_column': "'chain'", 'to': "orm['im.Project']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'end_date': ('django.db.models.fields.DateTimeField', [], {}), + 'homepage': ('django.db.models.fields.URLField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'issue_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'limit_on_members_number': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'member_join_policy': ('django.db.models.fields.IntegerField', [], {}), + 'member_leave_policy': ('django.db.models.fields.IntegerField', [], {}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_owned'", 'to': "orm['im.AstakosUser']"}), + 'private': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'resource_grants': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.ProjectResourceGrant']", 'blank': 'True'}), + 'response': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'response_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'responded_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'response_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'start_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}), + 'waive_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'waived_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'waive_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'waive_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.projectlock': { + 'Meta': {'object_name': 'ProjectLock'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.projectlog': { + 'Meta': {'object_name': 'ProjectLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.Project']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectmembership': { + 'Meta': {'unique_together': "(('person', 'project'),)", 'object_name': 'ProjectMembership'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Project']"}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}) + }, + 'im.projectmembershiplog': { + 'Meta': {'object_name': 'ProjectMembershipLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'membership': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.ProjectMembership']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectresourcegrant': { + 'Meta': {'unique_together': "(('resource', 'project_application'),)", 'object_name': 'ProjectResourceGrant'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'member_capacity': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'project_application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.ProjectApplication']", 'null': 'True'}), + 'project_capacity': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}) + }, + 'im.resource': { + 'Meta': {'object_name': 'Resource'}, + 'api_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'desc': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'project_default': ('django.db.models.fields.BigIntegerField', [], {}), + 'service_origin': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'service_type': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'ui_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'unit': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'uplimit': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) + }, + 'im.service': { + 'Meta': {'object_name': 'Service'}, + 'component': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Component']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.sessioncatalog': { + 'Meta': {'object_name': 'SessionCatalog'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.usersetting': { + 'Meta': {'unique_together': "(('user', 'setting'),)", 'object_name': 'UserSetting'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'setting': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'value': ('django.db.models.fields.IntegerField', [], {}) + } + } + + complete_apps = ['im'] diff --git a/snf-astakos-app/astakos/im/migrations/0072_auto__add_projectresourcequota__add_unique_projectresourcequota_resour.py b/snf-astakos-app/astakos/im/migrations/0072_auto__add_projectresourcequota__add_unique_projectresourcequota_resour.py new file mode 100644 index 0000000000000000000000000000000000000000..332a21fb011c23124701b90ccb7cd7682a3ac995 --- /dev/null +++ b/snf-astakos-app/astakos/im/migrations/0072_auto__add_projectresourcequota__add_unique_projectresourcequota_resour.py @@ -0,0 +1,438 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Adding model 'ProjectResourceQuota' + db.create_table('im_projectresourcequota', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('resource', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['im.Resource'])), + ('project', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['im.Project'])), + ('project_capacity', self.gf('django.db.models.fields.BigIntegerField')(default=0)), + ('member_capacity', self.gf('django.db.models.fields.BigIntegerField')(default=0)), + )) + db.send_create_signal('im', ['ProjectResourceQuota']) + + # Adding unique constraint on 'ProjectResourceQuota', fields ['resource', 'project'] + db.create_unique('im_projectresourcequota', ['resource_id', 'project_id']) + + # Changing field 'ProjectApplication.member_join_policy' + db.alter_column('im_projectapplication', 'member_join_policy', self.gf('django.db.models.fields.IntegerField')(null=True)) + + # Changing field 'ProjectApplication.private' + db.alter_column('im_projectapplication', 'private', self.gf('django.db.models.fields.NullBooleanField')(null=True)) + + # Changing field 'ProjectApplication.owner' + db.alter_column('im_projectapplication', 'owner_id', self.gf('django.db.models.fields.related.ForeignKey')(null=True, to=orm['im.AstakosUser'])) + + # Changing field 'ProjectApplication.end_date' + db.alter_column('im_projectapplication', 'end_date', self.gf('django.db.models.fields.DateTimeField')(null=True)) + + # Changing field 'ProjectApplication.name' + db.alter_column('im_projectapplication', 'name', self.gf('django.db.models.fields.CharField')(max_length=80, null=True)) + + # Changing field 'ProjectApplication.member_leave_policy' + db.alter_column('im_projectapplication', 'member_leave_policy', self.gf('django.db.models.fields.IntegerField')(null=True)) + + # Adding field 'Project.last_application' + db.add_column('im_project', 'last_application', self.gf('django.db.models.fields.related.ForeignKey')(related_name='last_of_project', null=True, to=orm['im.ProjectApplication']), keep_default=False) + + # Adding field 'Project.owner' + db.add_column('im_project', 'owner', self.gf('django.db.models.fields.related.ForeignKey')(related_name='projs_owned', null=True, to=orm['im.AstakosUser']), keep_default=False) + + # Adding field 'Project.realname' + db.add_column('im_project', 'realname', self.gf('django.db.models.fields.CharField')(max_length=80, null=True), keep_default=False) + + # Adding field 'Project.homepage' + db.add_column('im_project', 'homepage', self.gf('django.db.models.fields.URLField')(max_length=255, null=True), keep_default=False) + + # Adding field 'Project.description' + db.add_column('im_project', 'description', self.gf('django.db.models.fields.TextField')(null=True, blank=True), keep_default=False) + + # Adding field 'Project.end_date' + db.add_column('im_project', 'end_date', self.gf('django.db.models.fields.DateTimeField')(null=True), keep_default=False) + + # Adding field 'Project.member_join_policy' + db.add_column('im_project', 'member_join_policy', self.gf('django.db.models.fields.IntegerField')(null=True), keep_default=False) + + # Adding field 'Project.member_leave_policy' + db.add_column('im_project', 'member_leave_policy', self.gf('django.db.models.fields.IntegerField')(null=True), keep_default=False) + + # Adding field 'Project.limit_on_members_number' + db.add_column('im_project', 'limit_on_members_number', self.gf('django.db.models.fields.BigIntegerField')(null=True), keep_default=False) + + # Adding field 'Project.private' + db.add_column('im_project', 'private', self.gf('django.db.models.fields.BooleanField')(default=False), keep_default=False) + + + def backwards(self, orm): + + # Removing unique constraint on 'ProjectResourceQuota', fields ['resource', 'project'] + db.delete_unique('im_projectresourcequota', ['resource_id', 'project_id']) + + # Deleting model 'ProjectResourceQuota' + db.delete_table('im_projectresourcequota') + + # Changing field 'ProjectApplication.member_join_policy' + db.alter_column('im_projectapplication', 'member_join_policy', self.gf('django.db.models.fields.IntegerField')(default=0)) + + # Changing field 'ProjectApplication.private' + db.alter_column('im_projectapplication', 'private', self.gf('django.db.models.fields.BooleanField')()) + + # Changing field 'ProjectApplication.owner' + db.alter_column('im_projectapplication', 'owner_id', self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['im.AstakosUser'])) + + # Changing field 'ProjectApplication.end_date' + db.alter_column('im_projectapplication', 'end_date', self.gf('django.db.models.fields.DateTimeField')(default=None)) + + # Changing field 'ProjectApplication.name' + db.alter_column('im_projectapplication', 'name', self.gf('django.db.models.fields.CharField')(default='', max_length=80)) + + # Changing field 'ProjectApplication.member_leave_policy' + db.alter_column('im_projectapplication', 'member_leave_policy', self.gf('django.db.models.fields.IntegerField')(default=0)) + + # Deleting field 'Project.last_application' + db.delete_column('im_project', 'last_application_id') + + # Deleting field 'Project.owner' + db.delete_column('im_project', 'owner_id') + + # Deleting field 'Project.realname' + db.delete_column('im_project', 'realname') + + # Deleting field 'Project.homepage' + db.delete_column('im_project', 'homepage') + + # Deleting field 'Project.description' + db.delete_column('im_project', 'description') + + # Deleting field 'Project.end_date' + db.delete_column('im_project', 'end_date') + + # Deleting field 'Project.member_join_policy' + db.delete_column('im_project', 'member_join_policy') + + # Deleting field 'Project.member_leave_policy' + db.delete_column('im_project', 'member_leave_policy') + + # Deleting field 'Project.limit_on_members_number' + db.delete_column('im_project', 'limit_on_members_number') + + # Deleting field 'Project.private' + db.delete_column('im_project', 'private') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 12, 12, 10, 58, 54, 330483)'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 12, 12, 10, 58, 54, 330431)'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'im.additionalmail': { + 'Meta': {'object_name': 'AdditionalMail'}, + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.approvalterms': { + 'Meta': {'object_name': 'ApprovalTerms'}, + 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.astakosuser': { + 'Meta': {'object_name': 'AstakosUser', '_ormbases': ['auth.User']}, + 'accepted_email': ('django.db.models.fields.EmailField', [], {'default': 'None', 'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'accepted_policy': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'activation_sent': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date_signed_terms': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_reason': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}), + 'disturbed_quota': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'email_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_credits': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_signed_terms': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'invitations': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'is_rejected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'level': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'moderated_at': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'moderated_data': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.AstakosUserQuota']", 'symmetrical': 'False'}), + 'rejected_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {}), + 'user_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'primary_key': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'verification_code': ('django.db.models.fields.CharField', [], {'max_length': '255', 'unique': 'True', 'null': 'True'}), + 'verified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.astakosuserauthprovider': { + 'Meta': {'ordering': "('module', 'created')", 'unique_together': "(('identifier', 'module', 'user'),)", 'object_name': 'AstakosUserAuthProvider'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_backend': ('django.db.models.fields.CharField', [], {'default': "'astakos'", 'max_length': '255'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'info_data': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'module': ('django.db.models.fields.CharField', [], {'default': "'local'", 'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'auth_providers'", 'to': "orm['im.AstakosUser']"}) + }, + 'im.astakosuserquota': { + 'Meta': {'unique_together': "(('resource', 'user'),)", 'object_name': 'AstakosUserQuota'}, + 'capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.authproviderpolicyprofile': { + 'Meta': {'ordering': "['priority']", 'object_name': 'AuthProviderPolicyProfile'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['auth.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_exclusive': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'policy_add': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_automoderate': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_create': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_limit': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}), + 'policy_login': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_remove': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_required': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_switch': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'priority': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['im.AstakosUser']"}) + }, + 'im.chain': { + 'Meta': {'object_name': 'Chain'}, + 'chain': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.component': { + 'Meta': {'object_name': 'Component'}, + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'base_url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}) + }, + 'im.emailchange': { + 'Meta': {'object_name': 'EmailChange'}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'requested_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'emailchanges'", 'unique': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.endpoint': { + 'Meta': {'object_name': 'Endpoint'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'service': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'endpoints'", 'to': "orm['im.Service']"}) + }, + 'im.endpointdata': { + 'Meta': {'unique_together': "(('endpoint', 'key'),)", 'object_name': 'EndpointData'}, + 'endpoint': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'data'", 'to': "orm['im.Endpoint']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '1024'}) + }, + 'im.invitation': { + 'Meta': {'object_name': 'Invitation'}, + 'code': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'consumed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inviter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invitations_sent'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'is_consumed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'realname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.pendingthirdpartyuser': { + 'Meta': {'unique_together': "(('provider', 'third_party_identifier'),)", 'object_name': 'PendingThirdPartyUser'}, + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'info': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'third_party_identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'token': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'im.project': { + 'Meta': {'object_name': 'Project'}, + 'application': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'project'", 'unique': 'True', 'to': "orm['im.ProjectApplication']"}), + 'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'end_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'homepage': ('django.db.models.fields.URLField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_column': "'id'"}), + 'last_application': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'last_of_project'", 'null': 'True', 'to': "orm['im.ProjectApplication']"}), + 'limit_on_members_number': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'member_join_policy': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'member_leave_policy': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'members': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.AstakosUser']", 'through': "orm['im.ProjectMembership']", 'symmetrical': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True', 'null': 'True', 'db_index': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projs_owned'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'private': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'realname': ('django.db.models.fields.CharField', [], {'max_length': '80', 'null': 'True'}), + 'resource_grants': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.ProjectResourceQuota']", 'blank': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '1', 'db_index': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.projectapplication': { + 'Meta': {'unique_together': "(('chain', 'id'),)", 'object_name': 'ProjectApplication'}, + 'applicant': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_applied'", 'to': "orm['im.AstakosUser']"}), + 'chain': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'chained_apps'", 'db_column': "'chain'", 'to': "orm['im.Project']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'end_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'homepage': ('django.db.models.fields.URLField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'issue_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'limit_on_members_number': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'member_join_policy': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'member_leave_policy': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'null': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_owned'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'private': ('django.db.models.fields.NullBooleanField', [], {'default': 'False', 'null': 'True', 'blank': 'True'}), + 'resource_grants': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.ProjectResourceGrant']", 'blank': 'True'}), + 'response': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'response_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'responded_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'response_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'start_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}), + 'waive_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'waived_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'waive_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'waive_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.projectlock': { + 'Meta': {'object_name': 'ProjectLock'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.projectlog': { + 'Meta': {'object_name': 'ProjectLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.Project']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectmembership': { + 'Meta': {'unique_together': "(('person', 'project'),)", 'object_name': 'ProjectMembership'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Project']"}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}) + }, + 'im.projectmembershiplog': { + 'Meta': {'object_name': 'ProjectMembershipLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'membership': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.ProjectMembership']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectresourcegrant': { + 'Meta': {'unique_together': "(('resource', 'project_application'),)", 'object_name': 'ProjectResourceGrant'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'member_capacity': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'project_application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.ProjectApplication']", 'null': 'True'}), + 'project_capacity': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}) + }, + 'im.projectresourcequota': { + 'Meta': {'unique_together': "(('resource', 'project'),)", 'object_name': 'ProjectResourceQuota'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'member_capacity': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Project']"}), + 'project_capacity': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}) + }, + 'im.resource': { + 'Meta': {'object_name': 'Resource'}, + 'api_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'desc': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'project_default': ('django.db.models.fields.BigIntegerField', [], {}), + 'service_origin': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'service_type': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'ui_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'unit': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'uplimit': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) + }, + 'im.service': { + 'Meta': {'object_name': 'Service'}, + 'component': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Component']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.sessioncatalog': { + 'Meta': {'object_name': 'SessionCatalog'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.usersetting': { + 'Meta': {'unique_together': "(('user', 'setting'),)", 'object_name': 'UserSetting'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'setting': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'value': ('django.db.models.fields.IntegerField', [], {}) + } + } + + complete_apps = ['im'] diff --git a/snf-astakos-app/astakos/im/migrations/0073_project_fields.py b/snf-astakos-app/astakos/im/migrations/0073_project_fields.py new file mode 100644 index 0000000000000000000000000000000000000000..520c6a503870eb15c64b6059f9286c4506178a46 --- /dev/null +++ b/snf-astakos-app/astakos/im/migrations/0073_project_fields.py @@ -0,0 +1,405 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +UNINITIALIZED = 0 +NORMAL = 1 +SUSPENDED = 10 +TERMINATED = 100 +DELETED = 1000 +INITIALIZED_STATES = [NORMAL, SUSPENDED, TERMINATED] + +PENDING = 0 +APPROVED = 1 +REPLACED = 2 +DENIED = 3 +DISMISSED = 4 +CANCELLED = 5 +DELETED_STATES = [DISMISSED, CANCELLED] + + +def _partition_by(f, l): + d = {} + for x in l: + group = f(x) + group_l = d.get(group, []) + group_l.append(x) + d[group] = group_l + return d + + +class Migration(DataMigration): + + def forwards(self, orm): + projects = orm.Project.objects.select_related("application").all() + for project in projects: + app = project.application + project.owner = app.owner + project.realname = app.name + project.homepage = app.homepage + project.description = app.description + project.end_date = app.end_date + project.member_join_policy = app.member_join_policy + project.member_leave_policy = app.member_leave_policy + project.limit_on_members_number = app.limit_on_members_number + project.private = app.private + project.last_application = \ + project.chained_apps.all().order_by("-id")[0] + project.save() + + orm.Project.objects.filter(state=NORMAL, application__state=PENDING).\ + update(state=UNINITIALIZED) + orm.Project.objects.filter( + state=NORMAL, application__state__in=DELETED_STATES).\ + update(state=DELETED) + + grants = orm.ProjectResourceGrant.objects.\ + filter(project_application__project__isnull=False).\ + select_related("project_application__project") + + quotas = [] + for grant in grants: + project = grant.project_application.project + quotas.append(orm.ProjectResourceQuota( + resource=grant.resource, + project=project, + project_capacity=grant.project_capacity, + member_capacity=grant.member_capacity)) + + orm.ProjectResourceQuota.objects.bulk_create(quotas) + + initialized = orm.Project.objects.filter(state__in=INITIALIZED_STATES) + objs = orm.ProjectResourceQuota.objects + grants = objs.select_related("project").filter(project__in=initialized) + grants = _partition_by(lambda g: g.project_id, grants) + resources = orm.Resource.objects.all() + new_grants = [] + for project in initialized: + p_grants = grants.get(project.id, []) + p_res = set(g.resource_id for g in p_grants) + for resource in resources: + if resource.id not in p_res: + new_grants.append( + orm.ProjectResourceQuota( + resource=resource, + project=project, + project_capacity=resource.project_default, + member_capacity=resource.project_default)) + + orm.ProjectResourceQuota.objects.bulk_create(new_grants) + + def backwards(self, orm): + "Write your backwards methods here." + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 12, 12, 11, 1, 52, 856457)'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 12, 12, 11, 1, 52, 856406)'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'im.additionalmail': { + 'Meta': {'object_name': 'AdditionalMail'}, + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.approvalterms': { + 'Meta': {'object_name': 'ApprovalTerms'}, + 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.astakosuser': { + 'Meta': {'object_name': 'AstakosUser', '_ormbases': ['auth.User']}, + 'accepted_email': ('django.db.models.fields.EmailField', [], {'default': 'None', 'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'accepted_policy': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'activation_sent': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date_signed_terms': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_reason': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}), + 'disturbed_quota': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'email_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_credits': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_signed_terms': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'invitations': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'is_rejected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'level': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'moderated_at': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'moderated_data': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.AstakosUserQuota']", 'symmetrical': 'False'}), + 'rejected_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {}), + 'user_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'primary_key': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'verification_code': ('django.db.models.fields.CharField', [], {'max_length': '255', 'unique': 'True', 'null': 'True'}), + 'verified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.astakosuserauthprovider': { + 'Meta': {'ordering': "('module', 'created')", 'unique_together': "(('identifier', 'module', 'user'),)", 'object_name': 'AstakosUserAuthProvider'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_backend': ('django.db.models.fields.CharField', [], {'default': "'astakos'", 'max_length': '255'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'info_data': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'module': ('django.db.models.fields.CharField', [], {'default': "'local'", 'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'auth_providers'", 'to': "orm['im.AstakosUser']"}) + }, + 'im.astakosuserquota': { + 'Meta': {'unique_together': "(('resource', 'user'),)", 'object_name': 'AstakosUserQuota'}, + 'capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.authproviderpolicyprofile': { + 'Meta': {'ordering': "['priority']", 'object_name': 'AuthProviderPolicyProfile'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['auth.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_exclusive': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'policy_add': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_automoderate': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_create': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_limit': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}), + 'policy_login': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_remove': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_required': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_switch': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'priority': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['im.AstakosUser']"}) + }, + 'im.chain': { + 'Meta': {'object_name': 'Chain'}, + 'chain': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.component': { + 'Meta': {'object_name': 'Component'}, + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'base_url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}) + }, + 'im.emailchange': { + 'Meta': {'object_name': 'EmailChange'}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'requested_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'emailchanges'", 'unique': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.endpoint': { + 'Meta': {'object_name': 'Endpoint'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'service': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'endpoints'", 'to': "orm['im.Service']"}) + }, + 'im.endpointdata': { + 'Meta': {'unique_together': "(('endpoint', 'key'),)", 'object_name': 'EndpointData'}, + 'endpoint': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'data'", 'to': "orm['im.Endpoint']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '1024'}) + }, + 'im.invitation': { + 'Meta': {'object_name': 'Invitation'}, + 'code': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'consumed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inviter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invitations_sent'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'is_consumed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'realname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.pendingthirdpartyuser': { + 'Meta': {'unique_together': "(('provider', 'third_party_identifier'),)", 'object_name': 'PendingThirdPartyUser'}, + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'info': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'third_party_identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'token': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'im.project': { + 'Meta': {'object_name': 'Project'}, + 'application': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'project'", 'unique': 'True', 'to': "orm['im.ProjectApplication']"}), + 'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'end_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'homepage': ('django.db.models.fields.URLField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_column': "'id'"}), + 'last_application': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'last_of_project'", 'null': 'True', 'to': "orm['im.ProjectApplication']"}), + 'limit_on_members_number': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'member_join_policy': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'member_leave_policy': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'members': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.AstakosUser']", 'through': "orm['im.ProjectMembership']", 'symmetrical': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True', 'null': 'True', 'db_index': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projs_owned'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'private': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'realname': ('django.db.models.fields.CharField', [], {'max_length': '80', 'null': 'True'}), + 'resource_grants': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.ProjectResourceQuota']", 'blank': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '1', 'db_index': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.projectapplication': { + 'Meta': {'unique_together': "(('chain', 'id'),)", 'object_name': 'ProjectApplication'}, + 'applicant': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_applied'", 'to': "orm['im.AstakosUser']"}), + 'chain': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'chained_apps'", 'db_column': "'chain'", 'to': "orm['im.Project']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'end_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'homepage': ('django.db.models.fields.URLField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'issue_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'limit_on_members_number': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'member_join_policy': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'member_leave_policy': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'null': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_owned'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'private': ('django.db.models.fields.NullBooleanField', [], {'default': 'False', 'null': 'True', 'blank': 'True'}), + 'resource_grants': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.ProjectResourceGrant']", 'blank': 'True'}), + 'response': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'response_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'responded_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'response_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'start_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}), + 'waive_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'waived_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'waive_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'waive_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.projectlock': { + 'Meta': {'object_name': 'ProjectLock'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.projectlog': { + 'Meta': {'object_name': 'ProjectLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.Project']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectmembership': { + 'Meta': {'unique_together': "(('person', 'project'),)", 'object_name': 'ProjectMembership'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Project']"}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}) + }, + 'im.projectmembershiplog': { + 'Meta': {'object_name': 'ProjectMembershipLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'membership': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.ProjectMembership']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectresourcegrant': { + 'Meta': {'unique_together': "(('resource', 'project_application'),)", 'object_name': 'ProjectResourceGrant'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'member_capacity': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'project_application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.ProjectApplication']", 'null': 'True'}), + 'project_capacity': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}) + }, + 'im.projectresourcequota': { + 'Meta': {'unique_together': "(('resource', 'project'),)", 'object_name': 'ProjectResourceQuota'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'member_capacity': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Project']"}), + 'project_capacity': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}) + }, + 'im.resource': { + 'Meta': {'object_name': 'Resource'}, + 'api_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'desc': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'project_default': ('django.db.models.fields.BigIntegerField', [], {}), + 'service_origin': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'service_type': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'ui_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'unit': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'uplimit': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) + }, + 'im.service': { + 'Meta': {'object_name': 'Service'}, + 'component': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Component']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.sessioncatalog': { + 'Meta': {'object_name': 'SessionCatalog'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.usersetting': { + 'Meta': {'unique_together': "(('user', 'setting'),)", 'object_name': 'UserSetting'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'setting': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'value': ('django.db.models.fields.IntegerField', [], {}) + } + } + + complete_apps = ['im'] diff --git a/snf-astakos-app/astakos/im/migrations/0074_auto__del_field_project_application__chg_field_project_limit_on_member.py b/snf-astakos-app/astakos/im/migrations/0074_auto__del_field_project_application__chg_field_project_limit_on_member.py new file mode 100644 index 0000000000000000000000000000000000000000..7a8f24a06d08bdcf9cff3b3349981c175e5f4fa4 --- /dev/null +++ b/snf-astakos-app/astakos/im/migrations/0074_auto__del_field_project_application__chg_field_project_limit_on_member.py @@ -0,0 +1,370 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Deleting field 'Project.application' + db.delete_column('im_project', 'application_id') + + # Changing field 'Project.limit_on_members_number' + db.alter_column('im_project', 'limit_on_members_number', self.gf('django.db.models.fields.BigIntegerField')(default=0)) + + # Changing field 'Project.member_join_policy' + db.alter_column('im_project', 'member_join_policy', self.gf('django.db.models.fields.IntegerField')(default=0)) + + # Changing field 'Project.end_date' + db.alter_column('im_project', 'end_date', self.gf('django.db.models.fields.DateTimeField')(default=None)) + + # Changing field 'Project.member_leave_policy' + db.alter_column('im_project', 'member_leave_policy', self.gf('django.db.models.fields.IntegerField')(default=0)) + + # Changing field 'Project.homepage' + db.alter_column('im_project', 'homepage', self.gf('django.db.models.fields.URLField')(default='', max_length=255)) + + # Changing field 'Project.realname' + db.alter_column('im_project', 'realname', self.gf('django.db.models.fields.CharField')(default='', max_length=80)) + + # Changing field 'Project.description' + db.alter_column('im_project', 'description', self.gf('django.db.models.fields.TextField')(default='')) + + + def backwards(self, orm): + + # Adding field 'Project.application' + db.add_column('im_project', 'application', self.gf('django.db.models.fields.related.OneToOneField')(default=None, related_name='project', unique=True, to=orm['im.ProjectApplication']), keep_default=False) + + # Changing field 'Project.limit_on_members_number' + db.alter_column('im_project', 'limit_on_members_number', self.gf('django.db.models.fields.BigIntegerField')(null=True)) + + # Changing field 'Project.member_join_policy' + db.alter_column('im_project', 'member_join_policy', self.gf('django.db.models.fields.IntegerField')(null=True)) + + # Changing field 'Project.end_date' + db.alter_column('im_project', 'end_date', self.gf('django.db.models.fields.DateTimeField')(null=True)) + + # Changing field 'Project.member_leave_policy' + db.alter_column('im_project', 'member_leave_policy', self.gf('django.db.models.fields.IntegerField')(null=True)) + + # Changing field 'Project.homepage' + db.alter_column('im_project', 'homepage', self.gf('django.db.models.fields.URLField')(max_length=255, null=True)) + + # Changing field 'Project.realname' + db.alter_column('im_project', 'realname', self.gf('django.db.models.fields.CharField')(max_length=80, null=True)) + + # Changing field 'Project.description' + db.alter_column('im_project', 'description', self.gf('django.db.models.fields.TextField')(null=True)) + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 12, 12, 15, 56, 16, 309692)'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 12, 12, 15, 56, 16, 309559)'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'im.additionalmail': { + 'Meta': {'object_name': 'AdditionalMail'}, + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.approvalterms': { + 'Meta': {'object_name': 'ApprovalTerms'}, + 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.astakosuser': { + 'Meta': {'object_name': 'AstakosUser', '_ormbases': ['auth.User']}, + 'accepted_email': ('django.db.models.fields.EmailField', [], {'default': 'None', 'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'accepted_policy': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'activation_sent': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date_signed_terms': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_reason': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}), + 'disturbed_quota': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'email_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_credits': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_signed_terms': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'invitations': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'is_rejected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'level': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'moderated_at': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'moderated_data': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.AstakosUserQuota']", 'symmetrical': 'False'}), + 'rejected_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {}), + 'user_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'primary_key': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'verification_code': ('django.db.models.fields.CharField', [], {'max_length': '255', 'unique': 'True', 'null': 'True'}), + 'verified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.astakosuserauthprovider': { + 'Meta': {'ordering': "('module', 'created')", 'unique_together': "(('identifier', 'module', 'user'),)", 'object_name': 'AstakosUserAuthProvider'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_backend': ('django.db.models.fields.CharField', [], {'default': "'astakos'", 'max_length': '255'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'info_data': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'module': ('django.db.models.fields.CharField', [], {'default': "'local'", 'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'auth_providers'", 'to': "orm['im.AstakosUser']"}) + }, + 'im.astakosuserquota': { + 'Meta': {'unique_together': "(('resource', 'user'),)", 'object_name': 'AstakosUserQuota'}, + 'capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.authproviderpolicyprofile': { + 'Meta': {'ordering': "['priority']", 'object_name': 'AuthProviderPolicyProfile'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['auth.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_exclusive': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'policy_add': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_automoderate': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_create': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_limit': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}), + 'policy_login': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_remove': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_required': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_switch': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'priority': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['im.AstakosUser']"}) + }, + 'im.chain': { + 'Meta': {'object_name': 'Chain'}, + 'chain': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.component': { + 'Meta': {'object_name': 'Component'}, + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'base_url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}) + }, + 'im.emailchange': { + 'Meta': {'object_name': 'EmailChange'}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'requested_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'emailchanges'", 'unique': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.endpoint': { + 'Meta': {'object_name': 'Endpoint'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'service': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'endpoints'", 'to': "orm['im.Service']"}) + }, + 'im.endpointdata': { + 'Meta': {'unique_together': "(('endpoint', 'key'),)", 'object_name': 'EndpointData'}, + 'endpoint': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'data'", 'to': "orm['im.Endpoint']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '1024'}) + }, + 'im.invitation': { + 'Meta': {'object_name': 'Invitation'}, + 'code': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'consumed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inviter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invitations_sent'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'is_consumed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'realname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.pendingthirdpartyuser': { + 'Meta': {'unique_together': "(('provider', 'third_party_identifier'),)", 'object_name': 'PendingThirdPartyUser'}, + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'info': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'third_party_identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'token': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'im.project': { + 'Meta': {'object_name': 'Project'}, + 'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'end_date': ('django.db.models.fields.DateTimeField', [], {}), + 'homepage': ('django.db.models.fields.URLField', [], {'max_length': '255'}), + 'id': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_column': "'id'"}), + 'last_application': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'last_of_project'", 'null': 'True', 'to': "orm['im.ProjectApplication']"}), + 'limit_on_members_number': ('django.db.models.fields.BigIntegerField', [], {}), + 'member_join_policy': ('django.db.models.fields.IntegerField', [], {}), + 'member_leave_policy': ('django.db.models.fields.IntegerField', [], {}), + 'members': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.AstakosUser']", 'through': "orm['im.ProjectMembership']", 'symmetrical': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True', 'null': 'True', 'db_index': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projs_owned'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'private': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'realname': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'resource_grants': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.ProjectResourceQuota']", 'blank': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '1', 'db_index': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.projectapplication': { + 'Meta': {'unique_together': "(('chain', 'id'),)", 'object_name': 'ProjectApplication'}, + 'applicant': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_applied'", 'to': "orm['im.AstakosUser']"}), + 'chain': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'chained_apps'", 'db_column': "'chain'", 'to': "orm['im.Project']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'end_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'homepage': ('django.db.models.fields.URLField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'issue_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'limit_on_members_number': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'member_join_policy': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'member_leave_policy': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'null': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_owned'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'private': ('django.db.models.fields.NullBooleanField', [], {'default': 'False', 'null': 'True', 'blank': 'True'}), + 'resource_grants': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.ProjectResourceGrant']", 'blank': 'True'}), + 'response': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'response_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'responded_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'response_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'start_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}), + 'waive_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'waived_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'waive_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'waive_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.projectlock': { + 'Meta': {'object_name': 'ProjectLock'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.projectlog': { + 'Meta': {'object_name': 'ProjectLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.Project']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectmembership': { + 'Meta': {'unique_together': "(('person', 'project'),)", 'object_name': 'ProjectMembership'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Project']"}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}) + }, + 'im.projectmembershiplog': { + 'Meta': {'object_name': 'ProjectMembershipLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'membership': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.ProjectMembership']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectresourcegrant': { + 'Meta': {'unique_together': "(('resource', 'project_application'),)", 'object_name': 'ProjectResourceGrant'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'member_capacity': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'project_application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.ProjectApplication']", 'null': 'True'}), + 'project_capacity': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}) + }, + 'im.projectresourcequota': { + 'Meta': {'unique_together': "(('resource', 'project'),)", 'object_name': 'ProjectResourceQuota'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'member_capacity': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Project']"}), + 'project_capacity': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}) + }, + 'im.resource': { + 'Meta': {'object_name': 'Resource'}, + 'api_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'desc': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'project_default': ('django.db.models.fields.BigIntegerField', [], {}), + 'service_origin': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'service_type': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'ui_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'unit': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'uplimit': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) + }, + 'im.service': { + 'Meta': {'object_name': 'Service'}, + 'component': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Component']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.sessioncatalog': { + 'Meta': {'object_name': 'SessionCatalog'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.usersetting': { + 'Meta': {'unique_together': "(('user', 'setting'),)", 'object_name': 'UserSetting'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'setting': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'value': ('django.db.models.fields.IntegerField', [], {}) + } + } + + complete_apps = ['im'] diff --git a/snf-astakos-app/astakos/im/migrations/0075_auto__chg_field_projectresourcegrant_project_capacity__chg_field_proje.py b/snf-astakos-app/astakos/im/migrations/0075_auto__chg_field_projectresourcegrant_project_capacity__chg_field_proje.py new file mode 100644 index 0000000000000000000000000000000000000000..1e8eb4a7c76e0ca563da6b87cc784210a1e1e04d --- /dev/null +++ b/snf-astakos-app/astakos/im/migrations/0075_auto__chg_field_projectresourcegrant_project_capacity__chg_field_proje.py @@ -0,0 +1,334 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Changing field 'ProjectResourceGrant.project_capacity' + db.alter_column('im_projectresourcegrant', 'project_capacity', self.gf('django.db.models.fields.BigIntegerField')(default=0)) + + # Changing field 'ProjectResourceGrant.project_application' + db.alter_column('im_projectresourcegrant', 'project_application_id', self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['im.ProjectApplication'])) + + + def backwards(self, orm): + + # Changing field 'ProjectResourceGrant.project_capacity' + db.alter_column('im_projectresourcegrant', 'project_capacity', self.gf('django.db.models.fields.BigIntegerField')(null=True)) + + # Changing field 'ProjectResourceGrant.project_application' + db.alter_column('im_projectresourcegrant', 'project_application_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['im.ProjectApplication'], null=True)) + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 1, 7, 15, 51, 55, 791617)'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 1, 7, 15, 51, 55, 791491)'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'im.additionalmail': { + 'Meta': {'object_name': 'AdditionalMail'}, + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.approvalterms': { + 'Meta': {'object_name': 'ApprovalTerms'}, + 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.astakosuser': { + 'Meta': {'object_name': 'AstakosUser', '_ormbases': ['auth.User']}, + 'accepted_email': ('django.db.models.fields.EmailField', [], {'default': 'None', 'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'accepted_policy': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'activation_sent': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date_signed_terms': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_reason': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}), + 'disturbed_quota': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'email_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_credits': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_signed_terms': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'invitations': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'is_rejected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'level': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'moderated_at': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'moderated_data': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.AstakosUserQuota']", 'symmetrical': 'False'}), + 'rejected_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {}), + 'user_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'primary_key': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'verification_code': ('django.db.models.fields.CharField', [], {'max_length': '255', 'unique': 'True', 'null': 'True'}), + 'verified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.astakosuserauthprovider': { + 'Meta': {'ordering': "('module', 'created')", 'unique_together': "(('identifier', 'module', 'user'),)", 'object_name': 'AstakosUserAuthProvider'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_backend': ('django.db.models.fields.CharField', [], {'default': "'astakos'", 'max_length': '255'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'info_data': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'module': ('django.db.models.fields.CharField', [], {'default': "'local'", 'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'auth_providers'", 'to': "orm['im.AstakosUser']"}) + }, + 'im.astakosuserquota': { + 'Meta': {'unique_together': "(('resource', 'user'),)", 'object_name': 'AstakosUserQuota'}, + 'capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.authproviderpolicyprofile': { + 'Meta': {'ordering': "['priority']", 'object_name': 'AuthProviderPolicyProfile'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['auth.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_exclusive': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'policy_add': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_automoderate': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_create': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_limit': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}), + 'policy_login': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_remove': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_required': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_switch': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'priority': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['im.AstakosUser']"}) + }, + 'im.chain': { + 'Meta': {'object_name': 'Chain'}, + 'chain': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.component': { + 'Meta': {'object_name': 'Component'}, + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'base_url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}) + }, + 'im.emailchange': { + 'Meta': {'object_name': 'EmailChange'}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'requested_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'emailchanges'", 'unique': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.endpoint': { + 'Meta': {'object_name': 'Endpoint'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'service': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'endpoints'", 'to': "orm['im.Service']"}) + }, + 'im.endpointdata': { + 'Meta': {'unique_together': "(('endpoint', 'key'),)", 'object_name': 'EndpointData'}, + 'endpoint': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'data'", 'to': "orm['im.Endpoint']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '1024'}) + }, + 'im.invitation': { + 'Meta': {'object_name': 'Invitation'}, + 'code': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'consumed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inviter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invitations_sent'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'is_consumed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'realname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.pendingthirdpartyuser': { + 'Meta': {'unique_together': "(('provider', 'third_party_identifier'),)", 'object_name': 'PendingThirdPartyUser'}, + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'info': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'third_party_identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'token': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'im.project': { + 'Meta': {'object_name': 'Project'}, + 'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'end_date': ('django.db.models.fields.DateTimeField', [], {}), + 'homepage': ('django.db.models.fields.URLField', [], {'max_length': '255'}), + 'id': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_column': "'id'"}), + 'last_application': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'last_of_project'", 'null': 'True', 'to': "orm['im.ProjectApplication']"}), + 'limit_on_members_number': ('django.db.models.fields.BigIntegerField', [], {}), + 'member_join_policy': ('django.db.models.fields.IntegerField', [], {}), + 'member_leave_policy': ('django.db.models.fields.IntegerField', [], {}), + 'members': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.AstakosUser']", 'through': "orm['im.ProjectMembership']", 'symmetrical': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True', 'null': 'True', 'db_index': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projs_owned'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'private': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'realname': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'resource_grants': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.ProjectResourceQuota']", 'blank': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.projectapplication': { + 'Meta': {'unique_together': "(('chain', 'id'),)", 'object_name': 'ProjectApplication'}, + 'applicant': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_applied'", 'to': "orm['im.AstakosUser']"}), + 'chain': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'chained_apps'", 'db_column': "'chain'", 'to': "orm['im.Project']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'end_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'homepage': ('django.db.models.fields.URLField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'issue_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'limit_on_members_number': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'member_join_policy': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'member_leave_policy': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'null': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_owned'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'private': ('django.db.models.fields.NullBooleanField', [], {'default': 'False', 'null': 'True', 'blank': 'True'}), + 'resource_grants': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.ProjectResourceGrant']", 'blank': 'True'}), + 'response': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'response_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'responded_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'response_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'start_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}), + 'waive_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'waived_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'waive_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'waive_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.projectlock': { + 'Meta': {'object_name': 'ProjectLock'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.projectlog': { + 'Meta': {'object_name': 'ProjectLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.Project']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectmembership': { + 'Meta': {'unique_together': "(('person', 'project'),)", 'object_name': 'ProjectMembership'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Project']"}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}) + }, + 'im.projectmembershiplog': { + 'Meta': {'object_name': 'ProjectMembershipLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'membership': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.ProjectMembership']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectresourcegrant': { + 'Meta': {'unique_together': "(('resource', 'project_application'),)", 'object_name': 'ProjectResourceGrant'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'member_capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'project_application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.ProjectApplication']"}), + 'project_capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}) + }, + 'im.projectresourcequota': { + 'Meta': {'unique_together': "(('resource', 'project'),)", 'object_name': 'ProjectResourceQuota'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'member_capacity': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Project']"}), + 'project_capacity': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}) + }, + 'im.resource': { + 'Meta': {'object_name': 'Resource'}, + 'api_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'desc': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'project_default': ('django.db.models.fields.BigIntegerField', [], {}), + 'service_origin': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'service_type': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'ui_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'unit': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'uplimit': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) + }, + 'im.service': { + 'Meta': {'object_name': 'Service'}, + 'component': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Component']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.sessioncatalog': { + 'Meta': {'object_name': 'SessionCatalog'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.usersetting': { + 'Meta': {'unique_together': "(('user', 'setting'),)", 'object_name': 'UserSetting'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'setting': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'value': ('django.db.models.fields.IntegerField', [], {}) + } + } + + complete_apps = ['im'] diff --git a/snf-astakos-app/astakos/im/migrations/0076_auto__add_field_project_is_base__add_field_astakosuser_base_project.py b/snf-astakos-app/astakos/im/migrations/0076_auto__add_field_project_is_base__add_field_astakosuser_base_project.py new file mode 100644 index 0000000000000000000000000000000000000000..7f7b33d9becf49795face96cc266c6b4642e24cc --- /dev/null +++ b/snf-astakos-app/astakos/im/migrations/0076_auto__add_field_project_is_base__add_field_astakosuser_base_project.py @@ -0,0 +1,336 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Adding field 'Project.is_base' + db.add_column('im_project', 'is_base', self.gf('django.db.models.fields.BooleanField')(default=False), keep_default=False) + + # Adding field 'AstakosUser.base_project' + db.add_column('im_astakosuser', 'base_project', self.gf('django.db.models.fields.related.ForeignKey')(related_name='base_user', null=True, to=orm['im.Project']), keep_default=False) + + + def backwards(self, orm): + + # Deleting field 'Project.is_base' + db.delete_column('im_project', 'is_base') + + # Deleting field 'AstakosUser.base_project' + db.delete_column('im_astakosuser', 'base_project_id') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 1, 27, 15, 7, 18, 809294)'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 1, 27, 15, 7, 18, 809240)'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'im.additionalmail': { + 'Meta': {'object_name': 'AdditionalMail'}, + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.approvalterms': { + 'Meta': {'object_name': 'ApprovalTerms'}, + 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.astakosuser': { + 'Meta': {'object_name': 'AstakosUser', '_ormbases': ['auth.User']}, + 'accepted_email': ('django.db.models.fields.EmailField', [], {'default': 'None', 'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'accepted_policy': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'activation_sent': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'base_project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'base_user'", 'null': 'True', 'to': "orm['im.Project']"}), + 'date_signed_terms': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_reason': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}), + 'disturbed_quota': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'email_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_credits': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_signed_terms': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'invitations': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'is_rejected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'level': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'moderated_at': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'moderated_data': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.AstakosUserQuota']", 'symmetrical': 'False'}), + 'rejected_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {}), + 'user_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'primary_key': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'verification_code': ('django.db.models.fields.CharField', [], {'max_length': '255', 'unique': 'True', 'null': 'True'}), + 'verified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.astakosuserauthprovider': { + 'Meta': {'ordering': "('module', 'created')", 'unique_together': "(('identifier', 'module', 'user'),)", 'object_name': 'AstakosUserAuthProvider'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_backend': ('django.db.models.fields.CharField', [], {'default': "'astakos'", 'max_length': '255'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'info_data': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'module': ('django.db.models.fields.CharField', [], {'default': "'local'", 'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'auth_providers'", 'to': "orm['im.AstakosUser']"}) + }, + 'im.astakosuserquota': { + 'Meta': {'unique_together': "(('resource', 'user'),)", 'object_name': 'AstakosUserQuota'}, + 'capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.authproviderpolicyprofile': { + 'Meta': {'ordering': "['priority']", 'object_name': 'AuthProviderPolicyProfile'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['auth.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_exclusive': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'policy_add': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_automoderate': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_create': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_limit': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}), + 'policy_login': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_remove': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_required': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_switch': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'priority': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['im.AstakosUser']"}) + }, + 'im.chain': { + 'Meta': {'object_name': 'Chain'}, + 'chain': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.component': { + 'Meta': {'object_name': 'Component'}, + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'base_url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}) + }, + 'im.emailchange': { + 'Meta': {'object_name': 'EmailChange'}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'requested_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'emailchanges'", 'unique': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.endpoint': { + 'Meta': {'object_name': 'Endpoint'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'service': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'endpoints'", 'to': "orm['im.Service']"}) + }, + 'im.endpointdata': { + 'Meta': {'unique_together': "(('endpoint', 'key'),)", 'object_name': 'EndpointData'}, + 'endpoint': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'data'", 'to': "orm['im.Endpoint']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '1024'}) + }, + 'im.invitation': { + 'Meta': {'object_name': 'Invitation'}, + 'code': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'consumed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inviter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invitations_sent'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'is_consumed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'realname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.pendingthirdpartyuser': { + 'Meta': {'unique_together': "(('provider', 'third_party_identifier'),)", 'object_name': 'PendingThirdPartyUser'}, + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'info': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'third_party_identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'token': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'im.project': { + 'Meta': {'object_name': 'Project'}, + 'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'end_date': ('django.db.models.fields.DateTimeField', [], {}), + 'homepage': ('django.db.models.fields.URLField', [], {'max_length': '255'}), + 'id': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_column': "'id'"}), + 'is_base': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_application': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'last_of_project'", 'null': 'True', 'to': "orm['im.ProjectApplication']"}), + 'limit_on_members_number': ('django.db.models.fields.BigIntegerField', [], {}), + 'member_join_policy': ('django.db.models.fields.IntegerField', [], {}), + 'member_leave_policy': ('django.db.models.fields.IntegerField', [], {}), + 'members': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.AstakosUser']", 'through': "orm['im.ProjectMembership']", 'symmetrical': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True', 'null': 'True', 'db_index': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projs_owned'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'private': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'realname': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'resource_grants': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.ProjectResourceQuota']", 'blank': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.projectapplication': { + 'Meta': {'unique_together': "(('chain', 'id'),)", 'object_name': 'ProjectApplication'}, + 'applicant': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_applied'", 'to': "orm['im.AstakosUser']"}), + 'chain': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'chained_apps'", 'db_column': "'chain'", 'to': "orm['im.Project']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'end_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'homepage': ('django.db.models.fields.URLField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'issue_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'limit_on_members_number': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'member_join_policy': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'member_leave_policy': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'null': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_owned'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'private': ('django.db.models.fields.NullBooleanField', [], {'default': 'False', 'null': 'True', 'blank': 'True'}), + 'resource_grants': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.ProjectResourceGrant']", 'blank': 'True'}), + 'response': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'response_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'responded_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'response_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'start_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}), + 'waive_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'waived_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'waive_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'waive_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.projectlock': { + 'Meta': {'object_name': 'ProjectLock'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.projectlog': { + 'Meta': {'object_name': 'ProjectLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.Project']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectmembership': { + 'Meta': {'unique_together': "(('person', 'project'),)", 'object_name': 'ProjectMembership'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Project']"}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}) + }, + 'im.projectmembershiplog': { + 'Meta': {'object_name': 'ProjectMembershipLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'membership': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.ProjectMembership']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectresourcegrant': { + 'Meta': {'unique_together': "(('resource', 'project_application'),)", 'object_name': 'ProjectResourceGrant'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'member_capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'project_application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.ProjectApplication']"}), + 'project_capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}) + }, + 'im.projectresourcequota': { + 'Meta': {'unique_together': "(('resource', 'project'),)", 'object_name': 'ProjectResourceQuota'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'member_capacity': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Project']"}), + 'project_capacity': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}) + }, + 'im.resource': { + 'Meta': {'object_name': 'Resource'}, + 'api_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'desc': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'project_default': ('django.db.models.fields.BigIntegerField', [], {}), + 'service_origin': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'service_type': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'ui_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'unit': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'uplimit': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) + }, + 'im.service': { + 'Meta': {'object_name': 'Service'}, + 'component': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Component']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.sessioncatalog': { + 'Meta': {'object_name': 'SessionCatalog'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.usersetting': { + 'Meta': {'unique_together': "(('user', 'setting'),)", 'object_name': 'UserSetting'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'setting': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'value': ('django.db.models.fields.IntegerField', [], {}) + } + } + + complete_apps = ['im'] diff --git a/snf-astakos-app/astakos/im/migrations/0077_base_projects.py b/snf-astakos-app/astakos/im/migrations/0077_base_projects.py new file mode 100644 index 0000000000000000000000000000000000000000..fc0f6b7059a0824f9719908a22152fcc615a508e --- /dev/null +++ b/snf-astakos-app/astakos/im/migrations/0077_base_projects.py @@ -0,0 +1,394 @@ +# encoding: utf-8 +import datetime +from dateutil.relativedelta import relativedelta +from south.db import db +from south.v2 import DataMigration +from django.db import models + +CLOSED_POLICY = 3 +ACTIVATED = 1 + +class Migration(DataMigration): + + def new_chain(self, orm): + return orm.Chain.objects.create() + + def base_resources(self, orm, user): + resources = orm.Resource.objects.all() + grants = {} + for resource in resources: + grants[resource] = resource.uplimit + + objs = orm.AstakosUserQuota.objects.select_related() + custom_quota = objs.filter(user=user) + for cq in custom_quota: + grants[cq.resource] = cq.capacity + + tuples = [] + for resource, capacity in grants.iteritems(): + tuples.append((resource, capacity, capacity)) + return tuples + + def set_resources(self, project, grants): + for resource, m_capacity, p_capacity in grants: + g = project.projectresourcequota_set + g.create(resource=resource, + member_capacity=m_capacity, + project_capacity=p_capacity) + + def make_base_project(self, orm, user): + chain = self.new_chain(orm) + orm.Project.objects.create( + id=chain.chain, + uuid=user.uuid, + last_application=None, + owner=None, + realname=("system:" + user.uuid), + homepage="", + description=("system project for user " + user.username), + end_date=(datetime.datetime.now() + relativedelta(years=100)), + member_join_policy=CLOSED_POLICY, + member_leave_policy=CLOSED_POLICY, + limit_on_members_number=1, + private=True, + is_base=True) + + user.base_project_id = chain.chain + user.save() + + def new_membership(self, orm, project, user): + m = orm.ProjectMembership.objects.create( + project=project, person=user, state=1) + now = datetime.datetime.now() + m.log.create(from_state=None, to_state=1, date=now) + + def enable_base_project(self, orm, user): + project = user.base_project + project.name = project.realname + project.state = ACTIVATED + project.save() + base_grants = self.base_resources(orm, user) + self.set_resources(project, base_grants) + self.new_membership(orm, project, user) + + def forwards(self, orm): + acc_users = orm.AstakosUser.objects.filter(moderated=True, + is_rejected=False) + for user in acc_users: + self.make_base_project(orm, user) + self.enable_base_project(orm, user) + + def backwards(self, orm): + "Write your backwards methods here." + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 1, 27, 15, 9, 56, 442174)'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 1, 27, 15, 9, 56, 442123)'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'im.additionalmail': { + 'Meta': {'object_name': 'AdditionalMail'}, + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.approvalterms': { + 'Meta': {'object_name': 'ApprovalTerms'}, + 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.astakosuser': { + 'Meta': {'object_name': 'AstakosUser', '_ormbases': ['auth.User']}, + 'accepted_email': ('django.db.models.fields.EmailField', [], {'default': 'None', 'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'accepted_policy': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'activation_sent': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'base_project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'base_user'", 'null': 'True', 'to': "orm['im.Project']"}), + 'date_signed_terms': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_reason': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}), + 'disturbed_quota': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'email_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_credits': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_signed_terms': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'invitations': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'is_rejected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'level': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'moderated_at': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'moderated_data': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.AstakosUserQuota']", 'symmetrical': 'False'}), + 'rejected_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {}), + 'user_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'primary_key': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'verification_code': ('django.db.models.fields.CharField', [], {'max_length': '255', 'unique': 'True', 'null': 'True'}), + 'verified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.astakosuserauthprovider': { + 'Meta': {'ordering': "('module', 'created')", 'unique_together': "(('identifier', 'module', 'user'),)", 'object_name': 'AstakosUserAuthProvider'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_backend': ('django.db.models.fields.CharField', [], {'default': "'astakos'", 'max_length': '255'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'info_data': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'module': ('django.db.models.fields.CharField', [], {'default': "'local'", 'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'auth_providers'", 'to': "orm['im.AstakosUser']"}) + }, + 'im.astakosuserquota': { + 'Meta': {'unique_together': "(('resource', 'user'),)", 'object_name': 'AstakosUserQuota'}, + 'capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.authproviderpolicyprofile': { + 'Meta': {'ordering': "['priority']", 'object_name': 'AuthProviderPolicyProfile'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['auth.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_exclusive': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'policy_add': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_automoderate': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_create': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_limit': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}), + 'policy_login': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_remove': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_required': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_switch': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'priority': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['im.AstakosUser']"}) + }, + 'im.chain': { + 'Meta': {'object_name': 'Chain'}, + 'chain': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.component': { + 'Meta': {'object_name': 'Component'}, + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'base_url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}) + }, + 'im.emailchange': { + 'Meta': {'object_name': 'EmailChange'}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'requested_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'emailchanges'", 'unique': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.endpoint': { + 'Meta': {'object_name': 'Endpoint'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'service': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'endpoints'", 'to': "orm['im.Service']"}) + }, + 'im.endpointdata': { + 'Meta': {'unique_together': "(('endpoint', 'key'),)", 'object_name': 'EndpointData'}, + 'endpoint': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'data'", 'to': "orm['im.Endpoint']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '1024'}) + }, + 'im.invitation': { + 'Meta': {'object_name': 'Invitation'}, + 'code': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'consumed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inviter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invitations_sent'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'is_consumed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'realname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.pendingthirdpartyuser': { + 'Meta': {'unique_together': "(('provider', 'third_party_identifier'),)", 'object_name': 'PendingThirdPartyUser'}, + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'info': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'third_party_identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'token': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'im.project': { + 'Meta': {'object_name': 'Project'}, + 'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'end_date': ('django.db.models.fields.DateTimeField', [], {}), + 'homepage': ('django.db.models.fields.URLField', [], {'max_length': '255'}), + 'id': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_column': "'id'"}), + 'is_base': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_application': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'last_of_project'", 'null': 'True', 'to': "orm['im.ProjectApplication']"}), + 'limit_on_members_number': ('django.db.models.fields.BigIntegerField', [], {}), + 'member_join_policy': ('django.db.models.fields.IntegerField', [], {}), + 'member_leave_policy': ('django.db.models.fields.IntegerField', [], {}), + 'members': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.AstakosUser']", 'through': "orm['im.ProjectMembership']", 'symmetrical': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True', 'null': 'True', 'db_index': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projs_owned'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'private': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'realname': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'resource_grants': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.ProjectResourceQuota']", 'blank': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.projectapplication': { + 'Meta': {'unique_together': "(('chain', 'id'),)", 'object_name': 'ProjectApplication'}, + 'applicant': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_applied'", 'to': "orm['im.AstakosUser']"}), + 'chain': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'chained_apps'", 'db_column': "'chain'", 'to': "orm['im.Project']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'end_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'homepage': ('django.db.models.fields.URLField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'issue_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'limit_on_members_number': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'member_join_policy': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'member_leave_policy': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'null': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_owned'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'private': ('django.db.models.fields.NullBooleanField', [], {'default': 'False', 'null': 'True', 'blank': 'True'}), + 'resource_grants': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.ProjectResourceGrant']", 'blank': 'True'}), + 'response': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'response_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'responded_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'response_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'start_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}), + 'waive_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'waived_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'waive_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'waive_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.projectlock': { + 'Meta': {'object_name': 'ProjectLock'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.projectlog': { + 'Meta': {'object_name': 'ProjectLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.Project']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectmembership': { + 'Meta': {'unique_together': "(('person', 'project'),)", 'object_name': 'ProjectMembership'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Project']"}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}) + }, + 'im.projectmembershiplog': { + 'Meta': {'object_name': 'ProjectMembershipLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'membership': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.ProjectMembership']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectresourcegrant': { + 'Meta': {'unique_together': "(('resource', 'project_application'),)", 'object_name': 'ProjectResourceGrant'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'member_capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'project_application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.ProjectApplication']"}), + 'project_capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}) + }, + 'im.projectresourcequota': { + 'Meta': {'unique_together': "(('resource', 'project'),)", 'object_name': 'ProjectResourceQuota'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'member_capacity': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Project']"}), + 'project_capacity': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}) + }, + 'im.resource': { + 'Meta': {'object_name': 'Resource'}, + 'api_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'desc': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'project_default': ('django.db.models.fields.BigIntegerField', [], {}), + 'service_origin': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'service_type': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'ui_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'unit': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'uplimit': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) + }, + 'im.service': { + 'Meta': {'object_name': 'Service'}, + 'component': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Component']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.sessioncatalog': { + 'Meta': {'object_name': 'SessionCatalog'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.usersetting': { + 'Meta': {'unique_together': "(('user', 'setting'),)", 'object_name': 'UserSetting'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'setting': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'value': ('django.db.models.fields.IntegerField', [], {}) + } + } + + complete_apps = ['im'] diff --git a/snf-astakos-app/astakos/im/migrations/0078_auto__chg_field_astakosuser_base_project.py b/snf-astakos-app/astakos/im/migrations/0078_auto__chg_field_astakosuser_base_project.py new file mode 100644 index 0000000000000000000000000000000000000000..5cedf3df9971ccee79e0e782a40582d054e91e57 --- /dev/null +++ b/snf-astakos-app/astakos/im/migrations/0078_auto__chg_field_astakosuser_base_project.py @@ -0,0 +1,326 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + pass + + + def backwards(self, orm): + pass + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 1, 27, 15, 11, 55, 252204)'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 1, 27, 15, 11, 55, 252079)'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'im.additionalmail': { + 'Meta': {'object_name': 'AdditionalMail'}, + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.approvalterms': { + 'Meta': {'object_name': 'ApprovalTerms'}, + 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.astakosuser': { + 'Meta': {'object_name': 'AstakosUser', '_ormbases': ['auth.User']}, + 'accepted_email': ('django.db.models.fields.EmailField', [], {'default': 'None', 'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'accepted_policy': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'activation_sent': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'base_project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'base_user'", 'null': 'True', 'to': "orm['im.Project']"}), + 'date_signed_terms': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_reason': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}), + 'disturbed_quota': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'email_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_credits': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_signed_terms': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'invitations': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'is_rejected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'level': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'moderated_at': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'moderated_data': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.AstakosUserQuota']", 'symmetrical': 'False'}), + 'rejected_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {}), + 'user_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'primary_key': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'verification_code': ('django.db.models.fields.CharField', [], {'max_length': '255', 'unique': 'True', 'null': 'True'}), + 'verified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.astakosuserauthprovider': { + 'Meta': {'ordering': "('module', 'created')", 'unique_together': "(('identifier', 'module', 'user'),)", 'object_name': 'AstakosUserAuthProvider'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_backend': ('django.db.models.fields.CharField', [], {'default': "'astakos'", 'max_length': '255'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'info_data': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'module': ('django.db.models.fields.CharField', [], {'default': "'local'", 'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'auth_providers'", 'to': "orm['im.AstakosUser']"}) + }, + 'im.astakosuserquota': { + 'Meta': {'unique_together': "(('resource', 'user'),)", 'object_name': 'AstakosUserQuota'}, + 'capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.authproviderpolicyprofile': { + 'Meta': {'ordering': "['priority']", 'object_name': 'AuthProviderPolicyProfile'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['auth.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_exclusive': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'policy_add': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_automoderate': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_create': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_limit': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}), + 'policy_login': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_remove': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_required': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_switch': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'priority': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['im.AstakosUser']"}) + }, + 'im.chain': { + 'Meta': {'object_name': 'Chain'}, + 'chain': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.component': { + 'Meta': {'object_name': 'Component'}, + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'base_url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}) + }, + 'im.emailchange': { + 'Meta': {'object_name': 'EmailChange'}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'requested_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'emailchanges'", 'unique': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.endpoint': { + 'Meta': {'object_name': 'Endpoint'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'service': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'endpoints'", 'to': "orm['im.Service']"}) + }, + 'im.endpointdata': { + 'Meta': {'unique_together': "(('endpoint', 'key'),)", 'object_name': 'EndpointData'}, + 'endpoint': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'data'", 'to': "orm['im.Endpoint']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '1024'}) + }, + 'im.invitation': { + 'Meta': {'object_name': 'Invitation'}, + 'code': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'consumed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inviter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invitations_sent'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'is_consumed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'realname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.pendingthirdpartyuser': { + 'Meta': {'unique_together': "(('provider', 'third_party_identifier'),)", 'object_name': 'PendingThirdPartyUser'}, + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'info': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'third_party_identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'token': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'im.project': { + 'Meta': {'object_name': 'Project'}, + 'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'end_date': ('django.db.models.fields.DateTimeField', [], {}), + 'homepage': ('django.db.models.fields.URLField', [], {'max_length': '255'}), + 'id': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_column': "'id'"}), + 'is_base': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_application': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'last_of_project'", 'null': 'True', 'to': "orm['im.ProjectApplication']"}), + 'limit_on_members_number': ('django.db.models.fields.BigIntegerField', [], {}), + 'member_join_policy': ('django.db.models.fields.IntegerField', [], {}), + 'member_leave_policy': ('django.db.models.fields.IntegerField', [], {}), + 'members': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.AstakosUser']", 'through': "orm['im.ProjectMembership']", 'symmetrical': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True', 'null': 'True', 'db_index': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projs_owned'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'private': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'realname': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'resource_grants': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.ProjectResourceQuota']", 'blank': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.projectapplication': { + 'Meta': {'unique_together': "(('chain', 'id'),)", 'object_name': 'ProjectApplication'}, + 'applicant': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_applied'", 'to': "orm['im.AstakosUser']"}), + 'chain': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'chained_apps'", 'db_column': "'chain'", 'to': "orm['im.Project']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'end_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'homepage': ('django.db.models.fields.URLField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'issue_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'limit_on_members_number': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'member_join_policy': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'member_leave_policy': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'null': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_owned'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'private': ('django.db.models.fields.NullBooleanField', [], {'default': 'False', 'null': 'True', 'blank': 'True'}), + 'resource_grants': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.ProjectResourceGrant']", 'blank': 'True'}), + 'response': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'response_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'responded_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'response_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'start_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}), + 'waive_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'waived_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'waive_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'waive_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.projectlock': { + 'Meta': {'object_name': 'ProjectLock'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.projectlog': { + 'Meta': {'object_name': 'ProjectLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.Project']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectmembership': { + 'Meta': {'unique_together': "(('person', 'project'),)", 'object_name': 'ProjectMembership'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Project']"}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}) + }, + 'im.projectmembershiplog': { + 'Meta': {'object_name': 'ProjectMembershipLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'membership': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.ProjectMembership']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectresourcegrant': { + 'Meta': {'unique_together': "(('resource', 'project_application'),)", 'object_name': 'ProjectResourceGrant'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'member_capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'project_application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.ProjectApplication']"}), + 'project_capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}) + }, + 'im.projectresourcequota': { + 'Meta': {'unique_together': "(('resource', 'project'),)", 'object_name': 'ProjectResourceQuota'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'member_capacity': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Project']"}), + 'project_capacity': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}) + }, + 'im.resource': { + 'Meta': {'object_name': 'Resource'}, + 'api_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'desc': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'project_default': ('django.db.models.fields.BigIntegerField', [], {}), + 'service_origin': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'service_type': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'ui_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'unit': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'uplimit': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) + }, + 'im.service': { + 'Meta': {'object_name': 'Service'}, + 'component': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Component']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.sessioncatalog': { + 'Meta': {'object_name': 'SessionCatalog'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.usersetting': { + 'Meta': {'unique_together': "(('user', 'setting'),)", 'object_name': 'UserSetting'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'setting': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'value': ('django.db.models.fields.IntegerField', [], {}) + } + } + + complete_apps = ['im'] diff --git a/snf-astakos-app/astakos/im/migrations/0079_auto__add_field_projectmembership_initialized.py b/snf-astakos-app/astakos/im/migrations/0079_auto__add_field_projectmembership_initialized.py new file mode 100644 index 0000000000000000000000000000000000000000..c18f4ae1dfea429a6e0467c4e13f363bd1e1f3ad --- /dev/null +++ b/snf-astakos-app/astakos/im/migrations/0079_auto__add_field_projectmembership_initialized.py @@ -0,0 +1,331 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Adding field 'ProjectMembership.initialized' + db.add_column('im_projectmembership', 'initialized', self.gf('django.db.models.fields.BooleanField')(default=False), keep_default=False) + + + def backwards(self, orm): + + # Deleting field 'ProjectMembership.initialized' + db.delete_column('im_projectmembership', 'initialized') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 1, 28, 11, 58, 35, 462167)'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 1, 28, 11, 58, 35, 462107)'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'im.additionalmail': { + 'Meta': {'object_name': 'AdditionalMail'}, + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.approvalterms': { + 'Meta': {'object_name': 'ApprovalTerms'}, + 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.astakosuser': { + 'Meta': {'object_name': 'AstakosUser', '_ormbases': ['auth.User']}, + 'accepted_email': ('django.db.models.fields.EmailField', [], {'default': 'None', 'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'accepted_policy': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'activation_sent': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'base_project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'base_user'", 'null': 'True', 'to': "orm['im.Project']"}), + 'date_signed_terms': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_reason': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}), + 'disturbed_quota': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'email_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_credits': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_signed_terms': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'invitations': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'is_rejected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'level': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'moderated_at': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'moderated_data': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.AstakosUserQuota']", 'symmetrical': 'False'}), + 'rejected_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {}), + 'user_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'primary_key': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'verification_code': ('django.db.models.fields.CharField', [], {'max_length': '255', 'unique': 'True', 'null': 'True'}), + 'verified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.astakosuserauthprovider': { + 'Meta': {'ordering': "('module', 'created')", 'unique_together': "(('identifier', 'module', 'user'),)", 'object_name': 'AstakosUserAuthProvider'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_backend': ('django.db.models.fields.CharField', [], {'default': "'astakos'", 'max_length': '255'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'info_data': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'module': ('django.db.models.fields.CharField', [], {'default': "'local'", 'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'auth_providers'", 'to': "orm['im.AstakosUser']"}) + }, + 'im.astakosuserquota': { + 'Meta': {'unique_together': "(('resource', 'user'),)", 'object_name': 'AstakosUserQuota'}, + 'capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.authproviderpolicyprofile': { + 'Meta': {'ordering': "['priority']", 'object_name': 'AuthProviderPolicyProfile'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['auth.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_exclusive': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'policy_add': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_automoderate': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_create': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_limit': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}), + 'policy_login': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_remove': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_required': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_switch': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'priority': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['im.AstakosUser']"}) + }, + 'im.chain': { + 'Meta': {'object_name': 'Chain'}, + 'chain': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.component': { + 'Meta': {'object_name': 'Component'}, + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'base_url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}) + }, + 'im.emailchange': { + 'Meta': {'object_name': 'EmailChange'}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'requested_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'emailchanges'", 'unique': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.endpoint': { + 'Meta': {'object_name': 'Endpoint'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'service': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'endpoints'", 'to': "orm['im.Service']"}) + }, + 'im.endpointdata': { + 'Meta': {'unique_together': "(('endpoint', 'key'),)", 'object_name': 'EndpointData'}, + 'endpoint': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'data'", 'to': "orm['im.Endpoint']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '1024'}) + }, + 'im.invitation': { + 'Meta': {'object_name': 'Invitation'}, + 'code': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'consumed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inviter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invitations_sent'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'is_consumed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'realname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.pendingthirdpartyuser': { + 'Meta': {'unique_together': "(('provider', 'third_party_identifier'),)", 'object_name': 'PendingThirdPartyUser'}, + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'info': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'third_party_identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'token': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'im.project': { + 'Meta': {'object_name': 'Project'}, + 'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'end_date': ('django.db.models.fields.DateTimeField', [], {}), + 'homepage': ('django.db.models.fields.URLField', [], {'max_length': '255'}), + 'id': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_column': "'id'"}), + 'is_base': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_application': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'last_of_project'", 'null': 'True', 'to': "orm['im.ProjectApplication']"}), + 'limit_on_members_number': ('django.db.models.fields.BigIntegerField', [], {}), + 'member_join_policy': ('django.db.models.fields.IntegerField', [], {}), + 'member_leave_policy': ('django.db.models.fields.IntegerField', [], {}), + 'members': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.AstakosUser']", 'through': "orm['im.ProjectMembership']", 'symmetrical': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True', 'null': 'True', 'db_index': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projs_owned'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'private': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'realname': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'resource_grants': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.ProjectResourceQuota']", 'blank': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.projectapplication': { + 'Meta': {'unique_together': "(('chain', 'id'),)", 'object_name': 'ProjectApplication'}, + 'applicant': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_applied'", 'to': "orm['im.AstakosUser']"}), + 'chain': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'chained_apps'", 'db_column': "'chain'", 'to': "orm['im.Project']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'end_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'homepage': ('django.db.models.fields.URLField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'issue_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'limit_on_members_number': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'member_join_policy': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'member_leave_policy': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'null': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_owned'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'private': ('django.db.models.fields.NullBooleanField', [], {'default': 'False', 'null': 'True', 'blank': 'True'}), + 'resource_grants': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.ProjectResourceGrant']", 'blank': 'True'}), + 'response': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'response_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'responded_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'response_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'start_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}), + 'waive_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'waived_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'waive_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'waive_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.projectlock': { + 'Meta': {'object_name': 'ProjectLock'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.projectlog': { + 'Meta': {'object_name': 'ProjectLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.Project']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectmembership': { + 'Meta': {'unique_together': "(('person', 'project'),)", 'object_name': 'ProjectMembership'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'initialized': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Project']"}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}) + }, + 'im.projectmembershiplog': { + 'Meta': {'object_name': 'ProjectMembershipLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'membership': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.ProjectMembership']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectresourcegrant': { + 'Meta': {'unique_together': "(('resource', 'project_application'),)", 'object_name': 'ProjectResourceGrant'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'member_capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'project_application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.ProjectApplication']"}), + 'project_capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}) + }, + 'im.projectresourcequota': { + 'Meta': {'unique_together': "(('resource', 'project'),)", 'object_name': 'ProjectResourceQuota'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'member_capacity': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Project']"}), + 'project_capacity': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}) + }, + 'im.resource': { + 'Meta': {'object_name': 'Resource'}, + 'api_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'desc': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'project_default': ('django.db.models.fields.BigIntegerField', [], {}), + 'service_origin': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'service_type': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'ui_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'unit': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'uplimit': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) + }, + 'im.service': { + 'Meta': {'object_name': 'Service'}, + 'component': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Component']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.sessioncatalog': { + 'Meta': {'object_name': 'SessionCatalog'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.usersetting': { + 'Meta': {'unique_together': "(('user', 'setting'),)", 'object_name': 'UserSetting'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'setting': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'value': ('django.db.models.fields.IntegerField', [], {}) + } + } + + complete_apps = ['im'] diff --git a/snf-astakos-app/astakos/im/migrations/0080_initialized_memberships.py b/snf-astakos-app/astakos/im/migrations/0080_initialized_memberships.py new file mode 100644 index 0000000000000000000000000000000000000000..e7a6ce10b63ef71344944fb9b457e86fee99b147 --- /dev/null +++ b/snf-astakos-app/astakos/im/migrations/0080_initialized_memberships.py @@ -0,0 +1,334 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +ACCEPTED_STATES = [1, 5, 10] + +class Migration(DataMigration): + + def forwards(self, orm): + orm.ProjectMembership.objects.filter(state__in=ACCEPTED_STATES).\ + update(initialized=True) + ms = orm.ProjectMembershipLog.objects.\ + filter(to_state__in=ACCEPTED_STATES).\ + values_list("membership", flat=True) + orm.ProjectMembership.objects.filter(id__in=ms).\ + update(initialized=True) + + def backwards(self, orm): + "Write your backwards methods here." + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 1, 28, 12, 4, 23, 808946)'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 1, 28, 12, 4, 23, 808896)'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'im.additionalmail': { + 'Meta': {'object_name': 'AdditionalMail'}, + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.approvalterms': { + 'Meta': {'object_name': 'ApprovalTerms'}, + 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.astakosuser': { + 'Meta': {'object_name': 'AstakosUser', '_ormbases': ['auth.User']}, + 'accepted_email': ('django.db.models.fields.EmailField', [], {'default': 'None', 'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'accepted_policy': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'activation_sent': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'base_project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'base_user'", 'null': 'True', 'to': "orm['im.Project']"}), + 'date_signed_terms': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_reason': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}), + 'disturbed_quota': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'email_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_credits': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_signed_terms': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'invitations': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'is_rejected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'level': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'moderated_at': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'moderated_data': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.AstakosUserQuota']", 'symmetrical': 'False'}), + 'rejected_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {}), + 'user_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'primary_key': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'verification_code': ('django.db.models.fields.CharField', [], {'max_length': '255', 'unique': 'True', 'null': 'True'}), + 'verified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.astakosuserauthprovider': { + 'Meta': {'ordering': "('module', 'created')", 'unique_together': "(('identifier', 'module', 'user'),)", 'object_name': 'AstakosUserAuthProvider'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_backend': ('django.db.models.fields.CharField', [], {'default': "'astakos'", 'max_length': '255'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'info_data': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'module': ('django.db.models.fields.CharField', [], {'default': "'local'", 'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'auth_providers'", 'to': "orm['im.AstakosUser']"}) + }, + 'im.astakosuserquota': { + 'Meta': {'unique_together': "(('resource', 'user'),)", 'object_name': 'AstakosUserQuota'}, + 'capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.authproviderpolicyprofile': { + 'Meta': {'ordering': "['priority']", 'object_name': 'AuthProviderPolicyProfile'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['auth.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_exclusive': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'policy_add': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_automoderate': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_create': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_limit': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}), + 'policy_login': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_remove': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_required': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_switch': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'priority': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['im.AstakosUser']"}) + }, + 'im.chain': { + 'Meta': {'object_name': 'Chain'}, + 'chain': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.component': { + 'Meta': {'object_name': 'Component'}, + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'base_url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}) + }, + 'im.emailchange': { + 'Meta': {'object_name': 'EmailChange'}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'requested_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'emailchanges'", 'unique': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.endpoint': { + 'Meta': {'object_name': 'Endpoint'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'service': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'endpoints'", 'to': "orm['im.Service']"}) + }, + 'im.endpointdata': { + 'Meta': {'unique_together': "(('endpoint', 'key'),)", 'object_name': 'EndpointData'}, + 'endpoint': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'data'", 'to': "orm['im.Endpoint']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '1024'}) + }, + 'im.invitation': { + 'Meta': {'object_name': 'Invitation'}, + 'code': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'consumed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inviter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invitations_sent'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'is_consumed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'realname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.pendingthirdpartyuser': { + 'Meta': {'unique_together': "(('provider', 'third_party_identifier'),)", 'object_name': 'PendingThirdPartyUser'}, + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'info': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'third_party_identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'token': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'im.project': { + 'Meta': {'object_name': 'Project'}, + 'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'end_date': ('django.db.models.fields.DateTimeField', [], {}), + 'homepage': ('django.db.models.fields.URLField', [], {'max_length': '255'}), + 'id': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_column': "'id'"}), + 'is_base': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_application': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'last_of_project'", 'null': 'True', 'to': "orm['im.ProjectApplication']"}), + 'limit_on_members_number': ('django.db.models.fields.BigIntegerField', [], {}), + 'member_join_policy': ('django.db.models.fields.IntegerField', [], {}), + 'member_leave_policy': ('django.db.models.fields.IntegerField', [], {}), + 'members': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.AstakosUser']", 'through': "orm['im.ProjectMembership']", 'symmetrical': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True', 'null': 'True', 'db_index': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projs_owned'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'private': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'realname': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'resource_grants': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.ProjectResourceQuota']", 'blank': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.projectapplication': { + 'Meta': {'unique_together': "(('chain', 'id'),)", 'object_name': 'ProjectApplication'}, + 'applicant': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_applied'", 'to': "orm['im.AstakosUser']"}), + 'chain': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'chained_apps'", 'db_column': "'chain'", 'to': "orm['im.Project']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'end_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'homepage': ('django.db.models.fields.URLField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'issue_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'limit_on_members_number': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'member_join_policy': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'member_leave_policy': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'null': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_owned'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'private': ('django.db.models.fields.NullBooleanField', [], {'default': 'False', 'null': 'True', 'blank': 'True'}), + 'resource_grants': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.ProjectResourceGrant']", 'blank': 'True'}), + 'response': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'response_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'responded_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'response_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'start_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}), + 'waive_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'waived_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'waive_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'waive_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.projectlock': { + 'Meta': {'object_name': 'ProjectLock'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.projectlog': { + 'Meta': {'object_name': 'ProjectLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.Project']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectmembership': { + 'Meta': {'unique_together': "(('person', 'project'),)", 'object_name': 'ProjectMembership'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'initialized': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Project']"}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}) + }, + 'im.projectmembershiplog': { + 'Meta': {'object_name': 'ProjectMembershipLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'membership': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.ProjectMembership']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectresourcegrant': { + 'Meta': {'unique_together': "(('resource', 'project_application'),)", 'object_name': 'ProjectResourceGrant'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'member_capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'project_application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.ProjectApplication']"}), + 'project_capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}) + }, + 'im.projectresourcequota': { + 'Meta': {'unique_together': "(('resource', 'project'),)", 'object_name': 'ProjectResourceQuota'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'member_capacity': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Project']"}), + 'project_capacity': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}) + }, + 'im.resource': { + 'Meta': {'object_name': 'Resource'}, + 'api_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'desc': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'project_default': ('django.db.models.fields.BigIntegerField', [], {}), + 'service_origin': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'service_type': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'ui_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'unit': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'uplimit': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) + }, + 'im.service': { + 'Meta': {'object_name': 'Service'}, + 'component': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Component']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.sessioncatalog': { + 'Meta': {'object_name': 'SessionCatalog'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.usersetting': { + 'Meta': {'unique_together': "(('user', 'setting'),)", 'object_name': 'UserSetting'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'setting': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'value': ('django.db.models.fields.IntegerField', [], {}) + } + } + + complete_apps = ['im'] diff --git a/snf-astakos-app/astakos/im/migrations/0081_auto__add_field_astakosuserauthprovider_last_login_at.py b/snf-astakos-app/astakos/im/migrations/0081_auto__add_field_astakosuserauthprovider_last_login_at.py new file mode 100644 index 0000000000000000000000000000000000000000..e25a6469a102eb5558da6459b01eeb1f37c71c60 --- /dev/null +++ b/snf-astakos-app/astakos/im/migrations/0081_auto__add_field_astakosuserauthprovider_last_login_at.py @@ -0,0 +1,333 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'AstakosUserAuthProvider.last_login_at' + db.add_column('im_astakosuserauthprovider', 'last_login_at', + self.gf('django.db.models.fields.DateTimeField')(default=None, null=True), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'AstakosUserAuthProvider.last_login_at' + db.delete_column('im_astakosuserauthprovider', 'last_login_at') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'im.additionalmail': { + 'Meta': {'object_name': 'AdditionalMail'}, + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.approvalterms': { + 'Meta': {'object_name': 'ApprovalTerms'}, + 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.astakosuser': { + 'Meta': {'object_name': 'AstakosUser', '_ormbases': ['auth.User']}, + 'accepted_email': ('django.db.models.fields.EmailField', [], {'default': 'None', 'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'accepted_policy': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'activation_sent': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'base_project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'base_user'", 'null': 'True', 'to': "orm['im.Project']"}), + 'date_signed_terms': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_reason': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}), + 'disturbed_quota': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'email_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_credits': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_signed_terms': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'invitations': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'is_rejected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'level': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'moderated_at': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'moderated_data': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.AstakosUserQuota']", 'symmetrical': 'False'}), + 'rejected_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {}), + 'user_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'primary_key': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'verification_code': ('django.db.models.fields.CharField', [], {'max_length': '255', 'unique': 'True', 'null': 'True'}), + 'verified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.astakosuserauthprovider': { + 'Meta': {'ordering': "('module', 'created')", 'unique_together': "(('identifier', 'module', 'user'),)", 'object_name': 'AstakosUserAuthProvider'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_backend': ('django.db.models.fields.CharField', [], {'default': "'astakos'", 'max_length': '255'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'info_data': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'last_login_at': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'module': ('django.db.models.fields.CharField', [], {'default': "'local'", 'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'auth_providers'", 'to': "orm['im.AstakosUser']"}) + }, + 'im.astakosuserquota': { + 'Meta': {'unique_together': "(('resource', 'user'),)", 'object_name': 'AstakosUserQuota'}, + 'capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.authproviderpolicyprofile': { + 'Meta': {'ordering': "['priority']", 'object_name': 'AuthProviderPolicyProfile'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['auth.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_exclusive': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'policy_add': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_automoderate': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_create': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_limit': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}), + 'policy_login': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_remove': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_required': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_switch': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'priority': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['im.AstakosUser']"}) + }, + 'im.chain': { + 'Meta': {'object_name': 'Chain'}, + 'chain': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.component': { + 'Meta': {'object_name': 'Component'}, + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'base_url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}) + }, + 'im.emailchange': { + 'Meta': {'object_name': 'EmailChange'}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'requested_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'emailchanges'", 'unique': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.endpoint': { + 'Meta': {'object_name': 'Endpoint'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'service': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'endpoints'", 'to': "orm['im.Service']"}) + }, + 'im.endpointdata': { + 'Meta': {'unique_together': "(('endpoint', 'key'),)", 'object_name': 'EndpointData'}, + 'endpoint': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'data'", 'to': "orm['im.Endpoint']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '1024'}) + }, + 'im.invitation': { + 'Meta': {'object_name': 'Invitation'}, + 'code': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'consumed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inviter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invitations_sent'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'is_consumed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'realname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.pendingthirdpartyuser': { + 'Meta': {'unique_together': "(('provider', 'third_party_identifier'),)", 'object_name': 'PendingThirdPartyUser'}, + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'info': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'third_party_identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'token': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'im.project': { + 'Meta': {'object_name': 'Project'}, + 'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'end_date': ('django.db.models.fields.DateTimeField', [], {}), + 'homepage': ('django.db.models.fields.URLField', [], {'max_length': '255'}), + 'id': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_column': "'id'"}), + 'is_base': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_application': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'last_of_project'", 'null': 'True', 'to': "orm['im.ProjectApplication']"}), + 'limit_on_members_number': ('django.db.models.fields.BigIntegerField', [], {}), + 'member_join_policy': ('django.db.models.fields.IntegerField', [], {}), + 'member_leave_policy': ('django.db.models.fields.IntegerField', [], {}), + 'members': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.AstakosUser']", 'through': "orm['im.ProjectMembership']", 'symmetrical': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True', 'null': 'True', 'db_index': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projs_owned'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'private': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'realname': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'resource_grants': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.ProjectResourceQuota']", 'blank': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.projectapplication': { + 'Meta': {'unique_together': "(('chain', 'id'),)", 'object_name': 'ProjectApplication'}, + 'applicant': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_applied'", 'to': "orm['im.AstakosUser']"}), + 'chain': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'chained_apps'", 'db_column': "'chain'", 'to': "orm['im.Project']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'end_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'homepage': ('django.db.models.fields.URLField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'issue_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'limit_on_members_number': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'member_join_policy': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'member_leave_policy': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'null': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_owned'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'private': ('django.db.models.fields.NullBooleanField', [], {'default': 'False', 'null': 'True', 'blank': 'True'}), + 'resource_grants': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.ProjectResourceGrant']", 'blank': 'True'}), + 'response': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'response_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'responded_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'response_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'start_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}), + 'waive_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'waived_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'waive_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'waive_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.projectlock': { + 'Meta': {'object_name': 'ProjectLock'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.projectlog': { + 'Meta': {'object_name': 'ProjectLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.Project']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectmembership': { + 'Meta': {'unique_together': "(('person', 'project'),)", 'object_name': 'ProjectMembership'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'initialized': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Project']"}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}) + }, + 'im.projectmembershiplog': { + 'Meta': {'object_name': 'ProjectMembershipLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'membership': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.ProjectMembership']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectresourcegrant': { + 'Meta': {'unique_together': "(('resource', 'project_application'),)", 'object_name': 'ProjectResourceGrant'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'member_capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'project_application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.ProjectApplication']"}), + 'project_capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}) + }, + 'im.projectresourcequota': { + 'Meta': {'unique_together': "(('resource', 'project'),)", 'object_name': 'ProjectResourceQuota'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'member_capacity': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Project']"}), + 'project_capacity': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}) + }, + 'im.resource': { + 'Meta': {'object_name': 'Resource'}, + 'api_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'desc': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'project_default': ('django.db.models.fields.BigIntegerField', [], {}), + 'service_origin': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'service_type': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'ui_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'unit': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'uplimit': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) + }, + 'im.service': { + 'Meta': {'object_name': 'Service'}, + 'component': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Component']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.sessioncatalog': { + 'Meta': {'object_name': 'SessionCatalog'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.usersetting': { + 'Meta': {'unique_together': "(('user', 'setting'),)", 'object_name': 'UserSetting'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'setting': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'value': ('django.db.models.fields.IntegerField', [], {}) + } + } + + complete_apps = ['im'] diff --git a/snf-astakos-app/astakos/im/models.py b/snf-astakos-app/astakos/im/models.py index f200f951ce8e615b4d73f935be64c3511f8a02e4..2078eb1fef21ad39cfd28eec305971ac67b6016c 100644 --- a/snf-astakos-app/astakos/im/models.py +++ b/snf-astakos-app/astakos/im/models.py @@ -1,35 +1,17 @@ -# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import uuid import logging @@ -42,7 +24,8 @@ from urllib import quote from random import randint import os -from django.db import models, transaction +from django.db import models +from astakos.im import transaction from django.contrib.auth.models import User, UserManager, Group, Permission from django.utils.translation import ugettext as _ from django.db.models.signals import pre_save, post_save @@ -64,7 +47,6 @@ from astakos.im import auth_providers as auth import astakos.im.messages as astakos_messages from synnefo.lib.ordereddict import OrderedDict -from synnefo.util.text import uenc, udec from synnefo.util import units from astakos.im import presentation @@ -73,6 +55,9 @@ logger = logging.getLogger(__name__) DEFAULT_CONTENT_TYPE = None _content_type = None +SYSTEM_PROJECT_NAME_TPL = getattr(astakos_settings, "SYSTEM_PROJECT_NAME_TPL", + u"[system] %s") + def get_content_type(): global _content_type @@ -149,7 +134,7 @@ class Component(models.Model): msg = 'Token renewed for component %s' logger.log(astakos_settings.LOGGING_LEVEL, msg, self.name) - def __str__(self): + def __unicode__(self): return self.name @classmethod @@ -233,14 +218,15 @@ class Resource(models.Model): service_origin = models.CharField(max_length=255, db_index=True) unit = models.CharField(_('Unit'), null=True, max_length=255) uplimit = models.BigIntegerField(default=0) + project_default = models.BigIntegerField() ui_visible = models.BooleanField(default=True) api_visible = models.BooleanField(default=True) - def __str__(self): + def __unicode__(self): return self.name def full_name(self): - return str(self) + return unicode(self) def get_info(self): return {'service': self.service_origin, @@ -253,34 +239,41 @@ class Resource(models.Model): @property def group(self): default = self.name - return get_presentation(str(self)).get('group', default) + return get_presentation(unicode(self)).get('group', default) @property def help_text(self): default = "%s resource" % self.name - return get_presentation(str(self)).get('help_text', default) + return get_presentation(unicode(self)).get('help_text', default) @property def help_text_input_each(self): default = "%s resource" % self.name - return get_presentation(str(self)).get('help_text_input_each', default) + return get_presentation(unicode(self)).get( + 'help_text_input_each', default) + + @property + def help_text_input_total(self): + default = "%s resource" % self.name + key = 'help_text_input_total' + return get_presentation(str(self)).get(key, default) @property def is_abbreviation(self): - return get_presentation(str(self)).get('is_abbreviation', False) + return get_presentation(unicode(self)).get('is_abbreviation', False) @property def report_desc(self): default = "%s resource" % self.name - return get_presentation(str(self)).get('report_desc', default) + return get_presentation(unicode(self)).get('report_desc', default) @property def placeholder(self): - return get_presentation(str(self)).get('placeholder', self.unit) + return get_presentation(unicode(self)).get('placeholder', self.unit) @property def verbose_name(self): - return get_presentation(str(self)).get('verbose_name', self.name) + return get_presentation(unicode(self)).get('verbose_name', self.name) @property def display_name(self): @@ -405,30 +398,34 @@ class AstakosUser(User): auth_token_expires = models.DateTimeField( _('Token expiration date'), null=True) - updated = models.DateTimeField(_('Update date')) + updated = models.DateTimeField(_('Last update date')) # Arbitrary text to identify the reason user got deactivated. # To be used as a reference from administrators. deactivated_reason = models.TextField( - _('Reason the user was disabled for'), + _('Reason for user deactivation'), default=None, null=True) - deactivated_at = models.DateTimeField(_('User deactivated at'), null=True, + deactivated_at = models.DateTimeField(_('User deactivation date'), + null=True, blank=True) - has_credits = models.BooleanField(_('Has credits?'), default=False) + has_credits = models.BooleanField(_('User has credits'), default=False) # this is set to True when user profile gets updated for the first time - is_verified = models.BooleanField(_('Is verified?'), default=False) + is_verified = models.BooleanField(_('User is verified'), default=False) # user email is verified - email_verified = models.BooleanField(_('Email verified?'), default=False) + email_verified = models.BooleanField(_('User email is verified'), + default=False) # unique string used in user email verification url - verification_code = models.CharField(max_length=255, null=True, - blank=False, unique=True) + verification_code = models.CharField( + _('String used for email verification'), + max_length=255, null=True, + blank=False, unique=True) # date user email verified - verified_at = models.DateTimeField(_('User verified email at'), null=True, + verified_at = models.DateTimeField(_('User verification date'), null=True, blank=True) # email verification notice was sent to the user at this time @@ -436,13 +433,14 @@ class AstakosUser(User): null=True, blank=True) # user got rejected during moderation process - is_rejected = models.BooleanField(_('Account rejected'), + is_rejected = models.BooleanField(_('Account is rejected'), default=False) # reason user got rejected - rejected_reason = models.TextField(_('User rejected reason'), null=True, + rejected_reason = models.TextField(_('Reason for user rejection'), + null=True, blank=True) # moderation status - moderated = models.BooleanField(_('User moderated'), default=False) + moderated = models.BooleanField(_('Account is moderated'), default=False) # date user moderated (either accepted or rejected) moderated_at = models.DateTimeField(_('Date moderated'), default=None, blank=True, null=True) @@ -454,12 +452,13 @@ class AstakosUser(User): # the email used to accept the user accepted_email = models.EmailField(null=True, default=None, blank=True) - has_signed_terms = models.BooleanField(_('I agree with the terms'), + has_signed_terms = models.BooleanField(_('False if needs to sign terms'), default=False) - date_signed_terms = models.DateTimeField(_('Signed terms date'), + date_signed_terms = models.DateTimeField(_('Date of terms signing'), null=True, blank=True) # permanent unique user identifier - uuid = models.CharField(max_length=255, null=False, blank=False, + uuid = models.CharField(_('Unique user identifier'), + max_length=255, null=False, blank=False, unique=True) policy = models.ManyToManyField( @@ -468,12 +467,21 @@ class AstakosUser(User): disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'), default=False, db_index=True) + # This could have been OneToOneField, but fails due to + # https://code.djangoproject.com/ticket/13781 (fixed in v1.6) + base_project = models.ForeignKey('Project', related_name="base_user", + null=True) + objects = AstakosUserManager() @property def realname(self): return '%s %s' % (self.first_name, self.last_name) + @property + def realname_with_email(self): + return '%s (%s)' % (self.realname, self.email) + @property def log_display(self): """ @@ -488,6 +496,11 @@ class AstakosUser(User): self.first_name = first self.last_name = last + def get_base_project(self): + assert self.base_project is not None, \ + "User %s has no system project" % self + return self.base_project + def add_permission(self, pname): if self.has_perm(pname): return @@ -511,7 +524,7 @@ class AstakosUser(User): def is_accepted(self): return self.moderated and not self.is_rejected - def is_project_admin(self, application_id=None): + def is_project_admin(self): return self.uuid in astakos_settings.PROJECT_ADMINS @property @@ -598,23 +611,22 @@ class AstakosUser(User): @property def status_display(self): - msg = "" - if self.is_active: - msg = "Accepted/Active" - if self.is_rejected: - msg = "Rejected" - if self.rejected_reason: - msg += " (%s)" % self.rejected_reason if not self.email_verified: msg = "Pending email verification" - if not self.moderated: + elif not self.moderated: msg = "Pending moderation" - if not self.is_active and self.email_verified: - msg = "Accepted/Inactive" - if self.deactivated_reason: - msg += " (%s)" % (self.deactivated_reason) - - if self.moderated and not self.is_rejected: + elif self.is_rejected: + msg = "Rejected" + if self.rejected_reason: + msg += " (%s)" % self.rejected_reason + # accepted + else: + if self.is_active: + msg = "Accepted/Active" + else: + msg = "Accepted/Inactive" + if self.deactivated_reason: + msg += " (%s)" % (self.deactivated_reason) if self.accepted_policy == 'manual': msg += " (manually accepted)" else: @@ -720,7 +732,7 @@ class AstakosUser(User): # URL methods @property def auth_providers_display(self): - return ",".join(["%s:%s" % (p.module, p.identifier) for p in + return ",".join(["%s:%s" % (p.module, p.identifier or '') for p in self.get_enabled_auth_providers()]) def add_auth_provider(self, module='local', identifier=None, **params): @@ -728,17 +740,20 @@ class AstakosUser(User): provider.add_to_user() def get_resend_activation_url(self): - return reverse('send_activation', kwargs={'user_id': self.pk}) + return reverse('send_activation', urlconf="synnefo.webproject.urls", + kwargs={'user_id': self.pk}) def get_activation_url(self, nxt=False): - url = "%s?auth=%s" % (reverse('astakos.im.views.activate'), - quote(self.verification_code)) + activate_url = reverse('astakos.im.views.activate', + urlconf="synnefo.webproject.urls") + url = "%s?auth=%s" % (activate_url, quote(self.verification_code)) if nxt: url += "&next=%s" % quote(nxt) return url def get_password_reset_url(self, token_generator=default_token_generator): return reverse('astakos.im.views.target.local.password_reset_confirm', + urlconf="synnefo.webproject.urls", kwargs={'uidb36': int_to_base36(self.id), 'token': token_generator.make_token(self)}) @@ -780,7 +795,7 @@ class AstakosUser(User): return application.owner == self def owns_project(self, project): - return project.application.owner == self + return project.owner == self def is_associated(self, project): try: @@ -822,6 +837,29 @@ class AstakosUser(User): offline_tokens) offline_tokens.delete() + def get_last_logins(self): + providers = self.auth_providers.filter().order_by('-last_login_at') + providers = providers.filter(last_login_at__isnull=False) + logins = [] + for provider in providers: + logins.append((provider.module, provider.last_login_at)) + + return logins + + @property + def last_login_info_display(self): + logins = self.get_last_logins() + display = [] + + if len(logins) == 0: + return "No login info available" + + for module, date in logins: + display.append("[%s] %s" % (module, date)) + + return ", ".join(display) + + class AstakosUserAuthProviderManager(models.Manager): @@ -956,6 +994,8 @@ class AstakosUserAuthProvider(models.Model): default='astakos') info_data = models.TextField(default="", null=True, blank=True) created = models.DateTimeField('Creation date', auto_now_add=True) + last_login_at = models.DateTimeField('Last login date', null=True, + default=None) objects = AstakosUserAuthProviderManager() @@ -1120,6 +1160,7 @@ class EmailChange(models.Model): def get_url(self): return reverse('email_change_confirm', + urlconf="synnefo.webproject.urls", kwargs={'activation_key': self.activation_key}) def activation_key_expired(self): @@ -1258,7 +1299,7 @@ class UserSetting(models.Model): class Chain(models.Model): chain = models.AutoField(primary_key=True) - def __str__(self): + def __unicode__(self): return "%s" % (self.chain,) @@ -1295,24 +1336,28 @@ class ProjectApplication(models.Model): DISMISSED = 4 CANCELLED = 5 + MAX_HOMEPAGE_LENGTH = 255 + MAX_NAME_LENGTH = 80 + state = models.IntegerField(default=PENDING, db_index=True) owner = models.ForeignKey( AstakosUser, related_name='projects_owned', + null=True, db_index=True) chain = models.ForeignKey('Project', related_name='chained_apps', db_column='chain') - name = models.CharField(max_length=80) - homepage = models.URLField(max_length=255, null=True, + name = models.CharField(max_length=MAX_NAME_LENGTH, null=True) + homepage = models.URLField(max_length=MAX_HOMEPAGE_LENGTH, null=True, verify_exists=False) description = models.TextField(null=True, blank=True) start_date = models.DateTimeField(null=True, blank=True) - end_date = models.DateTimeField() - member_join_policy = models.IntegerField() - member_leave_policy = models.IntegerField() - limit_on_members_number = models.PositiveIntegerField(null=True) + end_date = models.DateTimeField(null=True) + member_join_policy = models.IntegerField(null=True) + member_leave_policy = models.IntegerField(null=True) + limit_on_members_number = models.BigIntegerField(null=True) resource_grants = models.ManyToManyField( Resource, null=True, @@ -1328,6 +1373,7 @@ class ProjectApplication(models.Model): waive_reason = models.TextField(null=True, blank=True) waive_actor = models.ForeignKey(AstakosUser, null=True, related_name='waived_apps') + private = models.NullBooleanField(default=False) objects = ProjectApplicationManager() @@ -1361,20 +1407,15 @@ class ProjectApplication(models.Model): return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown')) @property - def grants(self): - return self.projectresourcegrant_set.values('member_capacity', - 'resource__name') + def resource_set(self): + return self.projectresourcegrant_set.order_by('resource__name') @property def resource_policies(self): - return [str(rp) for rp in self.projectresourcegrant_set.all()] + return [unicode(rp) for rp in self.projectresourcegrant_set.all()] def is_modification(self): - # if self.state != self.PENDING: - # return False - parents = self.chained_applications().filter(id__lt=self.id) - parents = parents.filter(state__in=[self.APPROVED]) - return parents.count() > 0 + return self.chain.is_initialized() def chained_applications(self): return ProjectApplication.objects.filter(chain=self.chain) @@ -1477,10 +1518,9 @@ class ProjectResourceGrantManager(models.Manager): class ProjectResourceGrant(models.Model): resource = models.ForeignKey(Resource) - project_application = models.ForeignKey(ProjectApplication, - null=True) - project_capacity = models.BigIntegerField(null=True) - member_capacity = models.BigIntegerField(default=0) + project_application = models.ForeignKey(ProjectApplication) + project_capacity = models.BigIntegerField() + member_capacity = models.BigIntegerField() objects = ProjectResourceGrantManager() @@ -1488,47 +1528,83 @@ class ProjectResourceGrant(models.Model): unique_together = ("resource", "project_application") def display_member_capacity(self): - return units.show(self.member_capacity, self.resource.unit) + return units.show(self.member_capacity, self.resource.unit, + inf="Unlimited") - def __str__(self): - return 'Max %s per user: %s' % (self.resource.pluralized_display_name, - self.display_member_capacity()) + def display_project_capacity(self): + return units.show(self.project_capacity, self.resource.unit, + inf="Unlimited") + def project_diffs(self): + project = self.project_application.chain + try: + project_resource = project.resource_set.get(resource=self.resource) + except ProjectResourceQuota.DoesNotExist: + return [self.project_capacity, self.member_capacity] + + project_diff = \ + self.project_capacity - project_resource.project_capacity + if self.project_capacity == units.PRACTICALLY_INFINITE: + project_diff = units.PRACTICALLY_INFINITE + if project_resource.project_capacity == units.PRACTICALLY_INFINITE: + project_diff = -units.PRACTICALLY_INFINITE + + member_diff = self.member_capacity - project_resource.member_capacity + if self.member_capacity == units.PRACTICALLY_INFINITE: + member_diff = units.PRACTICALLY_INFINITE + if project_resource.member_capacity == units.PRACTICALLY_INFINITE: + member_diff = -units.PRACTICALLY_INFINITE + + return [project_diff, member_diff] + + def display_project_diff(self): + proj, member = self.project_diffs() + proj_abs, member_abs = proj, member + unit = self.resource.unit + + def disp(v, disp_func=None): + if not disp_func: + disp_func = lambda : '' + + if v == 0: + return '' + sign = u'+' if v >= 0 else u'-' + ext = units.show(abs(v), unit, inf="Unlimited") + if ext == "Unlimited" and sign == u'+': + disp = disp_func() + if disp: + ext = "from %s" % disp + else: + disp = disp_func() + ext = sign + "" + ext + return unicode(ext) -def _distinct(f, l): - d = {} - last = None - for x in l: - group = f(x) - if group == last: - continue - last = group - d[group] = x - return d + project_resource = None + try: + project = self.project_application.chain + project_resource = project.resource_set.get(resource=self.resource) + except: + pass + memb_disp = project_resource.display_member_capacity if \ + project_resource else None + proj_disp = project_resource.display_project_capacity if \ + project_resource else None + return [disp(proj_abs, proj_disp), + disp(member_abs, memb_disp)] -def invert_dict(d): - return dict((v, k) for k, v in d.iteritems()) + def __unicode__(self): + return 'Max %s per member: %s; project total: %s' % ( + self.resource.pluralized_display_name, + self.display_member_capacity(), + self.display_project_capacity()) class ProjectManager(models.Manager): - - def all_with_pending(self, flt=None): - flt = Q() if flt is None else flt - projects = list(self.select_related( - 'application', 'application__owner').filter(flt)) - - objs = ProjectApplication.objects.select_related('owner') - apps = objs.filter(state=ProjectApplication.PENDING, - chain__in=projects).order_by('chain', '-id') - app_d = _distinct(lambda app: app.chain_id, apps) - return [(project, app_d.get(project.pk)) for project in projects] - def expired_projects(self): model = self.model - q = ((model.o_state_q(model.O_ACTIVE) | - model.o_state_q(model.O_SUSPENDED)) & - Q(application__end_date__lt=datetime.now())) + q = (Q(state__in=[model.NORMAL, model.SUSPENDED]) & + Q(end_date__lt=datetime.now())) return self.filter(q) def user_accessible_projects(self, user): @@ -1541,14 +1617,13 @@ class ProjectManager(models.Manager): else: membs = user.projectmembership_set.associated() memb_projects = membs.values_list("project", flat=True) - flt = (Q(application__owner=user) | - Q(application__applicant=user) | + flt = (Q(owner=user) | + Q(last_application__applicant=user) | Q(id__in=memb_projects)) - relevant = model.o_states_q(model.RELEVANT_STATES) + relevant = ~Q(state=model.DELETED) return self.filter(flt, relevant).order_by( - 'application__issue_date').select_related( - 'application', 'application__owner', 'application__applicant') + 'creation_date').select_related('last_application', 'owner') def search_by_name(self, *search_strings): q = Q() @@ -1556,14 +1631,24 @@ class ProjectManager(models.Manager): q = q | Q(name__icontains=s) return self.filter(q) + def initialized(self, flt=None): + q = Q(state__in=self.model.INITIALIZED_STATES) + if flt is not None: + q &= flt + return self.filter(q) + + @property + def has_infinite_members_limit(self): + return self.limit_on_members_number == units.PRACTICALLY_INFINITE + + class Project(models.Model): id = models.BigIntegerField(db_column='id', primary_key=True) - application = models.OneToOneField( - ProjectApplication, - related_name='project') + last_application = models.ForeignKey(ProjectApplication, null=True, + related_name='last_of_project') members = models.ManyToManyField( AstakosUser, @@ -1571,40 +1656,76 @@ class Project(models.Model): creation_date = models.DateTimeField(auto_now_add=True) name = models.CharField( - max_length=80, + max_length=ProjectApplication.MAX_NAME_LENGTH, null=True, db_index=True, unique=True) + UNINITIALIZED = 0 NORMAL = 1 SUSPENDED = 10 TERMINATED = 100 + DELETED = 1000 + + INITIALIZED_STATES = [NORMAL, + SUSPENDED, + TERMINATED, + ] + + ALIVE_STATES = [NORMAL, + SUSPENDED, + ] + + SKIP_STATES = [DELETED, + TERMINATED, + ] DEACTIVATED_STATES = [SUSPENDED, TERMINATED] - state = models.IntegerField(default=NORMAL, + state = models.IntegerField(default=UNINITIALIZED, db_index=True) + uuid = models.CharField(max_length=255, unique=True) - objects = ProjectManager() - - def __str__(self): - return uenc(_("<project %s '%s'>") % - (self.id, udec(self.application.name))) + owner = models.ForeignKey( + AstakosUser, + related_name='projs_owned', + null=True, + db_index=True) + realname = models.CharField(max_length=ProjectApplication.MAX_NAME_LENGTH) + homepage = models.URLField( + max_length=ProjectApplication.MAX_HOMEPAGE_LENGTH, + verify_exists=False) + description = models.TextField(blank=True) + end_date = models.DateTimeField() + member_join_policy = models.IntegerField() + member_leave_policy = models.IntegerField() + limit_on_members_number = models.BigIntegerField() + resource_grants = models.ManyToManyField( + Resource, + null=True, + blank=True, + through='ProjectResourceQuota') + private = models.BooleanField(default=False) + is_base = models.BooleanField(default=False) - __repr__ = __str__ + objects = ProjectManager() def __unicode__(self): - return _("<project %s '%s'>") % (self.id, self.application.name) + return _("<project %s '%s'>") % (self.id, self.realname) + O_UNINITIALIZED = -1 O_PENDING = 0 O_ACTIVE = 1 + O_ACTIVE_PENDING = 2 O_DENIED = 3 O_DISMISSED = 4 O_CANCELLED = 5 O_SUSPENDED = 10 O_TERMINATED = 100 + O_DELETED = 1000 O_STATE_DISPLAY = { + O_UNINITIALIZED: _("Uninitialized"), O_PENDING: _("Pending"), O_ACTIVE: _("Active"), O_DENIED: _("Denied"), @@ -1612,72 +1733,87 @@ class Project(models.Model): O_CANCELLED: _("Cancelled"), O_SUSPENDED: _("Suspended"), O_TERMINATED: _("Terminated"), + O_DELETED: _("Deleted"), } + O_STATE_UNINITIALIZED = { + None: O_UNINITIALIZED, + ProjectApplication.PENDING: O_PENDING, + ProjectApplication.DENIED: O_DENIED, + } + O_STATE_DELETED = { + None: O_DELETED, + ProjectApplication.DISMISSED: O_DISMISSED, + ProjectApplication.CANCELLED: O_CANCELLED, + } + OVERALL_STATE = { - (NORMAL, ProjectApplication.PENDING): O_PENDING, - (NORMAL, ProjectApplication.APPROVED): O_ACTIVE, - (NORMAL, ProjectApplication.DENIED): O_DENIED, - (NORMAL, ProjectApplication.DISMISSED): O_DISMISSED, - (NORMAL, ProjectApplication.CANCELLED): O_CANCELLED, - (SUSPENDED, ProjectApplication.APPROVED): O_SUSPENDED, - (TERMINATED, ProjectApplication.APPROVED): O_TERMINATED, - } + NORMAL: lambda app_state: Project.O_ACTIVE, + UNINITIALIZED: lambda app_state: Project.O_STATE_UNINITIALIZED.get( + app_state, None), + DELETED: lambda app_state: Project.O_STATE_DELETED.get( + app_state, None), + SUSPENDED: lambda app_state: Project.O_SUSPENDED, + TERMINATED: lambda app_state: Project.O_TERMINATED, + } - OVERALL_STATE_INV = invert_dict(OVERALL_STATE) + def display_name_for_user(self, user): + if not self.is_base: + return self.realname - @classmethod - def o_state_q(cls, o_state): - p_state, a_state = cls.OVERALL_STATE_INV[o_state] - return Q(state=p_state, application__state=a_state) + if user.uuid == self.realname.replace("system:", ""): + return "System project" - @classmethod - def o_states_q(cls, o_states): - return reduce(lambda x, y: x | y, map(cls.o_state_q, o_states), Q()) + if user.is_project_admin(): + return "[system] %s" % (self.display_name(email=True), ) - INITIALIZED_STATES = [O_ACTIVE, - O_SUSPENDED, - O_TERMINATED, - ] + return self.display_name - RELEVANT_STATES = [O_PENDING, - O_DENIED, - O_ACTIVE, - O_SUSPENDED, - O_TERMINATED, - ] + def display_name(self, email=False): + if self.is_base: + uuid = self.realname.replace("system:", "") + try: + user = AstakosUser.objects.get(uuid=uuid) + if email: + username = "%s %s" % (user.email, user.realname) + else: + username = user.realname + except AstakosUser.DoesNotExist: + username = uuid - SKIP_STATES = [O_DISMISSED, - O_CANCELLED, - O_TERMINATED, - ] + return username + return self.realname @classmethod def _overall_state(cls, project_state, app_state): - return cls.OVERALL_STATE.get((project_state, app_state), None) + os = cls.OVERALL_STATE.get(project_state, None) + if os is None: + return None + return os(app_state) def overall_state(self): - return self._overall_state(self.state, self.application.state) + app_state = (self.last_application.state + if self.last_application else None) + return self._overall_state(self.state, app_state) def last_pending_application(self): - apps = self.chained_apps.filter( - state=ProjectApplication.PENDING).order_by('-id') - if apps: - return apps[0] + app = self.last_application + if app and app.state == ProjectApplication.PENDING: + return app return None def last_pending_modification(self): last_pending = self.last_pending_application() - if last_pending == self.application: - return None - return last_pending + if self.state != Project.UNINITIALIZED: + return last_pending + return None def state_display(self): return self.O_STATE_DISPLAY.get(self.overall_state(), _('Unknown')) def expiration_info(self): - return (str(self.id), self.name, self.state_display(), - str(self.application.end_date)) + return (unicode(self.id), self.name, self.state_display(), + unicode(self.end_date)) def last_deactivation(self): objs = self.log.filter(to_state__in=self.DEACTIVATED_STATES) @@ -1693,10 +1829,10 @@ class Project(models.Model): return self.state != self.NORMAL def is_active(self): - return self.overall_state() == self.O_ACTIVE + return self.state == self.NORMAL def is_initialized(self): - return self.overall_state() in self.INITIALIZED_STATES + return self.state in self.INITIALIZED_STATES ### Deactivation calls @@ -1723,14 +1859,28 @@ class Project(models.Model): def resume(self, actor=None, reason=None): self.set_state(self.NORMAL, actor=actor, reason=reason) if self.name is None: - self.name = self.application.name + self.name = self.realname self.save() - ### Logical checks + def activate(self, actor=None, reason=None): + assert self.state != self.DELETED, \ + "cannot activate: %s is deleted" % self + if self.state != self.NORMAL: + self.set_state(self.NORMAL, actor=actor, reason=reason) + if self.name != self.realname: + self.name = self.realname + self.save() + + def set_deleted(self, actor=None, reason=None): + self.set_state(self.DELETED, actor=actor, reason=reason) + def can_modify(self): + return self.state not in [self.UNINITIALIZED, self.DELETED] + + ### Logical checks @property def is_alive(self): - return self.overall_state() in [self.O_ACTIVE, self.O_SUSPENDED] + return self.state in [self.NORMAL, self.SUSPENDED] @property def is_terminated(self): @@ -1741,10 +1891,7 @@ class Project(models.Model): return self.is_deactivated(self.SUSPENDED) def violates_members_limit(self, adding=0): - application = self.application - limit = application.limit_on_members_number - if limit is None: - return False + limit = self.limit_on_members_number return (len(self.approved_members) + adding > limit) ### Other @@ -1764,6 +1911,59 @@ class Project(models.Model): def approved_members(self): return [m.person for m in self.approved_memberships] + @property + def member_join_policy_display(self): + policy = self.member_join_policy + return presentation.PROJECT_MEMBER_JOIN_POLICIES.get(policy) + + @property + def member_leave_policy_display(self): + policy = self.member_leave_policy + return presentation.PROJECT_MEMBER_LEAVE_POLICIES.get(policy) + + @property + def has_infinite_members_limit(self): + return self.limit_on_members_number == units.PRACTICALLY_INFINITE + + @property + def resource_set(self): + return self.projectresourcequota_set.order_by('resource__name') + + +def create_project(**kwargs): + if "uuid" not in kwargs: + kwargs["uuid"] = str(uuid.uuid4()) + return Project.objects.create(**kwargs) + + +class ProjectResourceQuotaManager(models.Manager): + def quotas_per_project(self, projects): + proj_ids = [proj.id for proj in projects] + quotas = self.filter( + project__in=proj_ids).select_related("resource") + return _partition_by(lambda g: g.project_id, quotas) + + +class ProjectResourceQuota(models.Model): + + resource = models.ForeignKey(Resource) + project = models.ForeignKey(Project) + project_capacity = models.BigIntegerField(default=0) + member_capacity = models.BigIntegerField(default=0) + + objects = ProjectResourceQuotaManager() + + class Meta: + unique_together = ("resource", "project") + + def display_member_capacity(self): + return units.show(self.member_capacity, self.resource.unit, + inf="Unlimited") + + def display_project_capacity(self): + return units.show(self.project_capacity, self.resource.unit, + inf="Unlimited") + class ProjectLogManager(models.Manager): def last_deactivations(self, projects): @@ -1795,8 +1995,21 @@ class ProjectMembershipManager(models.Manager): q = self.model.Q_ACCEPTED_STATES return self.filter(q) - def actually_accepted(self): + def actually_accepted(self, projects=None): q = self.model.Q_ACTUALLY_ACCEPTED + if projects is not None: + q &= Q(project__in=projects) + return self.filter(q) + + def actually_accepted_and_active(self): + q = self.model.Q_ACTUALLY_ACCEPTED + q &= Q(project__state=Project.NORMAL) + return self.filter(q) + + def initialized(self, projects=None): + q = Q(initialized=True) + if projects is not None: + q &= Q(project__in=projects) return self.filter(q) def requested(self): @@ -1857,6 +2070,7 @@ class ProjectMembership(models.Model): state = models.IntegerField(default=REQUESTED, db_index=True) + initialized = models.BooleanField(default=False) objects = ProjectMembershipManager() # Compiled queries @@ -1878,8 +2092,8 @@ class ProjectMembership(models.Model): ACCEPTED: _('Accepted member'), LEAVE_REQUESTED: _('Requested to leave'), USER_SUSPENDED: _('Suspended member'), - REJECTED: _('Request rejected'), - CANCELLED: _('Request cancelled'), + REJECTED: _('Join request rejected'), + CANCELLED: _('Join request cancelled'), REMOVED: _('Removed member'), } @@ -1893,11 +2107,9 @@ class ProjectMembership(models.Model): unique_together = ("person", "project") #index_together = [["project", "state"]] - def __str__(self): - return uenc(_("<'%s' membership in '%s'>") % - (self.person.username, self.project)) - - __repr__ = __str__ + def __unicode__(self): + return (_("<'%s' membership in '%s'>") % + (self.person.username, self.project)) def latest_log(self): logs = self.log.all() @@ -1918,6 +2130,10 @@ class ProjectMembership(models.Model): self.state = to_state self.save() + def is_active(self): + return (self.project.state == Project.NORMAL and + self.state in self.ACTUALLY_ACCEPTED) + ACTION_CHECKS = { "join": lambda m: m.state not in m.ASSOCIATED_STATES, "accept": lambda m: m.state == m.REQUESTED, @@ -1959,6 +2175,8 @@ class ProjectMembership(models.Model): s = self.ACTION_STATES[action] except KeyError: raise ValueError("No such action '%s'" % action) + if action == "accept": + self.initialized = True return self.set_state(s, actor=actor, reason=reason) diff --git a/snf-astakos-app/astakos/im/notifications.py b/snf-astakos-app/astakos/im/notifications.py index bc3aea6f81270254a8306b304c072aaf0e00ba43..1664a7b153516719666fb847f6e4eaeb7ee0df22 100644 --- a/snf-astakos-app/astakos/im/notifications.py +++ b/snf-astakos-app/astakos/im/notifications.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import logging import socket diff --git a/snf-astakos-app/astakos/im/presentation.py b/snf-astakos-app/astakos/im/presentation.py index 65f3a8c089eaf271da660633c9fcbeb3ef7e06c2..c236349e6e2b040753557d5ae75bfd06a35c0df6 100644 --- a/snf-astakos-app/astakos/im/presentation.py +++ b/snf-astakos-app/astakos/im/presentation.py @@ -1,35 +1,17 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from astakos.im import settings from synnefo.lib.utils import dict_merge @@ -38,7 +20,7 @@ RESOURCES = { 'groups': { 'compute': { 'help_text': ('Compute resources ' - '(amount of VMs, CPUs, RAM, System disk) '), + '(amount of VMs, CPUs, RAM, Hard disk) '), 'is_abbreviation': False, 'report_desc': '', 'verbose_name': 'compute', @@ -61,34 +43,46 @@ RESOURCES = { 'pithos.diskspace': { 'help_text': ('This is the space on Pithos for storing files ' 'and VM Images. '), - 'help_text_input_each': ('This is the total amount of space on ' + 'help_text_input_each': ('This is the maximum amount of space on ' 'Pithos that will be granted to each ' 'user of this Project '), + 'help_text_input_total': ('This is the total amount of space on ' + 'Pithos that will be granted for use ' + 'across all users of this Project '), 'is_abbreviation': False, - 'report_desc': 'Storage Space', + 'report_desc': 'File Storage Space', 'placeholder': 'eg. 10GB', - 'verbose_name': 'Storage Space', + 'verbose_name': 'File Storage Space', 'group': 'storage' }, 'cyclades.disk': { - 'help_text': ('This is the System Disk that the VMs have that ' + 'help_text': ('This is the Hard Disk that the VMs have that ' 'run the OS '), - 'help_text_input_each': ("This is the total amount of System Disk " + 'help_text_input_each': ("This is the maximum amount of System " + "Disk " "that will be granted to each user of " "this Project (this refers to the total " - "System Disk of all VMs, not each VM's " - "System Disk) "), + "Hard Disk of all VMs, not each VM's " + "Hard Disk)"), + 'help_text_input_total': ("This is the total amount of System " + "Disk that will be granted across all " + "users of this Project (this refers to " + "the total Hard Disk of all VMs, not " + "each VM's Hard Disk)"), 'is_abbreviation': False, - 'report_desc': 'System Disk', + 'report_desc': 'Hard Disk Storage', 'placeholder': 'eg. 5GB, 2GB etc', - 'verbose_name': 'System Disk', + 'verbose_name': 'Hard Disk Storage', 'group': 'compute' }, 'cyclades.total_ram': { 'help_text': 'RAM used by VMs ', - 'help_text_input_each': ('This is the total amount of RAM that ' + 'help_text_input_each': ('This is the maximum amount of RAM that ' 'will be granted to each user of this ' 'Project (on all VMs) '), + 'help_text_input_total': ('This is the total amount of RAM that ' + 'will be granted across all users of ' + 'this Project (on all VMs)'), 'is_abbreviation': True, 'report_desc': 'Total RAM', 'placeholder': 'eg. 4GB', @@ -98,9 +92,12 @@ RESOURCES = { }, 'cyclades.ram': { 'help_text': 'RAM used by active VMs ', - 'help_text_input_each': ('This is the total amount of RAM that ' + 'help_text_input_each': ('This is the maximum amount of RAM that ' 'will be granted to each user of this ' 'Project (on all active VMs) '), + 'help_text_input_total': ('This is the total amount of RAM that ' + 'will be granted across all users of ' + 'this Project (on all active VMs)'), 'is_abbreviation': False, 'report_desc': 'RAM', 'placeholder': 'eg. 4GB', @@ -110,9 +107,12 @@ RESOURCES = { }, 'cyclades.total_cpu': { 'help_text': 'CPUs used by VMs ', - 'help_text_input_each': ('This is the total number of CPUs that ' + 'help_text_input_each': ('This is the maximum number of CPUs that ' 'will be granted to each user of this ' - 'Project (on all VMs) '), + 'Project (on all VMs)'), + 'help_text_input_total': ('This is the total number of CPUs that ' + 'will be granted across all users of ' + 'this Project (on all VMs)'), 'is_abbreviation': True, 'report_desc': 'Total CPUs', 'placeholder': 'eg. 1', @@ -122,9 +122,12 @@ RESOURCES = { }, 'cyclades.cpu': { 'help_text': 'CPUs used by active VMs ', - 'help_text_input_each': ('This is the total number of CPUs that ' + 'help_text_input_each': ('This is the maximum number of CPUs that ' 'will be granted to each user of this ' 'Project (on all active VMs) '), + 'help_text_input_total': ('This is the total number of CPUs that ' + 'will be granted across all users ' + 'of this Project (on all active VMs) '), 'is_abbreviation': False, 'report_desc': 'CPUs', 'placeholder': 'eg. 1', @@ -135,9 +138,12 @@ RESOURCES = { 'cyclades.vm': { 'help_text': ('These are the VMs one can create on the ' 'Cyclades UI '), - 'help_text_input_each': ('This is the total number of VMs that ' + 'help_text_input_each': ('This is the maximum number of VMs that ' 'will be granted to each user of this ' 'Project '), + 'help_text_input_total': ('This is the total number of VMs that ' + 'will be granted across all users ' + 'of this Project in total'), 'is_abbreviation': True, 'report_desc': 'Virtual Machines', 'placeholder': 'eg. 2', @@ -148,9 +154,12 @@ RESOURCES = { 'cyclades.network.private': { 'help_text': ('These are the Private Networks one can create on ' 'the Cyclades UI. '), - 'help_text_input_each': ('This is the total number of Private ' + 'help_text_input_each': ('This is the maximum number of Private ' 'Networks that will be granted to each ' 'user of this Project '), + 'help_text_input_total': ('This is the total number of Private ' + 'Networks that will be granted across ' + 'all users of this Project'), 'is_abbreviation': False, 'report_desc': 'Private Networks', 'placeholder': 'eg. 1', @@ -161,9 +170,12 @@ RESOURCES = { 'cyclades.floating_ip': { 'help_text': ('These are the Public (Floating) IPs one can ' 'reserve on the Cyclades UI. '), - 'help_text_input_each': ('This is the total number of Public ' + 'help_text_input_each': ('This is the maximum number of Public ' '(Floating) IPs that will be granted to ' 'each user of this Project '), + 'help_text_input_total': ('This is the number of Public ' + '(Floating) IPs that will be granted ' + 'across all users of this Project'), 'is_abbreviation': False, 'report_desc': 'Public (Floating) IPs', 'placeholder': 'eg. 1', @@ -173,8 +185,11 @@ RESOURCES = { }, 'astakos.pending_app': { 'help_text': ('Pending project applications limit'), - 'help_text_input_each': ('Total pending project applications user ' - 'is allowed to create'), + 'help_text_input_each': ('Maximum pending project applications ' + 'user is allowed to create'), + 'help_text_input_total': ('Total pending project applications ' + ' project users are allowed to create ' + ' in total'), 'is_abbreviation': False, 'report_desc': 'Pending Project Applications', 'placeholder': 'eg. 2', @@ -278,3 +293,9 @@ PROJECT_MEMBER_LEAVE_POLICIES = { 2: 'owner accepts', 3: 'closed', } + +USAGE_TAG_MAP = { + 0: 'green', + 33: 'yellow', + 66: 'red' +} diff --git a/snf-astakos-app/astakos/im/project_notif.py b/snf-astakos-app/astakos/im/project_notif.py index 42ae05284a7d9ab14a07912b18b605427521297e..a2041343582c8b194a536a169266cdb31e8358bb 100644 --- a/snf-astakos-app/astakos/im/project_notif.py +++ b/snf-astakos-app/astakos/im/project_notif.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import logging from django.utils.translation import ugettext as _ @@ -50,7 +32,6 @@ MEM_ENROLL_NOTIF = { } SENDER = settings.SERVER_EMAIL -NOTIFY_RECIPIENTS = [e[1] for e in settings.MANAGERS + settings.HELPDESK] def membership_change_notify(project, user, action): @@ -79,119 +60,88 @@ def membership_enroll_notify(project, user): logger.error(e.message) -def membership_request_notify(project, requested_user): - try: - notification = build_notification( - SENDER, - [project.application.owner.email], - _(messages.PROJECT_MEMBERSHIP_REQUEST_SUBJECT) % project.__dict__, - template='im/projects/project_membership_request_notification.txt', - dictionary={'object': project, 'user': requested_user.email}) - notification.send() - except NotificationError, e: - logger.error(e.message) +MEMBERSHIP_REQUEST_DATA = { + "join": lambda p: ( + _(messages.PROJECT_MEMBERSHIP_REQUEST_SUBJECT) % p.__dict__, + "im/projects/project_membership_request_notification.txt"), + "leave": lambda p: ( + _(messages.PROJECT_MEMBERSHIP_LEAVE_REQUEST_SUBJECT) % p.__dict__, + "im/projects/project_membership_leave_request_notification.txt"), +} -def membership_leave_request_notify(project, requested_user): - template = 'im/projects/project_membership_leave_request_notification.txt' +def membership_request_notify(project, requested_user, action): + owner = project.owner + if owner is None: + return + subject, template = MEMBERSHIP_REQUEST_DATA[action](project) try: - notification = build_notification( - SENDER, - [project.application.owner.email], - _(messages.PROJECT_MEMBERSHIP_LEAVE_REQUEST_SUBJECT) % - project.__dict__, + build_notification( + SENDER, [owner.email], subject, template=template, - dictionary={'object': project, 'user': requested_user.email}) - notification.send() - except NotificationError, e: - logger.error(e.message) - - -def application_submit_notify(application): - try: - notification = build_notification( - SENDER, NOTIFY_RECIPIENTS, - _(messages.PROJECT_CREATION_SUBJECT) % application.__dict__, - template='im/projects/project_creation_notification.txt', - dictionary={'object': application}) - notification.send() - except NotificationError, e: - logger.error(e.message) - - -def application_deny_notify(application): - try: - notification = build_notification( - SENDER, - [application.owner.email], - _(messages.PROJECT_DENIED_SUBJECT) % application.__dict__, - template='im/projects/project_denial_notification.txt', - dictionary={'object': application}) - notification.send() - except NotificationError, e: - logger.error(e.message) - - -def application_approve_notify(application): - try: - notification = build_notification( - SENDER, - [application.owner.email], - _(messages.PROJECT_APPROVED_SUBJECT) % application.__dict__, - template='im/projects/project_approval_notification.txt', - dictionary={'object': application}) - notification.send() + dictionary={'object': project, 'user': requested_user.email} + ).send() except NotificationError, e: logger.error(e.message) -def project_termination_notify(project): - app = project.application - try: - build_notification( - SENDER, - [project.application.owner.email], - _(messages.PROJECT_TERMINATION_SUBJECT) % app.__dict__, - template='im/projects/project_termination_notification.txt', - dictionary={'object': project} - ).send() - except NotificationError, e: - logger.error(e.message) +APPLICATION_DATA = { + "submit_new": lambda a: ( + [e[1] for e in settings.PROJECT_CREATION_RECIPIENTS], + _(messages.PROJECT_CREATION_SUBJECT) % a.chain.realname, + "im/projects/project_creation_notification.txt"), + "submit_modification": lambda a: ( + [e[1] for e in settings.PROJECT_MODIFICATION_RECIPIENTS], + _(messages.PROJECT_MODIFICATION_SUBJECT) % a.chain.realname, + "im/projects/project_modification_notification.txt"), + "deny": lambda a: ( + [a.applicant.email], + _(messages.PROJECT_DENIED_SUBJECT) % a.chain.realname, + "im/projects/project_denial_notification.txt"), + "approve": lambda a: ( + [a.applicant.email], + _(messages.PROJECT_APPROVED_SUBJECT) % a.chain.realname, + "im/projects/project_approval_notification.txt"), +} -def project_suspension_notify(project): +def application_notify(application, action): + recipients, subject, template = APPLICATION_DATA[action](application) try: build_notification( - SENDER, - [project.application.owner.email], - _(messages.PROJECT_SUSPENSION_SUBJECT) % project.__dict__, - template='im/projects/project_suspension_notification.txt', - dictionary={'object': project} + SENDER, recipients, subject, + template=template, + dictionary={'object': application} ).send() except NotificationError, e: logger.error(e.message) -def project_unsuspension_notify(project): - try: - build_notification( - SENDER, - [project.application.owner.email], - _(messages.PROJECT_UNSUSPENSION_SUBJECT) % project.__dict__, - template='im/projects/project_unsuspension_notification.txt', - dictionary={'object': project} - ).send() - except NotificationError, e: - logger.error(e.message) +PROJECT_DATA = { + "terminate": lambda p: ( + _(messages.PROJECT_TERMINATION_SUBJECT) % p.realname, + "im/projects/project_termination_notification.txt"), + "reinstate": lambda p: ( + _(messages.PROJECT_REINSTATEMENT_SUBJECT) % p.realname, + "im/projects/project_reinstatement_notification.txt"), + "suspend": lambda p: ( + _(messages.PROJECT_SUSPENSION_SUBJECT) % p.realname, + "im/projects/project_suspension_notification.txt"), + "unsuspend": lambda p: ( + _(messages.PROJECT_UNSUSPENSION_SUBJECT) % p.realname, + "im/projects/project_unsuspension_notification.txt"), +} -def project_reinstatement_notify(project): +def project_notify(project, action): + owner = project.owner + if owner is None: + return + subject, template = PROJECT_DATA[action](project) try: build_notification( - SENDER, - [project.application.owner.email], - _(messages.PROJECT_REINSTATEMENT_SUBJECT) % project.__dict__, - template='im/projects/project_reinstatement_notification.txt', + SENDER, [owner.email], subject, + template=template, dictionary={'object': project} ).send() except NotificationError, e: diff --git a/snf-astakos-app/astakos/im/quotas.py b/snf-astakos-app/astakos/im/quotas.py index f072cdaa003f8c643fbb271a69befea0f0624ae1..2d7d2f37fbb04e33a4dd77990003c63103b434f7 100644 --- a/snf-astakos-app/astakos/im/quotas.py +++ b/snf-astakos-app/astakos/im/quotas.py @@ -1,94 +1,113 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. -from synnefo.util import units from astakos.im.models import ( - Resource, AstakosUserQuota, AstakosUser, Service, - Project, ProjectMembership, ProjectResourceGrant, ProjectApplication) + Resource, AstakosUser, Service, + Project, ProjectMembership, ProjectResourceQuota) import astakos.quotaholder_app.callpoint as qh from astakos.quotaholder_app.exception import NoCapacityError from django.db.models import Q +from collections import defaultdict + + +QuotaDict = lambda: defaultdict(lambda: defaultdict(dict)) + +PROJECT_TAG = "project:" +USER_TAG = "user:" + + +def project_ref(value): + return PROJECT_TAG + value + + +def get_project_ref(project): + return project_ref(project.uuid) + + +def user_ref(value): + return USER_TAG + value -def from_holding(holding): +def get_user_ref(user): + return user_ref(user.uuid) + + +def from_holding(holding, is_project=False): limit, usage_min, usage_max = holding - body = {'limit': limit, - 'usage': usage_max, - 'pending': usage_max-usage_min, + prefix = 'project_' if is_project else '' + body = {prefix+'limit': limit, + prefix+'usage': usage_max, + prefix+'pending': usage_max-usage_min, } return body -def limits_only(holding): - limit, usage_min, usage_max = holding - return limit +def get_user_counters(users, resources=None, sources=None, flt=None): + holders = [get_user_ref(user) for user in users] + return qh.get_quota(holders=holders, + resources=resources, + sources=sources, + flt=flt) -def transform_data(holdings, func=None): - if func is None: - func = from_holding +def get_project_counters(projects, resources=None, sources=None): + holders = [get_project_ref(project) for project in projects] + return qh.get_quota(holders=holders, + resources=resources, + sources=sources) - quota = {} - for (holder, source, resource), value in holdings.iteritems(): - holder_quota = quota.get(holder, {}) - source_quota = holder_quota.get(source, {}) - body = func(value) - source_quota[resource] = body - holder_quota[source] = source_quota - quota[holder] = holder_quota - return quota +def strip_names(counters): + stripped = {} + for ((holder, source, resource), value) in counters.iteritems(): + prefix, sep, holder = holder.partition(":") + assert prefix in ["user", "project"] + if source is not None: + prefix, sep, source = source.partition(":") + assert prefix == "project" + stripped[(holder, source, resource)] = value + return stripped -def get_counters(users, resources=None, sources=None, flt=None): - uuids = [user.uuid for user in users] - counters = qh.get_quota(holders=uuids, - resources=resources, - sources=sources, - flt=flt) - return counters +def get_related_sources(counters): + projects = set() + for (holder, source, resource) in counters.iterkeys(): + projects.add(source) + return list(projects) -def get_users_quotas(users, resources=None, sources=None, flt=None): - counters = get_counters(users, resources, sources, flt=flt) - quotas = transform_data(counters) - return quotas +def mk_quota_dict(users_counters, project_counters): + quota = QuotaDict() + for (holder, source, resource), u_value in users_counters.iteritems(): + p_value = project_counters[(source, None, resource)] + values_dict = from_holding(u_value) + values_dict.update(from_holding(p_value, is_project=True)) + quota[holder][source][resource] = values_dict + return quota + +def get_users_quotas_counters(users, resources=None, sources=None, flt=None): + user_counters = get_user_counters(users, resources, sources, flt=flt) + projects = get_related_sources(user_counters) + project_counters = qh.get_quota(holders=projects, resources=resources) + return strip_names(user_counters), strip_names(project_counters) -def get_users_quota_limits(users, resources=None, sources=None): - counters = get_counters(users, resources, sources) - limits = transform_data(counters, limits_only) - return limits + +def get_users_quotas(users, resources=None, sources=None, flt=None): + u_c, p_c = get_users_quotas_counters(users, resources, sources, flt=flt) + return mk_quota_dict(u_c, p_c) def get_user_quotas(user, resources=None, sources=None): @@ -96,14 +115,62 @@ def get_user_quotas(user, resources=None, sources=None): return quotas.get(user.uuid, {}) -def service_get_quotas(component, users=None): +def service_get_quotas(component, users=None, sources=None): + name_values = Service.objects.filter( + component=component).values_list('name') + service_names = [t for (t,) in name_values] + resources = Resource.objects.filter(service_origin__in=service_names) + resource_names = [r.name for r in resources] + astakosusers = AstakosUser.objects.verified() + if users is not None: + astakosusers = astakosusers.filter(uuid__in=users) + if sources is not None: + sources = [project_ref(s) for s in sources] + return get_users_quotas(astakosusers, resources=resource_names, + sources=sources) + + +def mk_limits_dict(counters): + quota = QuotaDict() + for (holder, source, resource), (limit, _, _) in counters.iteritems(): + quota[holder][source][resource] = limit + return quota + + +def mk_project_quota_dict(project_counters): + quota = QuotaDict() + for (holder, _, resource), p_value in project_counters.iteritems(): + values_dict = from_holding(p_value, is_project=True) + quota[holder][resource] = values_dict + return quota + + +def get_projects_quota(projects, resources=None, sources=None): + project_counters = get_project_counters(projects, resources, sources) + return mk_project_quota_dict(strip_names(project_counters)) + + +def service_get_project_quotas(component, projects=None): name_values = Service.objects.filter( component=component).values_list('name') service_names = [t for (t,) in name_values] resources = Resource.objects.filter(service_origin__in=service_names) resource_names = [r.name for r in resources] - counters = qh.get_quota(holders=users, resources=resource_names) - return transform_data(counters) + ps = Project.objects.initialized() + if projects is not None: + ps = ps.filter(uuid__in=projects) + return get_projects_quota(ps, resources=resource_names) + + +def get_project_quota(project, resources=None, sources=None): + quotas = get_projects_quota([project], resources, sources) + return quotas.get(project.uuid, {}) + + +def get_projects_quota_limits(): + project_counters = qh.get_quota(flt=Q(holder__startswith=PROJECT_TAG)) + user_counters = qh.get_quota(flt=Q(holder__startswith=USER_TAG)) + return mk_limits_dict(project_counters), mk_limits_dict(user_counters) def _level_quota_dict(quotas): @@ -116,21 +183,43 @@ def _level_quota_dict(quotas): return lst -def _set_user_quota(quotas, resource=None): +def set_quota(quotas, resource=None): q = _level_quota_dict(quotas) qh.set_quota(q, resource=resource) -SYSTEM = 'system' PENDING_APP_RESOURCE = 'astakos.pending_app' -def register_pending_apps(user, quantity, force=False): - provision = (user.uuid, SYSTEM, PENDING_APP_RESOURCE), quantity +def mk_user_provision(user, source, resource, quantity): + holder = user_ref(user) + source = project_ref(source) + return (holder, source, resource), quantity + + +def mk_project_provision(project, resource, quantity): + holder = project_ref(project) + return (holder, None, resource), quantity + + +def _mk_provisions(values): + provisions = [] + for (holder, source, resource, quantity) in values: + provisions += [((holder, source, resource), quantity), + ((source, None, resource), quantity)] + return provisions + + +def register_pending_apps(triples, force=False): + values = [(get_user_ref(user), get_project_ref(project), + PENDING_APP_RESOURCE, quantity) + for (user, project, quantity) in triples] + + provisions = _mk_provisions(values) try: s = qh.issue_commission(clientkey='astakos', force=force, - provisions=[provision]) + provisions=provisions) except NoCapacityError as e: limit = e.data['limit'] return False, limit @@ -140,15 +229,8 @@ def register_pending_apps(user, quantity, force=False): def get_pending_app_quota(user): quota = get_user_quotas(user) - return quota[SYSTEM][PENDING_APP_RESOURCE] - - -def update_base_quota(users, resource, value): - userids = [user.pk for user in users] - AstakosUserQuota.objects.\ - filter(resource__name=resource, user__pk__in=userids).\ - update(capacity=value) - qh_sync_locked_users(users, resource=resource) + source = user.get_base_project().uuid + return quota[source][PENDING_APP_RESOURCE] def _partition_by(f, l): @@ -161,165 +243,91 @@ def _partition_by(f, l): return d -def initial_quotas(users, flt=None): - if flt is None: - flt = Q() - - userids = [user.pk for user in users] - objs = AstakosUserQuota.objects.select_related('resource') - orig_quotas = objs.filter(user__pk__in=userids).filter(flt) - orig_quotas = _partition_by(lambda q: q.user_id, orig_quotas) - - initial = {} - for user in users: - qs = {} - for q in orig_quotas.get(user.pk, []): - qs[q.resource.name] = q.capacity - initial[user.uuid] = {SYSTEM: qs} - return initial - - -def get_grant_source(grant): - return SYSTEM - - -def add_limits(x, y): - return min(x+y, units.PRACTICALLY_INFINITE) - - -def astakos_users_quotas(users, resource=None): - users = list(users) +def astakos_project_quotas(projects, resource=None): + objs = ProjectResourceQuota.objects.select_related() flt = Q(resource__name=resource) if resource is not None else Q() - quotas = initial_quotas(users, flt=flt) - - userids = [user.pk for user in users] - ACTUALLY_ACCEPTED = ProjectMembership.ACTUALLY_ACCEPTED - objs = ProjectMembership.objects.select_related( - 'project', 'person', 'project__application') - memberships = objs.filter( - person__pk__in=userids, - state__in=ACTUALLY_ACCEPTED, - project__state=Project.NORMAL, - project__application__state=ProjectApplication.APPROVED) - - apps = set(m.project.application_id for m in memberships) - - objs = ProjectResourceGrant.objects.select_related() - grants = objs.filter(project_application__in=apps).filter(flt) - - for membership in memberships: - uuid = membership.person.uuid - userquotas = quotas.get(uuid, {}) - - application = membership.project.application - - for grant in grants: - if grant.project_application_id != application.id: - continue - - source = get_grant_source(grant) - source_quotas = userquotas.get(source, {}) - - resource = grant.resource.full_name() - prev = source_quotas.get(resource, 0) - new = add_limits(prev, grant.member_capacity) - source_quotas[resource] = new - userquotas[source] = source_quotas - quotas[uuid] = userquotas - - return quotas - - -def list_user_quotas(users, qhflt=None, initflt=None): + grants = objs.filter(project__in=projects).filter(flt) + grants_d = _partition_by(lambda g: g.project_id, grants) + + objs = ProjectMembership.objects + memberships = objs.initialized(projects).select_related( + "person", "project") + memberships_d = _partition_by(lambda m: m.project_id, memberships) + + user_quota = QuotaDict() + project_quota = QuotaDict() + + for project in projects: + pr_ref = get_project_ref(project) + state = project.state + if state not in Project.INITIALIZED_STATES: + continue + + project_grants = grants_d.get(project.id, []) + project_memberships = memberships_d.get(project.id, []) + for grant in project_grants: + resource = grant.resource.name + val = grant.project_capacity if state == Project.NORMAL else 0 + project_quota[pr_ref][None][resource] = val + for membership in project_memberships: + u_ref = get_user_ref(membership.person) + val = grant.member_capacity if membership.is_active() else 0 + user_quota[u_ref][pr_ref][resource] = val + + return project_quota, user_quota + + +def list_user_quotas(users, qhflt=None): qh_quotas = get_users_quotas(users, flt=qhflt) - astakos_initial = initial_quotas(users, flt=initflt) - return qh_quotas, astakos_initial - - -# Syncing to quotaholder - -def get_users_for_update(user_ids): - uids = sorted(user_ids) - objs = AstakosUser.objects - return list(objs.filter(id__in=uids).order_by('id').select_for_update()) - - -def get_user_for_update(user_id): - return get_users_for_update([user_id])[0] - - -def qh_sync_locked_users(users, resource=None): - astakos_quotas = astakos_users_quotas(users, resource=resource) - _set_user_quota(astakos_quotas, resource=resource) - + return qh_quotas -def qh_sync_users(users, resource=None): - uids = [user.id for user in users] - users = get_users_for_update(uids) - qh_sync_locked_users(users, resource=resource) +def qh_sync_projects(projects, resource=None): + p_quota, u_quota = astakos_project_quotas(projects, resource=resource) + p_quota.update(u_quota) + set_quota(p_quota, resource=resource) -def qh_sync_users_diffs(users, sync=True): - uids = [user.id for user in users] - if sync: - users = get_users_for_update(uids) - astakos_quotas = astakos_users_quotas(users) - qh_limits = get_users_quota_limits(users) - diff_quotas = {} - for holder, local in astakos_quotas.iteritems(): - registered = qh_limits.get(holder, None) - if local != registered: - diff_quotas[holder] = dict(local) - - if sync: - _set_user_quota(diff_quotas) - return qh_limits, diff_quotas - - -def qh_sync_locked_user(user): - qh_sync_locked_users([user]) - - -def qh_sync_user(user): - qh_sync_users([user]) - - -def qh_sync_new_users(users): - entries = [] - for resource in Resource.objects.all(): - for user in users: - entries.append( - AstakosUserQuota(user=user, resource=resource, - capacity=resource.uplimit)) - AstakosUserQuota.objects.bulk_create(entries) - qh_sync_users(users) +def qh_sync_project(project): + qh_sync_projects([project]) -def qh_sync_new_user(user): - qh_sync_new_users([user]) +def membership_quota(membership): + project = membership.project + pr_ref = get_project_ref(project) + u_ref = get_user_ref(membership.person) + objs = ProjectResourceQuota.objects.select_related() + grants = objs.filter(project=project) + user_quota = QuotaDict() + is_active = membership.is_active() + for grant in grants: + resource = grant.resource.name + value = grant.member_capacity if is_active else 0 + user_quota[u_ref][pr_ref][resource] = value + return user_quota -def members_to_sync(project): - objs = ProjectMembership.objects.select_related('person') - memberships = objs.filter(project=project, - state__in=ProjectMembership.ACTUALLY_ACCEPTED) - return set(m.person for m in memberships) +def qh_sync_membership(membership): + quota = membership_quota(membership) + set_quota(quota) -def qh_sync_project(project): - users = members_to_sync(project) - qh_sync_users(users) +def pick_limit_scheme(project, resource): + return resource.uplimit if project.is_base else resource.project_default def qh_sync_new_resource(resource): - users = AstakosUser.objects.filter( - moderated=True, is_rejected=False).order_by('id').select_for_update() + projects = Project.objects.filter(state__in=Project.INITIALIZED_STATES).\ + select_for_update() entries = [] - for user in users: + for project in projects: + limit = pick_limit_scheme(project, resource) entries.append( - AstakosUserQuota(user=user, resource=resource, - capacity=resource.uplimit)) - AstakosUserQuota.objects.bulk_create(entries) - qh_sync_users(users, resource=resource.name) + ProjectResourceQuota( + project=project, + resource=resource, + project_capacity=limit, + member_capacity=limit)) + ProjectResourceQuota.objects.bulk_create(entries) + qh_sync_projects(projects, resource=resource.name) diff --git a/snf-astakos-app/astakos/im/register.py b/snf-astakos-app/astakos/im/register.py index 93f0f1f2bdb274f90380ae082eb66c2eb711cf7b..59f34fb05f77b3508fcacbda426b71bdea8568b9 100644 --- a/snf-astakos-app/astakos/im/register.py +++ b/snf-astakos-app/astakos/im/register.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from synnefo.util import units from astakos.im.models import Resource, Service, Endpoint, EndpointData @@ -93,6 +75,8 @@ def add_resource(resource_dict): if value is not None: setattr(r, field, value) + r.project_default = 0 if r.api_visible else units.PRACTICALLY_INFINITE + for field in main_fields: value = resource_dict.get(field) if value is not None: @@ -113,19 +97,28 @@ def add_resource(resource_dict): return r, exists -def update_resources(updates): - resources = [] - for resource, uplimit in updates: - resources.append(resource) - old_uplimit = resource.uplimit - if uplimit == old_uplimit: - logger.info("Resource %s has limit %s; no need to update." - % (resource.name, uplimit)) - else: - resource.uplimit = uplimit - resource.save() - logger.info("Updated resource %s with limit %s." - % (resource.name, uplimit)) +def update_base_default(resource, base_default): + old_base_default = resource.uplimit + if base_default == old_base_default: + logger.info("Resource %s has base default %s; no need to update." + % (resource.name, base_default)) + else: + resource.uplimit = base_default + resource.save() + logger.info("Updated resource %s with base default %s." + % (resource.name, base_default)) + + +def update_project_default(resource, project_default): + old_project_default = resource.project_default + if project_default == old_project_default: + logger.info("Resource %s has project default %s; no need to update." + % (resource.name, project_default)) + else: + resource.project_default = project_default + resource.save() + logger.info("Updated resource %s with project default %s." + % (resource.name, project_default)) def resources_to_dict(resources): diff --git a/snf-astakos-app/astakos/im/settings.py b/snf-astakos-app/astakos/im/settings.py index 57cb4fd2c526d2804688205dc37599b3f47ce3d5..137123de6b21ad1531d1e8cffbb17f1be4ed8848 100644 --- a/snf-astakos-app/astakos/im/settings.py +++ b/snf-astakos-app/astakos/im/settings.py @@ -1,41 +1,22 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.conf import settings from synnefo_branding import settings as synnefo_settings from synnefo.lib import parse_base_url from astakos.api.services import astakos_services as vanilla_astakos_services -from synnefo.util.keypath import get_path from synnefo.lib import join_urls from synnefo.lib.services import fill_endpoints @@ -50,11 +31,11 @@ BASE_HOST, BASE_PATH = parse_base_url(BASE_URL) astakos_services = deepcopy(vanilla_astakos_services) fill_endpoints(astakos_services, BASE_URL) -ACCOUNTS_PREFIX = get_path(astakos_services, 'astakos_account.prefix') -VIEWS_PREFIX = get_path(astakos_services, 'astakos_ui.prefix') -KEYSTONE_PREFIX = get_path(astakos_services, 'astakos_identity.prefix') -WEBLOGIN_PREFIX = get_path(astakos_services, 'astakos_weblogin.prefix') -ADMIN_PREFIX = get_path(astakos_services, 'astakos_admin.prefix') +ACCOUNTS_PREFIX = astakos_services['astakos_account']['prefix'] +VIEWS_PREFIX = astakos_services['astakos_ui']['prefix'] +KEYSTONE_PREFIX = astakos_services['astakos_identity']['prefix'] +WEBLOGIN_PREFIX = astakos_services['astakos_weblogin']['prefix'] +ADMIN_PREFIX = astakos_services['astakos_admin']['prefix'] # Set the expiration time of newly created auth tokens # to be this many hours after their creation time. @@ -74,6 +55,39 @@ ADMINS = tuple(getattr(settings, 'ADMINS', ())) MANAGERS = tuple(getattr(settings, 'MANAGERS', ())) HELPDESK = tuple(getattr(settings, 'HELPDESK', ())) +# For convenience, Astakos groups the notifications in three categories and +# let the user define the recipients for these categories. +# - ACCOUNT_NOTIFICATIONS_RECIPIENTS receive notifications for 'account pending +# moderation' and 'account activated' actions. +# - FEEDBACK_NOTIFICATIONS_RECIPIENTS receive feedback notifications +# - PROJECT_NOTIFICATIONS_RECIPIENTS receive notifications for 'project +# creation' and 'project modification' actions. +ACCOUNT_NOTIFICATIONS_RECIPIENTS = tuple(set(tuple( + getattr(settings, 'ACCOUNT_NOTIFICATIONS_RECIPIENTS', + HELPDESK + MANAGERS + ADMINS)))) +FEEDBACK_NOTIFICATIONS_RECIPIENTS = tuple(set(tuple( + getattr(settings, 'FEEDBACK_NOTIFICATIONS_RECIPIENTS', + HELPDESK)))) +PROJECT_NOTIFICATIONS_RECIPIENTS = tuple(set(tuple( + getattr(settings, 'PROJECT_NOTIFICATIONS_RECIPIENTS', + HELPDESK + MANAGERS)))) + +# Using the following settings, one can explicitly specify the recipients for a +# specific notification. By default, these settings are not exposed to the +# config file. +ACCOUNT_PENDING_MODERATION_RECIPIENTS = tuple(set(tuple( + getattr(settings, 'ACCOUNT_PENDING_MODERATION_RECIPIENTS', + ACCOUNT_NOTIFICATIONS_RECIPIENTS)))) +ACCOUNT_ACTIVATED_RECIPIENTS = tuple(set(tuple( + getattr(settings, 'ACCOUNT_ACTIVATED_RECIPIENTS', + ACCOUNT_NOTIFICATIONS_RECIPIENTS)))) +PROJECT_CREATION_RECIPIENTS = tuple(set(tuple( + getattr(settings, 'PROJECT_CREATION_RECIPIENTS', + PROJECT_NOTIFICATIONS_RECIPIENTS)))) +PROJECT_MODIFICATION_RECIPIENTS = tuple(set(tuple( + getattr(settings, 'PROJECT_MODIFICATION_RECIPIENTS', + PROJECT_NOTIFICATIONS_RECIPIENTS)))) + CONTACT_EMAIL = settings.CONTACT_EMAIL SERVER_EMAIL = settings.SERVER_EMAIL SECRET_KEY = settings.SECRET_KEY @@ -86,7 +100,7 @@ IM_MODULES = getattr(settings, 'ASTAKOS_IM_MODULES', ['local']) # Force user profile verification FORCE_PROFILE_UPDATE = getattr(settings, 'ASTAKOS_FORCE_PROFILE_UPDATE', False) -#Enable invitations +# Enable invitations INVITATIONS_ENABLED = getattr(settings, 'ASTAKOS_INVITATIONS_ENABLED', False) COOKIE_NAME = getattr(settings, 'ASTAKOS_COOKIE_NAME', '_pithos2_a') @@ -221,9 +235,6 @@ default_success_url = join_urls('/', BASE_PATH, VIEWS_PREFIX, "landing") LOGIN_SUCCESS_URL = getattr(settings, 'ASTAKOS_LOGIN_SUCCESS_URL', default_success_url) -# Whether or not to display projects in astakos menu -PROJECTS_VISIBLE = getattr(settings, 'ASTAKOS_PROJECTS_VISIBLE', False) - # A way to extend the components presentation metadata COMPONENTS_META = getattr(settings, 'ASTAKOS_COMPONENTS_META', {}) @@ -257,3 +268,21 @@ ENDPOINT_CACHE_TIMEOUT = getattr(settings, RESOURCE_CACHE_TIMEOUT = getattr(settings, 'ASTAKOS_RESOURCE_CACHE_TIMEOUT', 60) + +ADMIN_API_ENABLED = getattr(settings, 'ASTAKOS_ADMIN_API_ENABLED', False) + +_default_project_members_limit_choices = ( + ('Unlimited', 'Unlimited'), + ('5', '5'), + ('15', '15'), + ('50', '50'), + ('100', '100') +) + +PROJECT_MEMBERS_LIMIT_CHOICES = getattr( + settings, 'ASTAKOS_PROJECT_MEMBERS_LIMIT_CHOICES', + _default_project_members_limit_choices) + +ADMIN_API_PERMITTED_GROUPS = getattr(settings, + 'ASTAKOS_ADMIN_API_PERMITTED_GROUPS', + ['admin-api']) diff --git a/snf-astakos-app/astakos/im/static/im/cloudbar/cloudbar.css b/snf-astakos-app/astakos/im/static/im/cloudbar/cloudbar.css index 681ffc6e3f0838f17ca85a6a4e1ca39e357b0300..c4ef686605e297adf42f6ab1076b163f8c617865 100644 --- a/snf-astakos-app/astakos/im/static/im/cloudbar/cloudbar.css +++ b/snf-astakos-app/astakos/im/static/im/cloudbar/cloudbar.css @@ -14,6 +14,7 @@ div.cloudbar li {font-family:'Open Sans',sans-serif !important; letter-s .cloudbar .wrapper { width:auto; padding:0;} .cloudbar a { color:#fff; text-decoration:none;} +.cloudbar a img { border: 0 none; } .cloudbar .profile { float:right; background:#3582AC; min-width:190px; padding:0; text-align:right; } .cloudbar .profile:hover { background:#5A97B8; } .cloudbar .profile a { text-decoration:none; color:#fff; display:block; width:100%;} diff --git a/snf-astakos-app/astakos/im/static/im/cloudbar/cloudbar.js b/snf-astakos-app/astakos/im/static/im/cloudbar/cloudbar.js index 616709a7e2fa6bee29e883d1add2c69d06003345..96e4713cb33839966cf1d80c69fd9195b8b53fc1 100644 --- a/snf-astakos-app/astakos/im/static/im/cloudbar/cloudbar.js +++ b/snf-astakos-app/astakos/im/static/im/cloudbar/cloudbar.js @@ -19,19 +19,26 @@ $(document).ready(function(){ var USER_DATA = window.CLOUDBAR_USER_DATA || {'user': 'Not logged in', 'logged_in': false}; var COOKIE_NAME = window.CLOUDBAR_COOKIE_NAME || '_pithos2_a'; + var VERSION = window.CLOUDBAR_VERSION || ''; var cssloc = window.CLOUDBAR_LOCATION || "http://127.0.0.1:8989/"; // load css var css = $("<link />"); - css.attr({rel:'stylesheet', type:'text/css', href:cssloc + 'cloudbar.css'}); + css.attr({rel:'stylesheet', type:'text/css', href:cssloc + 'cloudbar.css?'+VERSION}); $("head").append(css); - // load fonts - var font_url = 'https://fonts.googleapis.com/css?family=Open+Sans:400,600,700&subset=latin,greek-ext,greek'; - var css_font = $("<link />"); - css_font.attr({rel:'stylesheet', type:'text/css', href:font_url}); - $("head").append(css_font); + // load extra css + var extra_css = window.CLOUDBAR_EXTRA_CSS || []; + if (window.CLOUDBAR_INCLUDE_FONTS === false) { extra_css = []; } + var css_tag = undefined; + var css_uri = undefined; + for (var i=0; i<extra_css.length; i++) { + css_uri = extra_css[i]; + css_tag = $("<link />"); + css_tag.attr({rel:'stylesheet', type:'text/css', href: css_uri}); + $("head").append(css_tag); + } // load service specific css var SKIP_ADDITIONAL_CSS = window.SKIP_ADDITIONAL_CSS == undefined ? false : window.SKIP_ADDITIONAL_CSS; @@ -138,7 +145,6 @@ $(document).ready(function(){ equalWidths ( $('.cloudbar .profile ul'), $('.cloudbar .profile')); $('.cloudbar .profile .full>a').live('focus', function(e){ - console.info('i just focused'); e.preventDefault(); equalWidths ( $('.cloudbar .profile ul'), $('.cloudbar .profile')); $(this).siblings('ul').show(); @@ -150,7 +156,6 @@ $(document).ready(function(){ }); $('.cloudbar .profile ul li:last a').live('focusout', function(e){ - console.info('i just focused out in style'); $(this).parents('ul').attr('style', ''); $(this).parents('ul').removeAttr('style'); equalWidths ( $('.cloudbar .profile ul'), $('.cloudbar .profile')); diff --git a/snf-astakos-app/astakos/im/static/im/css/dropkick.css b/snf-astakos-app/astakos/im/static/im/css/dropkick.css index 538ea4a15eca317c6c9c88c257726d065e0fbaae..2a5a3e7cab7dc5789bd85795fbafa4bb96d697e6 100644 --- a/snf-astakos-app/astakos/im/static/im/css/dropkick.css +++ b/snf-astakos-app/astakos/im/static/im/css/dropkick.css @@ -13,7 +13,7 @@ /* dropkick select extra styles */ -.form-row .dk_container { border-radius:0; margin-bottom:0; border: 1px solid gray; height: 21px; letter-spacing: 1px; line-height: 22px; margin-bottom: -1px; width:270px; padding:0.8em; padding-left:1.5em; font-weight:normal; font-family: 'Didact Gothic', Verdana, sans-serif; font-size:1em; background:transparent; border-color:#adadad;} +.form-row .dk_container { border-radius:0; margin-bottom:0; border: 1px solid gray; height: 21px; letter-spacing: 1px; line-height: 22px; margin-bottom: -1px; width:267px; padding:0.8em; padding-left:1.5em; font-weight:normal; font-family: 'Didact Gothic', Verdana, sans-serif; font-size:1em; background:transparent; border-color:#adadad;} .form-row .dk_toggle { border-radius:0; padding:0 0 10px; border:0 none; text-decoration:none;background-image:url(../images/select-arrow-down_grey.png); background-position:75% 5px;} .form-row .dk_toggle:hover { text-decoration:none; background-image:url(../images/select-arrow-down_yellow.png); background-position:75% 6px; } .form-row .dk_open { background:transparent; box-shadow: none; } diff --git a/snf-astakos-app/astakos/im/static/im/css/formating.css b/snf-astakos-app/astakos/im/static/im/css/formating.css index 97af08659631e1c3a2120bc9307ca8d9a95566ed..fd63c149c6621135b2de073ffc86d9f93941f9f8 100644 --- a/snf-astakos-app/astakos/im/static/im/css/formating.css +++ b/snf-astakos-app/astakos/im/static/im/css/formating.css @@ -211,4 +211,5 @@ h2 .header-actions { float: right; font-size: 0.8em;} display: block; color:#F24E53; font-family: monospace; -} \ No newline at end of file +} + diff --git a/snf-astakos-app/astakos/im/static/im/css/forms.css b/snf-astakos-app/astakos/im/static/im/css/forms.css index a5985c3e7e362035ba4df99371fcb8f20fe8d361..8d786ed674a299e22f1a2507d5ef4e207da677f5 100644 --- a/snf-astakos-app/astakos/im/static/im/css/forms.css +++ b/snf-astakos-app/astakos/im/static/im/css/forms.css @@ -20,6 +20,7 @@ form.login { margin-bottom: 22px; width:340px; } form h2 span { padding-bottom: 3px; } form .form-row { min-height: 30px; position: relative;} form .form-row.submit { margin: 22px 0 ;} +form .form-row.submit .submit.align-right { float: right;} form .form-row .extra-link { color: #808080; text-decoration: none; border: none; margin-top:15px; line-height:98%; display:inline-block; padding-top:15px; float: right; position:absolute; right:0; top:0; } form .form-row .extra-link:hover { border-bottom:1px solid #9e9e9e;} form .form-row label { font-size: 1.077em; } @@ -77,6 +78,7 @@ form.innerlabels span.info { left: 290px; } form.withlabels span.info { left:485px; } form span.info em { display:block; overflow:hidden; position:absolute; left:0; text-indent:-100px; top:0; height:21px; width:21px; background:url(../images/symbols.png) no-repeat -4px -31px;cursor:pointer; } form span.info:hover em { background-position:-4px -3px; } +.stats .bar i.warn-msg, form span.info span { position:absolute; left:29px; top:-2px; width:120px; padding-left:30px; background:url(../images/black-line.jpg ) no-repeat left 8px; min-height:50px; display:none; font-size:0.846em;} form span.info:hover span { display:block; } form span.extra-img:hover +span.info span { display:block; } diff --git a/snf-astakos-app/astakos/im/static/im/css/modules.css b/snf-astakos-app/astakos/im/static/im/css/modules.css index 6b6540ae4f2d8610c1317c38ae4a5920ea525738..315ab98e6ed240f04305c172b7b53d7b81fc0f1c 100644 --- a/snf-astakos-app/astakos/im/static/im/css/modules.css +++ b/snf-astakos-app/astakos/im/static/im/css/modules.css @@ -51,6 +51,7 @@ img.right { margin:0 0 1em 1em; float:right;} .container .navigation ul li.active a { color:#F89A1C; } .dotted { background:url(../images/double-dots.jpg) no-repeat bottom center; padding:0 0 30px; margin-bottom:30px;} +.full-dotted.hidden { display: none; } .full-dotted { background:url(../images/dots.jpg) repeat-x top; margin-top:20px; padding-top:20px; } .full-dotted:first-child { background:none; padding-top:0; margin-top:0; } .two-cols .rt { float:right; width:400px;} @@ -264,16 +265,28 @@ table.my-projects tr th:hover { cursor: pointer; text-decoration: underline .content a.submit { margin:0; display:inline-block; margin:10px 0 ; height:auto; min-width:100px; text-align:center;} table.alt-style tr:nth-child(2n) td { background:#F2F2F2 } dl.alt-style dt { width:30%; float:left; color:#3582AC; font-weight:normal;} -dl.alt-style dt:nth-child(2n) { background:black; } -dl.alt-style dd { overflow:hidden; position:relative;} +dl.alt-style dd .resource { width:50%; float: left; } +dl.alt-style dd { overflow:hidden; position:relative; margin-left: 0; } +.policy-diff { font-weight: bold; } +.policy-diff.green { color: green; } +.policy-diff.yellow { color: #F6921E; } +.policy-diff.gray { color: #444; } +.policy-diff.red { color: red; } +.policy-diff.details span { color: #aaa; } +.policy-diff.details { display: block; padding: 0; margin: 0; font-size: 0.8em; font-style: italic; } +dl.resources dd { width: 60%; } .projects { padding-bottom:30px; position:relative; } .projects h2 span { color:#3582AC;} +.projects h2 span.prefix {} .projects h2 em { float:right; } .projects h3 { font-size:1.154em; } .projects h3 .rt-action { float:right;} .projects .submit-rt { margin:0; text-align:right; } .projects +.buttons-list.fixpos { left:0; right:auto; } -.project-actions a { font-size: 0.7em } +.project-actions .msg-wrap::after { content:'-'; } +.project-actions .msg-wrap:last-child::after { content:''; } +.back-to-action { display: block; position: absolute; top: -20px; font-size: 0.8em } +/*.project-actions a { font-size: 0.7em }*/ .project-actions a.inactive { color:#ccc; cursor: default} .project-actions a.inactive:hover { text-decoration: none;} .projects dl.alt-style .faint { color:#ccc; font-style: italic} @@ -281,6 +294,14 @@ dl.alt-style dd { overflow:hidden; position:relative;} .projects form.members { margin-top:20px;} .projects form.members label { padding-top:0; text-transform: uppercase;} .my-projects .owner { display: none;} +.resources-heading h3, .resources-heading .resource-label { width: 30%; float: left;} +/* usage */ +.with-usage .resources-heading h3, +.with-usage .resources-heading .resource-label { width: 25%; float: left;} +.with-usage dl.alt-style dt { width: 25%; float:left; color:#3582AC; font-weight:normal;} +.with-usage dl.resources dd { width: 75%; } +.with-usage dl.alt-style dd .resource { width: 33%; float: left; } +.with-usage dl.alt-style dd .resource.fix-col { width: 32%; padding-left: 4px; } /* new faq-userguide styles */ @@ -411,6 +432,7 @@ table.alt-style tr td.info-td div { padding:15px; border:1px dashed #000 } /* quotas-form */ +.quotas-form fieldset.hidden { display: none; } .quotas-form fieldset { background:url(../images/dots.jpg) repeat-x scroll center bottom transparent; margin-bottom:3em; padding-bottom:5em; position:relative; } .quotas-form fieldset#icons { padding-bottom:3em; } .quotas-form legend { color:#55B577; font-size:1.308em; position:relative; } @@ -429,6 +451,7 @@ form.quotas-form span.info span { width:285px; } .quotas-form .with-checkbox .checkbox-widget { margin-top:9px; } .quotas-form .with-checkbox span.info { top:12px; } .quotas-form .form-row.submit { text-align:center; } +.quotas-form .form-row input.custom-select { width: 40px; margin-left: -1px; padding-right: 1.5em} .quotas-form input[type="submit"] { margin:15px 0; } /* grey green buttons .quotas-form input[type="submit"] { background-color:#B3B3B3 } @@ -468,6 +491,9 @@ form.quotas-form span.info span { width:285px; } .quotas-form .double-checks .with-checkbox input[type="text"].hideshow { display:block; } .quotas-form .with-checkbox+.with-checkbox { width:196px; } .summary dl.alt-style dt { color:#55B577; } +.quotas-form .system-warning { display: none; margin-top: 3em;} +.quotas-form .hidden-contents .form-row { display: none; } +.quotas-form .hidden-contents .system-warning { display: block; } .quotas-form .with-info .double-checks p { clear:both; } .quotas-form .with-info .with-checkbox+.with-checkbox { width:auto; } .quotas-form .with-info .double-checks { position:relative; margin-bottom:70px; } @@ -501,28 +527,33 @@ form input[type="text"]:-ms-input-placeholder, /* stats */ .stats ul { margin:0; padding:0; list-style:none outside none; } .stats ul li { margin:0 0 1em 0; padding:0 0 1em 0; list-style:none outside none; background:url(../images/stats-line.jpg) repeat-x left bottom} +.stats h2 { margin-left: 124px; margin-bottom: 1em; padding-top: 8px;} .stats .bar { padding: 0; float:left; } .stats .bar div { width:340px; height:30px; border:1px solid #000; margin-top:20px; overflow:hidden;} -.stats .bar span { text-align:right; display:block; float:left; height:100%; position: relative; overflow: visible; } +.stats .bar span.project { background-color: #999999 !important; border-left: 1px solid #777777; margin-right: -1px; float: right !important;} +.stats .bar { overflow: visible; } +.stats .bar i.warn-msg.visible, +.stats .bar i.warn.visible { display: block; } +.stats .bar i.warn-msg { left: -157px !important; top: 8px !important; z-index: 101; padding-left: 0; padding-right: 30px; background-position: right 8px !important; padding-left: 0 !important; width: 80px !important; background-color: #fff !important; padding-left: 10px;} +.stats .bar i.warn-msg.hovered { display: block; } +.stats .bar i.warn { display: none; top: 7px; left: -30px; background:url(../images/symbols.png) no-repeat -58px -3px; z-index:101; width:21px; height:21px; overflow:hidden; position:absolute; } +.stats .bar span.member, +.stats .bar span.project { text-align:right; display:block; float:left; height:100%; position: relative; overflow: visible; } .stats .bar span.hovered { } .stats .bar span.value { background-color: transparent !important; } .stats .bar span em { color:#000; } .stats .bar span.hovered em { color:#fff; } -.stats .bar em { - font-style:normal; - color:#222; - line-height:30px; - font-size:1.231em; - padding-left:10px; - -} - +.stats .bar em { font-style:normal; color:#222; line-height: 27px; font-size: 1.231em; padding-left: 10px; } +.stats .resource-projects { float: left; } .stats .bar span + em { position: absolute; } .stats .bar { position: relative; } -.stats .red .bar span { background:#ef4f54; } -.stats .yellow .bar span { background:#f6921e; } -.stats .green .bar span { background:#55b577; } -.stats .img-wrap { float:left; width:100px; background:url(../images/statistics_icons.png) no-repeat center center; padding:30px 0; } +.stats .resource-bar.red .bar span { background:#ef4f54; } +.stats .resource-bar.orange .bar span { background:#f6921e; } +.stats .resource-bar.green .bar span { background:#55b577; } +.stats .img-wrap { height: 100px; } +.stats .img-wrap { float:left; width:100px; background:url(../images/statistics_icons.png) no-repeat center center; padding: 0 0; } +.stats .info.warn p { color: #f00 !important; } +.stats .info.warn span.warn-msg { display: inline-block; font-size: 0.8em; color: #f00; font-weight: bold; margin-right: 5px;} .stats .info { margin:0 25px ; width:320px; float:left; } .stats .info p { color:#999; margin:0; } .stats .info h3 { font-size:1.231em; color:#222222 } @@ -541,8 +572,17 @@ form input[type="text"]:-ms-input-placeholder, .stats .floating_ip .img-wrap { background-image:url(../images/floating_ip-stats.png) } .stats .red .img-wrap { background-position: 15px 7px; } -.stats .yellow .img-wrap { background-position: -124px 7px; } +.stats .orange .img-wrap { background-position: -124px 7px; } .stats .green .img-wrap { background-position: -263px 7px; } +.stats.filter-base + div.resource-bar.no-base-project { display: none} +.stats .resource-bar.base-project .bar span.project, +.stats .resource-bar.base-project .bar .warn, +.stats .resource-bar.base-project .bar .warn-msg { display: none !important } +.stats .resource-bar.project { margin-top: 1em; } +.stats .resource-bar.project { font-size: 0.8em;} +.stats .resource-bar.project h3 { margin-bottom: 0.5em; } +.stats .resource-bar.project .bar div { width:340px; height:25px; border:1px solid #000; margin-top:5px; overflow:hidden; font-size: 0.8em} .projects .editable form textarea { width:70%; height:50px; max-width:70%; width:270px; height:120px;} @@ -706,4 +746,3 @@ table.cols-3 tr.links td { font-size:1.154em} .form-actions.inactive form.link-like input[type="submit"] { background:#e8e8e8; cursor:default; } .search-projects + #projects-list_wrapper table caption { font-size: 1.308em;} - diff --git a/snf-astakos-app/astakos/im/static/im/js/common.js b/snf-astakos-app/astakos/im/static/im/js/common.js index 6d2a7564e255596ee95e183843d62009f640730e..9ccc7ac14c5f0286b8becf8f7f5abde6bcbe1e27 100644 --- a/snf-astakos-app/astakos/im/static/im/js/common.js +++ b/snf-astakos-app/astakos/im/static/im/js/common.js @@ -241,7 +241,6 @@ $(document).ready(function() { $('select.dropkicked').dropkick({ change: function (value, label) { $(this).parents('form').submit(); - } }); diff --git a/snf-astakos-app/astakos/im/static/im/js/jquery.dropkick-1.0.0.js b/snf-astakos-app/astakos/im/static/im/js/jquery.dropkick-1.0.0.js index 0262cbb66b1ef2dd205cb952701396a589c8ac03..b6755876248c36c49035d6f109ddfae58d6d9351 100644 --- a/snf-astakos-app/astakos/im/static/im/js/jquery.dropkick-1.0.0.js +++ b/snf-astakos-app/astakos/im/static/im/js/jquery.dropkick-1.0.0.js @@ -273,6 +273,7 @@ $select = data.$select; $select.val(value); + $select.change(); $dk.find('.dk_label').text(fixLabel(label)); @@ -410,4 +411,4 @@ } }); }); -})(jQuery, window, document); \ No newline at end of file +})(jQuery, window, document); diff --git a/snf-astakos-app/astakos/im/static/im/js/quotas.js b/snf-astakos-app/astakos/im/static/im/js/quotas.js index 7fd1edfb37f4f1ff81c445a88fc511ab1c62d26b..a11965fc8596309c04497e6c5629c21718b50911 100644 --- a/snf-astakos-app/astakos/im/static/im/js/quotas.js +++ b/snf-astakos-app/astakos/im/static/im/js/quotas.js @@ -43,6 +43,7 @@ function group_form_toggle_resources(el){ function bytesToSize2(bytes) { var sizes = [ 'n/a', 'bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; var i = +Math.floor(Math.log(bytes) / Math.log(1024)); + if (!isFinite(i)) { return 0 + 'KB'} return (bytes / Math.pow(1024, i)).toFixed( 0 ) + sizes[ isNaN( bytes ) ? 0 : i+1 ]; } @@ -67,7 +68,9 @@ $(document).ready(function() { // get the hidden input field without the proxy // and check the python form field - hidden_name = $(this).siblings('input[type="hidden"]').attr('name').replace("proxy_",""); + var name = $(this).siblings('input[type="hidden"]').attr('name'); + if (!name) { return } + var hidden_name = name.replace("proxy_",""); $("input[name='"+hidden_name+"']").val("1"); // prevent extra actions if it is checked @@ -143,8 +146,12 @@ $(document).ready(function() { hidden_name = $(this).attr('name').replace("_proxy",""); var hidden_input = $("input[name='"+hidden_name+"']"); + if (value.match(/^(inf|unlimited)/i)) { + $(this).parents('.form-row').removeClass('with-errors'); + hidden_input.val("Unlimited"); + return; + } if (value) { - // actions for humanize fields if ($(this).hasClass('dehumanize')){ @@ -159,8 +166,7 @@ $(document).ready(function() { msg="Please enter an integer"; } else { var num = parseInt(value); - if ( num == '0' ) { - flag = 1 ; msg="This value can not be zero. Try something like 10GB, 2MB etc" + if ( num == '0' ) { } else { if ( value && !num ) { flag = 1 ; msg="Invalid format. Try something like 10GB, 2MB etc"} @@ -211,9 +217,11 @@ $(document).ready(function() { $(this).parents('.form-row').find('.error-msg').html(msg); bytes = value; $(this).focus(); + $(this).data("not-valid", true); } else { + $(this).data("not-valid", false); $(this).parents('.form-row').removeClass('with-errors'); } @@ -224,20 +232,13 @@ $(document).ready(function() { // validation actions for int fields else { - var is_int = value.match (new RegExp('^[1-9][0-9]*$')); + var is_int = value.match (new RegExp('^[0-9][0-9]*$')); if ( !is_int ){ $(this).parents('.form-row').find('.error-msg').html('Enter a positive integer'); $(this).parents('.form-row').addClass('with-errors'); } else { - if ( value == '0'){ - $(this).parents('.form-row').find('.error-msg').html('Ensure this value is greater than or equal to 1'); - $(this).parents('.form-row').addClass('with-errors'); - }else { - $(this).parents('.form-row').removeClass('with-errors'); - } - - + $(this).parents('.form-row').removeClass('with-errors'); } hidden_input.val(value); @@ -270,34 +271,34 @@ $(document).ready(function() { - $('.group input[name$="_uplimit_proxy"]').each(function() { + $('.group input[name$="_m_uplimit_proxy"], .group input[name$="_p_uplimit_proxy"]').each(function() { if ($(this).val()){ - // get value from input var value = $(this).val(); - // get hidden input name hidden_name = $(this).attr('name'); - hidden_field_name = hidden_name.replace("_proxy",""); + hidden_field_name = hidden_name.replace("_proxy", ""); $("input[name='"+hidden_field_name+"']").val(value); var field = $(this); - if ( (field.hasClass('dehumanize')) && !($(this).parents('.form-row').hasClass('with-errors'))) { // for dehumanize fields transform bytes to KB, MB, etc // unless there is an error - field.val(bytesToSize2(value)) + if (value.match(/^(inf|unlimited)/i)) { + field.val("Unlimited"); + field.data("value", "Unlimited"); + } else { + field.val(bytesToSize2(value) || 0); + field.data("value", bytesToSize2(value) || 0); + } } else { // else just return the value field.val(value); + field.data("value", value); } - var group_class = field.parents('div[class^="group"]').attr('class').replace('group ', ''); - - - - + // select group icon $('.quotas-form ul li a').each(function() { @@ -311,24 +312,17 @@ $(document).ready(function() { $("input[name='"+hidden_name+"']").val("1"); group_form_show_resources($(this)); - } }); - - // if the field has class error, transfer error to the proxy fields if ( $(this).parents('.form-row').hasClass('with-errors') ) { field.parents('.form-row').addClass('with-errors'); } - - } }); - - $('#group_create_form').submit(function(){ var flag = 0; $('.quotas-form .group input[type="text"]').each(function() { @@ -338,8 +332,6 @@ $(document).ready(function() { } }); - console.info(flag); - if (flag =='0') { $('#icons').focus(); $('#icons span.info').addClass('error-msg'); @@ -351,7 +343,7 @@ $(document).ready(function() { } - if ($('.not-visible .group .with-errors').length >0 ){ + if ($('.not-visible .group .with-errors').length > 0 ){ //$('.not-visible .group .with-errors').first().find('input[type="text"]').focus(); return false; @@ -373,6 +365,69 @@ $(document).ready(function() { $(this).parents('.with-errors').removeClass('strong-error'); }); - - -}); \ No newline at end of file + + // enforce uplimit updates + $('.quotas-form .quota input[type="text"]').trigger("keyup"); + + + $('.resource-col input').each(function() { + if (!$(this).attr("name").indexOf("proxy")) { return } + if ($(this).hasClass("dehumanize")) { + var value = $(this).data("value"); + $(this).data("value", bytesToSize2(value) || 0); + } + }); + + $('.resource-col input').bind("blur", function() { + var name = $(this).attr("name"); + var initial_value = $(this).data("value"); + var value = $(this).val(); + var changed_value = $(this).data("changed-value"); + + if ((!changed_value && initial_value != value) || + changed_value != value) { + $(this).data("changed", true); + } else { + $(this).data("changed", false); + } + + var replace_str = "m_uplimit_proxy"; + if (name.indexOf("p_uplimit_proxy") >= 0) { + replace_str = "p_uplimit_proxy"; + } + + window.setTimeout((function() { + return function() { + var get_el = function(id) { + return $("input[name='"+name.replace(replace_str, id)+"']"); + } + + var member_proxy_el = get_el("m_uplimit_proxy"); + var member_value_el = get_el("m_uplimit"); + var project_proxy_el = get_el("p_uplimit_proxy"); + var project_value_el = get_el("p_uplimit"); + var members_el = $("input[name='limit_on_members_number']"); + + if (member_proxy_el.is(":focus") || + project_proxy_el.is(":focus")) { + return + } + + if (!member_proxy_el.val() && !project_proxy_el.val()) { + return + } + + if (!member_proxy_el.val()) { + if (project_proxy_el.data("not-valid")) { + return + } + member_proxy_el.val(project_proxy_el.val()); + member_proxy_el.trigger("keyup"); + member_proxy_el.data("changed-value", + project_proxy_el.val()); + } + + }})(replace_str), 100); + + }); +}); diff --git a/snf-astakos-app/astakos/im/static/im/js/usage.js b/snf-astakos-app/astakos/im/static/im/js/usage.js index fe207a4f1f23fe84d18889bdd4eb5e0c592a91b4..ee6cca7daf30a50dc9d31db88f3f3622ee92af14 100644 --- a/snf-astakos-app/astakos/im/static/im/js/usage.js +++ b/snf-astakos-app/astakos/im/static/im/js/usage.js @@ -1,6 +1,15 @@ ;(function() { +var truncate = function(str, n){ + var p = new RegExp("^.{0," + n + "}[\S]*", 'g'); + var re = str.match(p); + var l = re[0].length; + var re = re[0].replace(/\s$/,''); + if (l < str.length) return _.escape(re) + '…'; + return _.escape(str); +}; + // helper humanize methods // https://github.com/taijinlee/humanize/blob/master/humanize.js humanize = {}; @@ -40,23 +49,33 @@ humanize.numberFormat = function(number, decimals, decPoint, thousandsSep) { }; -DO_LOG = false +DO_LOG = false; LOG = DO_LOG ? _.bind(console.log, console) : function() {}; WARN = DO_LOG ? _.bind(console.warn, console) : function() {}; var default_usage_cls_map = { 0: 'green', - 33: 'yellow', + 33: 'orange', 66: 'red' } function UsageView(settings) { this.settings = settings; this.url = this.settings.url; + this.projects_url = this.settings.projects_url; this.container = $(this.settings.container); + this.project_url_tpl = this.settings.project_url_tpl; + + this.filter_all_btn = $("h2 .filter-all"); + this.filter_base_btn = $("h2 .filter-base"); + this.filter_all_btn.click(_.bind(this.handle_filter_action, this, 'all')); + this.filter_base_btn.click(_.bind(this.handle_filter_action, this, 'base')); + this.meta = this.settings.meta; this.groups = this.settings.groups; + this.projects = {}; this.el = {}; + this.updating_projects = false; this.usage_cls_map = this.settings.usage_cls_map || default_usage_cls_map; this.initialize(); } @@ -64,12 +83,14 @@ function UsageView(settings) { _.extend(UsageView.prototype, { tpls: { - 'main': '<div class="stats clearfix"><ul></ul></div>', - 'quotas': "#quotaTpl" + 'main': '<div class="stats filter-base clearfix"><ul></ul></div>', + 'quotas': "#quotaTpl", + 'projectQuota': "#projectQuotaTpl" }, initialize: function() { LOG("Initializing UsageView", this.settings); + this.updateProjects(this.meta.projects_details); this.initResources(); // initial usage set ???? @@ -78,12 +99,24 @@ _.extend(UsageView.prototype, { this.setQuotas(this.settings.quotas); } this.initLayout(); - this.updateQuotas(); + this.el.main.removeClass('filter-base'); }, $: function(selector) { return this.container; }, + + handle_filter_action: function(filter) { + if (filter == 'base') { + this.el.main.addClass('filter-base'); + this.filter_all_btn.show(); + this.filter_base_btn.hide(); + } else { + this.el.main.removeClass('filter-base'); + this.filter_all_btn.hide(); + this.filter_base_btn.show(); + } + }, render: function(tpl, params) { LOG("Rendering", tpl, params); @@ -101,34 +134,85 @@ _.extend(UsageView.prototype, { this.container.append(this.el.main); var ul = this.container.find("ul"); this.el.list = this.render('quotas', { - 'resources': this.resources_ordered + 'resources': this.resources_ordered, + }); + ul.append(this.el.list).hide(); + _.each(this.resources_ordered, function(resource) { + this.renderResourceProjects(this.container, resource); + }, this); + this.updateQuotas(); + this.handle_filter_action('base'); + ul.show(); + + // live is deprecated in latest jquery versions + $(".bar").live("mouseenter", function() { + var warn = $(this).find("i.warn"); + if (warn.hasClass("visible")) { + $(this).find("i.warn-msg").addClass("hovered"); + } + }).live("mouseleave", function() { + $(this).find("i.warn-msg").removeClass("hovered"); }); - ul.append(this.el.list); }, + renderResourceProjects: function(list, resource) { + var resource_el = list.find("li[data-resource='"+resource.name+"']"); + var projects_el = resource_el.find(".resource-projects"); + projects_el.empty(); + _.each(resource.projects_list, function(project) { + _.extend(project, {report_desc: resource.report_desc}); + projects_el.append(this.render('projectQuota', project)); + }, this); + }, + initResources: function() { var ordered = this.meta.resources_order; var resources = {}; var resources_ordered = []; + var projects = this.projects; _.each(this.meta.resources, function(group, index) { _.each(group[1], function(resource, rindex) { resource.resource_name = resource.name.split(".")[1]; resources[resource.name] = resource; - }) - }); + }, this); + }, this); - resources_ordered = _.filter(_.map(ordered, - function(rk, index) { - rk.index = index; - return resources[rk] - }), - function(i) { return i}); + resources_ordered = _.filter( + _.map(ordered, function(rk, index) { + rk.index = index; + return resources[rk] + }), function(i) { return i }); + this.resources = resources; this.resources_ordered = resources_ordered; LOG("Resources initialized", this.resources_ordered, this.resources); }, + + updateProject: function(uuid, details) { + LOG("Update project", uuid, details); + this.projects[uuid] = details; + }, + + addProject: function(uuid, details) { + LOG("New project", uuid, details); + details.display_name = truncate(details.name, 25); + details.details_url = this.project_url_tpl.replace("UUID", details.id); + this.projects[uuid] = details; + }, + + updateProjects: function(projects, uuids) { + this.projects = {}; + _.each(projects, function(details) { + if (this.projects[details.id]) { + this.updateProject(details.id, details); + } else { + this.addProject(details.id, details); + } + }, this); + LOG("Projects updated", this.projects); + }, updateLayout: function() { LOG("Updating layout", this.quotas); @@ -137,64 +221,179 @@ _.extend(UsageView.prototype, { var usage = self.getUsage(key); if (!usage) { return } var el = self.$().find("li[data-resource='"+key+"']"); - self.updateResourceElement(el, usage); + el.removeClass("green yellow red"); + el.addClass(usage.cls); + _.each(self.resources[key].projects_list, function(project){ + var project_el = el.find(".project-" + project.id); + if (project_el.length === 0) { + self.renderResourceProjects(self.container, self.resources[key]); + } else { + self.updateResourceElement(project_el, project.usage, project); + } + }); + var project_ids = _.keys(self.project_quotas); + _.each(el.find(".resource-bar.project"), function(el) { + if (project_ids.indexOf($(el).data("project")) == -1) { + self.renderResourceProjects(self.container, self.resources[key]); + } + }) }) }, updateResourceElement: function(el, usage) { + var bar_el = el.find(".bar span.member"); + var bar_value = el.find(".bar .value"); + var project_bar_el = el.find(".bar span.project"); + el.find(".currValue").text(usage.curr); el.find(".maxValue").text(usage.max); - el.find(".bar span").css({width:usage.perc+"%"}); - el.find(".bar .value").text(usage.perc+"%"); - var left = usage.label_left == 'auto' ? - usage.label_left : usage.label_left + "%"; - el.find(".bar .value").css({left:left}); - el.find(".bar .value").css({color:usage.label_color}); + el.find(".leftValue").text(usage.left); + bar_el.css({width:usage.ratio + "%"}); + project_bar_el.css({width: usage.user_project_ratio + "%"}); + + bar_value.text(usage.ratio+"%"); + var left = usage.label_left; + bar_value.css({left:left}); + bar_value.css({color:usage.label_color}); el.removeClass("green yellow red"); el.addClass(usage.cls); + if (el.hasClass("summary")) { + el.parent().removeClass("green yellow red"); + el.parent().addClass(usage.cls); + }; + + el.find("i.warn").removeClass("visible"); + el.find("i.warn-msg").text(usage.project_warn_msg); + if (usage.project_warn) { + el.find("i.warn").addClass("visible"); + } else { + el.find("i.warn-msg").removeClass("hovered"); + } }, - getUsage: function(resource_name) { - var resource = this.quotas[resource_name]; + getUsage: function(resource_name, quotas) { + var resource = quotas ? quotas[resource_name] : this.quotas[resource_name]; var resource_meta = this.resources[resource_name]; if (!resource_meta) { return } - var value, limit, percentage; + var value, limit, ratio, cls, label_left, label_col, left, + project_value, project_limit, project_ratio, project_cls, project_left, + user_project_left; limit = resource.limit; value = resource.usage; - if (value < 0 ) { value = 0 } - - percentage = (value/limit) * 100; - if (value == 0) { percentage = 0 } - if (value > limit) { - percentage = 100; + left = limit - value; + project_limit = resource.project_limit; + project_value = resource.project_usage; + project_left = project_limit - project_value; + user_project_left = 0; + user_project_ratio = 0; + + if (left > project_left) { + user_project_left = left - project_left; + } + + if (user_project_left < 0) { user_project_left = 0; } + if (left < 0) { left = 0; } + if (project_left < 0) { project_left = 0; } + if (value < 0) { value = 0; } + if (project_value < 0) { project_value = 0; } + + ratio = (value/limit) * 100; + if (value == 0) { ratio = 0; } + if (value >= limit) { + ratio = 100; + } + + if (left && limit && user_project_left) { + user_project_ratio = (user_project_left / limit) * 100; + if (user_project_ratio > ratio) { + user_project_ratio = 100 - ratio; + } + user_project_ratio = parseInt(user_project_ratio); + console.log("USER PROJECT RATIO", user_project_ratio); + } + + project_ratio = (project_value/project_limit) * 100; + if (project_value == 0) { project_ratio = 0 } + if (project_value >= project_limit) { + project_ratio = 100; } if (resource_meta.unit == 'bytes') { value = humanize.filesize(value); limit = humanize.filesize(limit); + left = humanize.filesize(left); + user_project_left = humanize.filesize(user_project_left); + project_value = humanize.filesize(value); + project_limit = humanize.filesize(limit); + project_left = humanize.filesize(project_left); } - var cls = 'green'; + cls = 'green'; + project_cls = 'green'; _.each(this.usage_cls_map, function(ucls, u){ - if (percentage >= u) { - cls = ucls + if (ratio >= u) { + cls = ucls; } - }) - - var label_left = percentage >= 30 ? percentage - 17 : 'auto'; - var label_col = label_left == 'auto' ? 'inherit' : '#fff'; - percentage = humanize.numberFormat(percentage, 0); - qdata = {'curr': value, 'max': limit, 'perc': percentage, 'cls': cls, - 'label_left': label_left, 'label_color': label_col} + if (project_ratio >= u) { + project_cls = ucls; + } + }); + + var span = (ratio + '').length >= 3 ? 15 : 12; + label_left = ratio >= 30 ? ratio - span : ratio; + label_col = label_left == ratio ? 'inherit' : '#fff'; + if (label_left != 'auto') { label_left = label_left + "%"; } + + ratio = humanize.numberFormat(ratio , 0); + project_ratio = humanize.numberFormat(project_ratio , 0); + + var project_warn = user_project_ratio && ratio != 100 ? true : false; + var project_warn_msg = "WARNING: " + project_left + " left in project."; + + qdata = { + 'curr': value, + 'max': limit, + 'left': left, + 'ratio': ratio, + 'cls': cls, + 'label_left': label_left, + 'label_color': label_col, + 'project_curr': project_value, + 'project_max': project_limit, + 'project_ratio': project_ratio, + 'project_cls': project_cls, + 'project_warn': project_warn, + 'project_warn_msg': project_warn_msg, + 'user_project_left': user_project_left, + 'user_project_ratio': user_project_ratio + }; _.extend(qdata, resource); - return qdata + return qdata; }, - - setQuotas: function(data) { + + setQuotas: function(data, update_projects) { LOG("Set quotas", data); + this.last_quota_received = data; + var project_uuids = _.keys(data); + var self = this; - this.quotas = data; + var sums = {}; + _.each(data, function(quotas, project_uuid) { + _.each(quotas, function(values, qname) { + if (!sums[qname]) { + sums[qname] = {}; + } + var qitem = sums[qname]; + _.each(values, function(value, param) { + var current = qitem[param]; + qitem[param] = current ? qitem[param] + value : value; + }, this); + }); + }, this); + + this.project_quotas = data; + this.quotas = sums; _.each(this.quotas, function(v, k) { var r = self.resources[k]; var usage = self.getUsage(k); @@ -204,12 +403,77 @@ _.extend(UsageView.prototype, { if (!self.resources_ordered[r.index]) { return } self.resources_ordered[r.index].usage = usage; }); + + var active_project_uuids = _.keys(this.project_quotas); + var project_change = false; + _.each(this.project_quotas, function(resources, uuid) { + if (project_change) { return } + _.each(resources, function(v, k){ + if (project_change) { return } + + if (!self.resources[k]) { return } + if (!self.resources[k].projects) { + self.resources[k].projects = {}; + self.resources[k].projects_list = []; + } + + var resource = self.resources[k]; + var project_usage = self.getUsage(k, resources); + var resource_projects_list = resource.projects_list; + var resource_projects = resource.projects; + + if (!self.projects[uuid]) { + self.getProjects(function(data) { + self.updateProjects(data); + self.setQuotas(self.last_quota_received); + self.updateLayout(); + }); + + project_change = true; + return; + } + if (!project_usage) { return; } + + if (!resource.projects[uuid]) { + resource.projects[uuid] = _.clone(self.projects[uuid]); + if (self.projects[uuid].system_project) { + resource.projects[uuid].display_name = 'System project'; + } + } + + var resource_project = resource.projects[uuid]; + resource_project.usage = project_usage; + + if (resource_project.index === undefined) { + resource_project.index = resource_projects_list.length; + resource_projects_list.push(resource_project); + } else { + resource_projects_list[resource_project.index] = resource_project; + } + + resource.projects_list.sort(function(p1, p2) { + if (p1.system_project && !p2.system_project) { return -1; } + if (!p1.system_project && p2.system_project) { return 1; } + return -1; + }); + + // update indexes + _.each(resource.projects_list, function(p, index) { + if (!_.contains(active_project_uuids, p.id)) { + p.not_a_member = true; + } else { + p.not_a_member = false; + } + p.index = index; + }); + }); + }); }, - _ajaxOptions: function() { + _ajaxOptions: function(url) { var token = $.cookie(this.settings.cookie_name).split("|")[1]; return { - 'url': this.url, + 'url': url || this.url, 'headers': { 'X-Auth-Token': token }, @@ -220,12 +484,20 @@ _.extend(UsageView.prototype, { LOG("Updating quotas"); var self = this; this.getQuotas(function(data){ - self.setQuotas(data.system); + self.setQuotas(data, true); self.updateLayout(); }) }, + getProjects: function(callback) { + var options = this._ajaxOptions(this.projects_url); + options.success = callback; + LOG("Calling projects API", options); + $.ajax(options); + }, + getQuotas: function(callback) { + if (this.updating_projects) { return } var options = this._ajaxOptions(); options.success = callback; LOG("Calling quotas API", options); diff --git a/snf-astakos-app/astakos/im/tables.py b/snf-astakos-app/astakos/im/tables.py index 8d6b100f94ef807856f509bdad5071f6d88e3e90..4c3574bac30b707cfb7a01e5ef99fb5754a45fb0 100644 --- a/snf-astakos-app/astakos/im/tables.py +++ b/snf-astakos-app/astakos/im/tables.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.utils.translation import ugettext as _ from django.utils.safestring import mark_safe @@ -40,7 +22,7 @@ from django_tables2 import A import django_tables2 as tables from astakos.im.models import * -from astakos.im.templatetags.filters import truncatename +from astakos.im.util import truncatename from astakos.im.functions import can_join_request, membership_allowed_actions @@ -54,7 +36,11 @@ class LinkColumn(tables.LinkColumn): self.append = kwargs.pop('append', None) super(LinkColumn, self).__init__(*args, **kwargs) + def get_value(self, value, record, bound_column): + return value + def render(self, value, record, bound_column): + value = self.get_value(value, record, bound_column) link = super(LinkColumn, self).render(value, record, bound_column) extra = '' if self.append: @@ -70,6 +56,21 @@ class LinkColumn(tables.LinkColumn): return super(LinkColumn, self).render_link(uri, text, attrs) +class ProjectNameColumn(LinkColumn): + + def get_value(self, value, record, bound_column): + # inspect columnt context to resolve user, fallback to value + # if failed + try: + table = getattr(bound_column, 'table', None) + if table: + user = getattr(table, 'request').user + value = record.display_name_for_user(user) + except: + pass + return value + + # Helper columns class RichLinkColumn(tables.TemplateColumn): @@ -187,19 +188,19 @@ def action_extra_context(project, table, self): allowed = membership_allowed_actions(membership, user) if 'leave' in allowed: url = reverse('astakos.im.views.project_leave', - args=(membership.id,)) + args=(membership.project.uuid,)) action = _('Leave') confirm = True prompt = _('Are you sure you want to leave from the project?') elif 'cancel' in allowed: - url = reverse('astakos.im.views.project_cancel_member', - args=(membership.id,)) + url = reverse('project_cancel_join', + args=(project.uuid,)) action = _('Cancel') confirm = True prompt = _('Are you sure you want to cancel the join request?') if can_join_request(project, user, membership): - url = reverse('astakos.im.views.project_join', args=(project.id,)) + url = reverse('project_join', args=(project.uuid,)) action = _('Join') confirm = True prompt = _('Are you sure you want to join this project?') @@ -225,9 +226,9 @@ class UserTable(tables.Table): def project_name_append(project, column): - pending_apps = column.table.pending_apps - app = pending_apps.get(project.id) - if app and app.id != project.application_id: + if project.state != project.UNINITIALIZED and \ + project.last_application is not None and \ + project.last_application.state == ProjectApplication.PENDING: return mark_safe("<br /><i class='tiny'>%s</i>" % _('modifications pending')) return u'' @@ -236,38 +237,51 @@ def project_name_append(project, column): # Table classes class UserProjectsTable(UserTable): + _links = [ + {'url': '?show_base=1', 'label': 'Show system projects'}, + {'url': '?', 'label': 'Hide system projects'} + ] + links = [] + def __init__(self, *args, **kwargs): + self.request = kwargs.pop('request', None) + if self.request and self.request.user.is_project_admin(): + self.links = [self._links[0]] + if self.request and self.request.GET.get('show_base', False): + self.links = [self._links[1]] self.pending_apps = kwargs.pop('pending_apps') self.memberships = kwargs.pop('memberships') self.accepted = kwargs.pop('accepted') self.requested = kwargs.pop('requested') super(UserProjectsTable, self).__init__(*args, **kwargs) + if self.request and self.request.user.is_project_admin(): + self.caption = _("Projects") + owner_col = dict(self.columns.items())['owner'] + setattr(owner_col.column, 'accessor', 'owner.realname_with_email') + caption = _('My projects') - name = LinkColumn('astakos.im.views.project_detail', + name = ProjectNameColumn('project_detail', coerce=lambda x: truncatename(x, 25), append=project_name_append, - args=(A('id'),), + args=(A('uuid'),), orderable=False, - accessor='application.name') - - issue_date = tables.DateColumn(verbose_name=_('Application'), - format=DEFAULT_DATE_FORMAT, - orderable=False, - accessor='application.issue_date') - start_date = tables.DateColumn(format=DEFAULT_DATE_FORMAT, - orderable=False, - accessor='application.start_date') + accessor='display_name') + + creation_date = tables.DateColumn(verbose_name=_('Application'), + format=DEFAULT_DATE_FORMAT, + orderable=False, + accessor='creation_date') end_date = tables.DateColumn(verbose_name=_('Expiration'), format=DEFAULT_DATE_FORMAT, orderable=False, - accessor='application.end_date') + accessor='end_date') members_count_f = tables.Column(verbose_name=_("Members"), empty_values=(), orderable=False) owner = tables.Column(verbose_name=_("Owner"), - accessor='application.owner') + accessor='owner.realname') membership_status = tables.Column(verbose_name=_("Status"), empty_values=(), orderable=False) @@ -294,7 +308,7 @@ class UserProjectsTable(UserTable): if c > 0: pending_members_url = reverse( 'project_pending_members', - kwargs={'chain_id': record.id}) + kwargs={'project_uuid': record.uuid}) pending_members = "<i class='tiny'> - %d %s</i>" % ( c, _('pending')) @@ -307,7 +321,7 @@ class UserProjectsTable(UserTable): (pending_members_url, c, _('pending'))) append = mark_safe(pending_members) members_url = reverse('project_approved_members', - kwargs={'chain_id': record.id}) + kwargs={'project_uuid': record.uuid}) members_count = len(self.accepted.get(project.id, [])) if self.user.owns_project(record) or self.user.is_project_admin(): members_count = '<a href="%s">%d</a>' % (members_url, @@ -315,12 +329,11 @@ class UserProjectsTable(UserTable): return mark_safe(str(members_count) + append) class Meta: - sequence = ('name', 'membership_status', 'owner', 'issue_date', + sequence = ('name', 'membership_status', 'owner', 'creation_date', 'end_date', 'members_count_f', 'project_action') attrs = {'id': 'projects-list', 'class': 'my-projects alt-style'} template = "im/table_render.html" empty_text = _('No projects') - exclude = ('start_date', ) def member_action_extra_context(membership, table, col): @@ -332,21 +345,22 @@ def member_action_extra_context(membership, table, col): return context if membership.state == ProjectMembership.REQUESTED: - urls = ['astakos.im.views.project_reject_member', - 'astakos.im.views.project_accept_member'] + urls = ['project_reject_member', + 'project_accept_member'] actions = [_('Reject'), _('Accept')] prompts = [_('Are you sure you want to reject this member?'), _('Are you sure you want to accept this member?')] confirms = [True, True] if membership.state in ProjectMembership.ACCEPTED_STATES: - urls = ['astakos.im.views.project_remove_member'] + urls = ['project_remove_member'] actions = [_('Remove')] prompts = [_('Are you sure you want to remove this member?')] confirms = [True, True] for i, url in enumerate(urls): - context.append(dict(url=reverse(url, args=(membership.pk,)), + context.append(dict(url=reverse(url, args=(membership.project.uuid, + membership.pk,)), action=actions[i], prompt=prompts[i], confirm=confirms[i])) return context diff --git a/snf-astakos-app/astakos/im/templates/im/base.html b/snf-astakos-app/astakos/im/templates/im/base.html index 8e2a46f5030d62c90d3c3b25b8fc8faea825574d..e2d2179f76671e70b1a3a5dc1a5f20c715e47e9a 100644 --- a/snf-astakos-app/astakos/im/templates/im/base.html +++ b/snf-astakos-app/astakos/im/templates/im/base.html @@ -27,8 +27,10 @@ {% endblock page.favicons %} {% block page.css %} - - <link href='https://fonts.googleapis.com/css?family=Open+Sans:400,600,700&subset=latin,greek-ext,greek' rel='stylesheet' type='text/css'> + + {% for url in BRANDING_FONTS_CSS_URLS %} + <link href="{{ url }}" rel="stylesheet" type="text/css" > + {% endfor %} <link rel="stylesheet" type="text/css" href="{{ IM_STATIC_URL }}css/global.css"> <link rel="stylesheet" type="text/css" href="{{ IM_STATIC_URL }}css/print.css" media="print"> <!--[if lte IE 7]> @@ -78,6 +80,7 @@ {% if CLOUDBAR_ACTIVE %} {{ CLOUDBAR_CODE }} + <script>window.CLOUDBAR_INCLUDE_FONTS = false;</script> {% endif %} </head> diff --git a/snf-astakos-app/astakos/im/templates/im/plain_email.txt b/snf-astakos-app/astakos/im/templates/im/plain_email.txt new file mode 100644 index 0000000000000000000000000000000000000000..64b4b79b8fc997058727f8f65c1e00470466bdb4 --- /dev/null +++ b/snf-astakos-app/astakos/im/templates/im/plain_email.txt @@ -0,0 +1,11 @@ +{% extends "im/email.txt" %} + +{% block content %} +Dear {{ user.realname }}, + +{{ text }} + +If you did not sign up for this account you can ignore this email. +{% endblock %} + +{% block note %} {% endblock%} diff --git a/snf-astakos-app/astakos/im/templates/im/projects/_form_field.html b/snf-astakos-app/astakos/im/templates/im/projects/_form_field.html new file mode 100644 index 0000000000000000000000000000000000000000..850cbf914ff824fa7ca72fac028a0f85c8175934 --- /dev/null +++ b/snf-astakos-app/astakos/im/templates/im/projects/_form_field.html @@ -0,0 +1,18 @@ +{% if field.name in filter_fields %} +<div class="form-row + {% if field.errors|length %}with-errors{% endif %} + {% if field.is_hidden %}with-hidden{% endif %}"> + {{ field.errors }} + <p class="clearfix {% if field.blank %}required{% endif %}"> + {{ field.label_tag }} + {{ field|safe }} + <span class="extra-img"> </span> + {% if field.help_text %} + <span class="info"> + <em>more info</em> + <span>{{ field.help_text|safe }}</span> + </span> + {% endif %} + </p> + </div> +{% endif %} diff --git a/snf-astakos-app/astakos/im/templates/im/projects/_form_resource_field.html b/snf-astakos-app/astakos/im/templates/im/projects/_form_resource_field.html new file mode 100644 index 0000000000000000000000000000000000000000..b552502c1cfbf19aa3fdfe5baa8e1f99a98ab9fa --- /dev/null +++ b/snf-astakos-app/astakos/im/templates/im/projects/_form_resource_field.html @@ -0,0 +1,101 @@ +{% load astakos_tags filters %} +<fieldset class="quota"> +<legend> +{% if rinfo.is_abbreviation %} + {{ rinfo.verbose_name|upper }} +{% else %} + {{ rinfo.verbose_name|capfirst }} +{% endif %} +<span class="info"> +<em>more info</em> +<span>{{ rinfo.help_text }}</span> +</span> +</legend> + +<div class="form-row resource-pair clearfix"> +<div class="form-row resource-col"> + <p class="clearfix"> + <label for="id_{{resource}}_p_uplimit_proxy" > + Total + </label> + <input type="text" + id="id_{{resource}}_p_uplimit_proxy" + name="{{ resource }}_p_uplimit_proxy" + placeholder="{{ rinfo.placeholder }}" + {% with value=object|get_member_resource_grant_value:resource %} + {% with resource|add:'_m_uplimit' as input_value %} + {% if request.POST and value != request.POST|lookup:input_value %} + data-changed="true" + {% endif %} + {% endwith %} + {% endwith %} + {% if rinfo.unit == 'bytes' %} + class="dehumanize" + {% endif %} + {% if request.POST %} + {% with resource|add:'_p_uplimit' as input_value %} + value="{{ request.POST|lookup:input_value }}" + data-value="{{ request.POST|lookup:input_value }}" + {% endwith %} + {% else %} + {% with value=object|get_project_resource_grant_value:resource %} + {% if value %} + value="{{ value|inf_value_display }}" + data-value="{{ value|inf_value_display }}" + {% else %} + value="" + {% endif %} + {% endwith %} + {% endif %} + autocomplete="off"> + <span class="extra-img"> </span> + <span class="info"><em>more info</em> + <span>{{ rinfo.help_text_input_total }}</span></span> + </p> + <p class="error-msg">Invalid format</p> + <p class="msg"></p> +</div> +<div class="form-row resource-col"> + <p class="clearfix"> + <label for="id_{{resource}}_m_uplimit_proxy" > + Max per member {{rdata.pluralized_display_name}} + </label> + <input type="text" + {% with value=object|get_member_resource_grant_value:resource %} + {% with resource|add:'_m_uplimit' as input_value %} + {% if request.POST and value != request.POST|lookup:input_value %} + data-changed="true" + {% endif %} + {% endwith %} + {% endwith %} + id="id_{{resource}}_m_uplimit_proxy" + name="{{ resource }}_m_uplimit_proxy" + placeholder="{{ rinfo.placeholder }}" + {% if rinfo.unit == 'bytes' %} + class="dehumanize" + {% endif %} + {% if request.POST %} + {% with resource|add:'_m_uplimit' as input_value %} + value="{{ request.POST|lookup:input_value }}" + data-value="{{ request.POST|lookup:input_value }}" + {% endwith %} + {% else %} + {% with value=object|get_member_resource_grant_value:resource %} + {% if value %} + value="{{ value|inf_value_display }}" + data-value="{{ value|inf_value_display }}" + {% else %} + value = "" + {% endif %} + {% endwith %} + {% endif %} + autocomplete="off"> + <span class="extra-img"> </span> + <span class="info"><em>more info</em> + <span>{{ rinfo.help_text_input_each }}</span></span> + </p> + <p class="error-msg">Invalid format</p> + <p class="msg"></p> +</div> +</div> +</fieldset> diff --git a/snf-astakos-app/astakos/im/templates/im/projects/_form_resource_groups_buttons.html b/snf-astakos-app/astakos/im/templates/im/projects/_form_resource_groups_buttons.html new file mode 100644 index 0000000000000000000000000000000000000000..8a36c8611e28cd0aeb8054c90c6bf83a65bfb5ae --- /dev/null +++ b/snf-astakos-app/astakos/im/templates/im/projects/_form_resource_groups_buttons.html @@ -0,0 +1,18 @@ +{% load astakos_tags filters %} +<ul class="clearfix"> + {% with object|selected_resource_groups as selected_groups %} + {% for group, info in resource_groups_dict.items %} + <li> + <a href="#{{group}}" id="group_{{group}}" + {% if group in selected_groups %}class="selected"{% endif %}> + <img src="{{ IM_STATIC_URL }}images/create-{{group}}.png" + alt="{{ group }}" /> + </a> + <input type="hidden" name="is_selected_{{group}}" + id="proxy_id_is_selected_{{group}}" + {% if group in selected_groups %}value="1"{% else %}value="0"{% endif %}/> + <p class="msg">{{ info.help_text }}</p> + </li> + {% endfor %} + {% endwith %} +</ul> diff --git a/snf-astakos-app/astakos/im/templates/im/projects/_form_resources_fields.html b/snf-astakos-app/astakos/im/templates/im/projects/_form_resources_fields.html new file mode 100644 index 0000000000000000000000000000000000000000..b9b5827cdedccca3c3fbf1edd2871daade734436 --- /dev/null +++ b/snf-astakos-app/astakos/im/templates/im/projects/_form_resources_fields.html @@ -0,0 +1,32 @@ +{% load astakos_tags filters %} + +{% for group, resources in resource_catalog_dict.items %} +{% for resource, rinfo in resources.items %} +{% with type="hidden" %} + {% with value=object|get_member_resource_grant_value:resource %} + <input type="{{ type }}" + id="id_{{resource}}_m_uplimit" + name="{{resource}}_m_uplimit" + {% if value %}value="{{value|inf_display}}"{% endif %} /> + {% endwith %} + {% with value=object|get_project_resource_grant_value:resource %} + <input type="{{ type }}" + id="id_{{resource}}_p_uplimit" + name="{{resource}}_p_uplimit" + {% if value %}value="{{value|inf_display}}"{% endif %} /> + {% endwith %} + {% endwith %} + {% endfor %} +{% endfor %} + +<div class="visible"> </div> +<div class="not-visible"> +{% for group, resources in resource_catalog_dict.items %} +<div class="group group_{{group}}" id="{{ group }}"> + <a href="#icons" class="delete">X remove resource</a> + {% for resource, rinfo in resources.items %} + {% include "im/projects/_form_resource_field.html" %} + {% endfor %} +</div> +{% endfor %} +</div> diff --git a/snf-astakos-app/astakos/im/templates/im/projects/_project_application_detail_actions.html b/snf-astakos-app/astakos/im/templates/im/projects/_project_application_detail_actions.html new file mode 100644 index 0000000000000000000000000000000000000000..7352b0039ea88e77265e3441d686563aa6ca6605 --- /dev/null +++ b/snf-astakos-app/astakos/im/templates/im/projects/_project_application_detail_actions.html @@ -0,0 +1,30 @@ +{% load astakos_tags i18n %} + +<!-- make room for buttons --> +{% if owner_mode or admin_mode %} +<br /> +{% endif %} + +<div class="project-actions"> +{% if owner_mode or admin_mode %} + {% if project.is_initialized %} + {% if object.can_approve and owner_mode %} + {% confirm_link "CANCEL PROJECT MODIFICATION" "project_modification_cancel" "project_app_cancel" "project.uuid,object.pk" "" "OK" %} + {% endif %} + {% else %} + {% if object.can_approve and owner_mode %} + {% confirm_link "CANCEL PROJECT APPLICATION" "project_app_cancel" "project_app_cancel" "project.uuid,object.pk" "" "OK" %} + {% endif %} + {% endif %} + {% if object.can_dismiss %} + {% confirm_link "DISMISS" "project_app_dismiss" "project_app_dismiss" "project.uuid,object.pk" %} + {% endif %} +{% endif %} + +{% if admin_mode %} + {% if object.can_approve %} + {% confirm_link "APPROVE" "project_app_approve" "project_app_approve" "project.uuid,object.pk" "" "OK" %} + {% confirm_link "DENY" "project_app_deny" "project_app_deny" "project.uuid,object.pk" %} + {% endif %} +{% endif %} +</div> diff --git a/snf-astakos-app/astakos/im/templates/im/projects/_project_detail_actions.html b/snf-astakos-app/astakos/im/templates/im/projects/_project_detail_actions.html index c136b453f8ee4efba898c9b57dbb5940e8bca9b0..c9f34d3052459bab4292446ffd54d8520f67e497 100644 --- a/snf-astakos-app/astakos/im/templates/im/projects/_project_detail_actions.html +++ b/snf-astakos-app/astakos/im/templates/im/projects/_project_detail_actions.html @@ -2,49 +2,27 @@ <!-- make room for buttons --> {% if owner_mode or admin_mode or can_join_request or can_leave_request %} - <br /> +<br /> {% endif %} <div class="project-actions"> {% if owner_mode or admin_mode %} -<a class="owner-action" href="{% url astakos.im.views.project_modify object.pk %}">MODIFY</a> - -{% if owner_mode %} -{% with project.last_pending_application as last_pending %} -{% if last_pending != None %} -{% if project.is_initialized %} -- {% confirm_link "CANCEL PROJECT MODIFICATION" "project_modification_cancel" "project_app_cancel" last_pending.pk "" "OK" %} -{% else %} -- {% confirm_link "CANCEL PROJECT APPLICATION" "project_app_cancel" "project_app_cancel" last_pending.pk "" "OK" %} -{% endif %} -{% endif %} -{% endwith %} -{% endif %} - -{% if admin_mode %} -{% if object.can_approve %} -- {% confirm_link "APPROVE" "project_app_approve" "project_app_approve" object.pk "" "OK" %} -- {% confirm_link "DENY" "project_app_deny" "project_app_deny" object.pk %} -{% endif %} -{% endif %} - -{% if owner_mode %} -{% if object.can_dismiss %} -- {% confirm_link "DISMISS" "project_app_dismiss" "project_app_dismiss" object.pk %} -{% endif %} -{% endif %} - -<!-- only one is possible, perhaps add cancel button too --> -{% if can_join_request or can_leave_request %} -- +{% if object.can_modify %} +<div class="msg-wrap inline"> + <a class="" href="{% url project_modify project_uuid=object.uuid %}">MODIFY</a> +</div> {% endif %} {% endif %} {% if can_join_request %} -{% confirm_link "JOIN" "project_join" "project_join" project.pk %} +{% confirm_link "JOIN" "project_join" "project_join" project.uuid %} {% endif %} {% if can_leave_request %} -{% confirm_link "LEAVE" "project_leave" "project_leave" membership_id %} +{% confirm_link "LEAVE" "project_leave" "project_leave" project.uuid %} +{% endif %} + +{% if can_cancel_join_request %} +{% confirm_link "CANCEL JOIN REQUEST" "project_cancel_member" "project_cancel_join" project.uuid "" "OK" %} {% endif %} </div> diff --git a/snf-astakos-app/astakos/im/templates/im/projects/addmembers_form.html b/snf-astakos-app/astakos/im/templates/im/projects/addmembers_form.html index 1834cbfe7df7705e031f901f30e141833b0bb596..8fc519a656033ffa37e84a6bd2a4f70fca6c217c 100644 --- a/snf-astakos-app/astakos/im/templates/im/projects/addmembers_form.html +++ b/snf-astakos-app/astakos/im/templates/im/projects/addmembers_form.html @@ -1,4 +1,4 @@ -<form action="{% url project_detail object.chain_id %}#members-table" +<form action="{% url project_detail project.uuid %}#members-table" method="post" class="withlabels upperlabels" id="members-table" > {% csrf_token %} {% with addmembers_form as form %} diff --git a/snf-astakos-app/astakos/im/templates/im/projects/project_application_detail.html b/snf-astakos-app/astakos/im/templates/im/projects/project_application_detail.html new file mode 100644 index 0000000000000000000000000000000000000000..351ab5a518ffb72391877925a117fd9e45743491 --- /dev/null +++ b/snf-astakos-app/astakos/im/templates/im/projects/project_application_detail.html @@ -0,0 +1,90 @@ +{% extends "im/projects/project_detail.html" %} +{% load filters %} + +{% block project.title %} +{% if is_modification %} +<a class="back-to-action" href="{% url project_detail project.uuid %}">< back to project</a> +{% endif %} +{{ project|project_name_for_user:request.user }} +{% if is_modification %} +<span class="prefix"> MODIFICATION</span> +{% endif %} +{% endblock %} + +{% block object.actions %} +{% include "im/projects/_project_application_detail_actions.html" %} +{% endblock %} + +{% block object.status %} +{% if project.is_initialized %} +{{ object.state_display|upper}} +{% else %} +PROJECT APPLICATION {{ object.state_display|upper}} +{% endif %} +{% endblock %} + + +{% block page.project_details %} +{% if application.state == object.DENIED and application.response %} +<dt>Reason denied</dt> +<dd><em>{{ application.response }}</em></dd> +{% endif %} +<dt>Name</dt> +<dd> +{{ application|display_modification_param:"name" }} +</dd> +<dt>Homepage url</dt> +<dd> +{{ application|display_modification_param:"homepage" }} +</dd> +<dt>Description</dt> +<dd> +{{ application|display_modification_param:"description" }} +</dd> + +{% if owner_mode or admin_mode and not is_modification %} +<dt>Creation date</dt> +<dd>{{project.creation_date|date:"d/m/Y"}} </dd> +{% endif %} + +<dt>End Date</dt> +<dd>{{ application|display_date_modification_param:"end_date,d/m/Y" }} </dd> +<dt>Owner</dt> +{% if admin_mode %} +{% if owner_mode %} +<dd>{{ application|display_modification_param:"owner,owner_owner" }} </dd> +{% else %} +<dd>{{ application|display_modification_param:"owner,owner_admin" }} </dd> +{% endif %} +{% else %} +{% if owner_mode %} +<dd>{{ application|display_modification_param:"owner,owner_owner" }} </dd> +{% else %} +<dd>{{ application|display_modification_param:"owner,owner" }} </dd> +{% endif %} +{% endif %} + +<dt>Applicant</dt> +<dd>{{ application.applicant }}</dd> + +</dl> +</div> +<div class="full-dotted"> +<h3>MEMBERSHIP OPTIONS</h3> +<dl class="alt-style"> +<dt>Member join policy</dt> +<dd>{{ application|display_modification_param:"member_join_policy" }} </dd> +<dt>Member leave policy</dt> +<dd>{{ application|display_modification_param:"member_leave_policy" }} </dd> +<dt>Total number of members</dt> +<dd>{{ application|display_modification_param_diff:"limit_on_members_number" }} </dd> +{% endblock %} + +{% block page.members %}{% endblock %} +{% block page.resources_heading %} +{% if is_modification %} +MODIFIED RESOURCES +{% else %} +{{ block.super }} +{% endif %} +{% endblock %} diff --git a/snf-astakos-app/astakos/im/templates/im/projects/project_approval_notification.txt b/snf-astakos-app/astakos/im/templates/im/projects/project_approval_notification.txt index 3585789c74a63ef55276699073c238a082e944d2..239a39c623e00bfca7241c46b7f295555ff1ff96 100644 --- a/snf-astakos-app/astakos/im/templates/im/projects/project_approval_notification.txt +++ b/snf-astakos-app/astakos/im/templates/im/projects/project_approval_notification.txt @@ -1,5 +1,5 @@ {% extends "im/email.txt" %} {% block content %} -Your project application request ({{object.name}}) has been approved. -{% endblock %} \ No newline at end of file +Your application for project {{object.chain.realname}} has been approved. +{% endblock %} diff --git a/snf-astakos-app/astakos/im/templates/im/projects/project_creation_notification.txt b/snf-astakos-app/astakos/im/templates/im/projects/project_creation_notification.txt index 2c150e5d50234b043ddb516ba19929955558fce3..31297b3a68f0db3fcf939201ee9530ada8163f6e 100644 --- a/snf-astakos-app/astakos/im/templates/im/projects/project_creation_notification.txt +++ b/snf-astakos-app/astakos/im/templates/im/projects/project_creation_notification.txt @@ -1,21 +1,22 @@ {% extends "im/email.txt" %} {% load filters %} - + {% block content %} -The following project application has been submitted: +The following application for a new project has been submitted: Id: {{object.id}} -Project: {{object.chain_id}} -Name: {{object.name}} +Project: {{object.chain.uuid}} +Applicant: {{object.applicant}} Issue date: {{object.issue_date|date:"d/m/Y"}} + +Name: {{object.name}} +Owner: {{object.owner}} Start date: {{object.start_date|date:"d/m/Y"}} End date: {{object.end_date|date:"d/m/Y"}} Member Join Policy: {{object.member_join_policy_display}} Member Leave Policy: {{object.member_leave_policy_display}} -Owner: {{object.owner}} -Applicant: {{object.applicant}} -Maximum participant number: {{object.limit_on_members_number}} -Policies: +Max members: {{object.limit_on_members_number|format_inf}} +Quota limits: {% for rp in object.projectresourcegrant_set.all %} {{rp}} {% endfor %} diff --git a/snf-astakos-app/astakos/im/templates/im/projects/project_denial_notification.txt b/snf-astakos-app/astakos/im/templates/im/projects/project_denial_notification.txt index a484ea66ca5d7145f02a40a19e27c6ccc0f4b126..b546e4e9d4ebf91026f21ec1fd0063e8616904a0 100644 --- a/snf-astakos-app/astakos/im/templates/im/projects/project_denial_notification.txt +++ b/snf-astakos-app/astakos/im/templates/im/projects/project_denial_notification.txt @@ -1,7 +1,7 @@ {% extends "im/email.txt" %} {% block content %} -Your project application request ({{object.name}}) has been denied. +Your application for project {{object.chain.realname}} has been denied. Comment: {{object.response}} diff --git a/snf-astakos-app/astakos/im/templates/im/projects/project_detail.html b/snf-astakos-app/astakos/im/templates/im/projects/project_detail.html index 0f02113ae3c3b08c1f4f208f6b5764ee473abd02..3d2e6fab239fa8577e0e39eb60a89e41b6087d9b 100644 --- a/snf-astakos-app/astakos/im/templates/im/projects/project_detail.html +++ b/snf-astakos-app/astakos/im/templates/im/projects/project_detail.html @@ -3,109 +3,137 @@ {% load astakos_tags filters django_tables2 %} {% block page.body %} -{% with object.chain as project %} <div class="projects"> <h2> - <em> - {% if owner_mode or admin_mode %} - {% if project_view %} - PROJECT {{ project.state_display|upper }} - {% with project.last_pending_modification as last_pending %} - {% if last_pending != None %} - - <a href="{% url astakos.im.views.project_app last_pending.pk %}"> - MODIFICATION PENDING</a> - {% else %} - <!-- note that pending modifications have priority --> - {% if object.has_denied_modifications %} - - <a href="{% url astakos.im.views.project_app object.last_denied.pk %}"> - MODIFICATION DENIED</a> - {% endif %} - {% endif %} - {% endwith %} - {% else %} - <!-- application view --> - PROJECT {% if object.is_modification %} MODIFICATION {% endif %} - {{ object.state_display|upper }} - {% endif %} + <em> + {% block object.status %} + PROJECT {{ object.state_display|upper}} + {% endblock %} + {% if owner_mode or admin_mode %} + {% block object.modification_status %} + {% if last_app|is_pending_app %} + {% if applicant_mode or admin_mode %} + - <a href="{% url project_app project.uuid last_app.pk %}">PENDING MODIFICATION</a> {% else %} - <!-- third user --> - <!-- assert in project view --> - {% if project.is_deactivated %} - PROJECT {{ project.state_display|upper }} - - {% endif %} - {{ mem_display|upper }} + - PENDING MODIFICATION + {% endif %} + {% endif %} + {% if last_app|is_denied_app %} + - <a href="{% url project_app project.uuid last_app.pk %}">DENIED MODIFICATION</a> + {% endif %} + {% endblock %} {% endif %} </em> <span> - {% if not project_view %} - <!-- owner mode only assumed --> - {% if object.is_modification %} - <span class="extratitle">MODIFICATION OF </span> - {% endif %} - {% endif %} - {{ object.name|upper }} + {% block project.title %} + {{ object|project_name_for_user:request.user }} + {% endblock %} </span> - - {% block project.actions %} - {% include "im/projects/_project_detail_actions.html" %} + + {% block object.actions %} + {% include "im/projects/_project_detail_actions.html" %} {% endblock %} </h2> {% block inner_project %} + {% if not project.is_base %} <div class="full-dotted"> <h3>PROJECT DETAILS</h3> <dl class="alt-style"> + {% block page.project_details %} <dt>Name</dt> - <dd>{{ object.name }} </dd> + <dd>{{ object.realname }} </dd> <dt>Homepage url</dt> <dd> {% if object.homepage %} - <a href="{{ object.homepage }}">{{ object.homepage }}</a> + <a target="_blank" href="{{ object.homepage }}"> + {{ object.homepage }} + </a> {% else %} - Not set yet + Not set yet {% endif %} </dd> <dt>Description</dt> <dd>{{ object.description }} </dd> - {% if owner_mode %} - <dt>Application date</dt> - <dd>{{object.issue_date|date:"d/m/Y"}} </dd> + {% if owner_mode or admin_mode %} + <dt>Creation date</dt> + {% block object.created_at %} + <dd>{{object.creation_date|date:"d/m/Y"}} </dd> + {% endblock %} {% endif %} - <dt>Start date</dt> - <dd>{{object.start_date|date:"d/m/Y"}} </dd> <dt>End Date</dt> <dd>{{object.end_date|date:"d/m/Y"}} </dd> - {% if owner_mode %} - <dt>Comments</dt> - <dd>{{ object.comments }} </dd> - {% endif %} - <dt>Owner</dt> <dd> - {% if owner_mode %} - Me - {% else %} - {{object.owner.realname}} {% if admin_mode or user.is_superuser %}({{object.owner.email}}){% endif %} + {% if owner_mode %} + Me + {% else %} + {{ object.owner.realname }} + {% if admin_mode or user.is_superuser %} + ({{object.owner.email}}) + {% endif %} {% endif %} </dd> + {% endblock page.project_details %} </dl> </div> - + {% else %} + {{ project.uuid|owner_by_uuid }} + {% endif %} - <div class="full-dotted"> - <h3>RESOURCES</h3> - {% if object.projectresourcegrant_set.all %} - <dl class="alt-style"> - {% for rp in object.projectresourcegrant_set.all %} - <dt>{{rp.resource.pluralized_display_name}} per user</dt> - <dd>{{rp.display_member_capacity}}</dd> + <div class="full-dotted {% if display_usage %}with-usage{% endif %}"> + + <div class="resources-heading clearfix"> + <h3>{% block page.resources_heading %}RESOURCES{% endblock %}</h3> + </div> + <div class="resources-heading clearfix"> + <h3></h3> + {% if resources_set.count %} + <div class="resource-label"> + <em>Max per member</em> + </div> + <div class="resource-label"> + <em>Total</em> + </div> + {% if display_usage %} + <div class="resource-label"> + <em>Usage</em> + </div> + {% endif %} + {% endif %} + </div> + {% if resources_set.all %} + <dl class="alt-style resources"> + {% for rp in resources_set.all|sorted_resources %} + {% if rp.resource.ui_visible %} + <dt>{{rp.resource.pluralized_display_name}}</dt> + <dd> + <div class="resource"> + {{ rp.display_member_capacity|inf_display|default:"(not set)" }} + {% if is_modification %} + {{ rp|resource_diff:"member" }} + {% endif %} + </div> + <div class="resource fix-col"> + {{ rp.display_project_capacity|inf_display|default:"(not set)" }} + {% if is_modification %} + {{ rp|resource_diff:"project" }} + {% endif %} + </div> + {% if display_usage %} + <div class="resource usage fix-col"> + {{ rp.resource|display_resource_usage_for_project:project }} + </div> + {% endif %} + </dd> + {% endif %} {% empty %} - No resources + <p>No resources</p> {% endfor %} </dl> {% else %} @@ -113,13 +141,14 @@ {% endif %} </div> - + {% block page.members %} + {% if not project.is_base %} <div class="full-dotted"> <h3> - {% if owner_mode and project_view %} + {% if owner_mode %} {% if project.is_alive %} - <a href="{% url project_members object.chain_id %}">MEMBERS </a> + <a href="{% url project_members project.uuid %}">MEMBERS </a> {% else %} MEMBERS {% endif %} @@ -134,7 +163,7 @@ <dt>Max participants</dt> <dd> {% if object.limit_on_members_number != None %} - {{object.limit_on_members_number}} + {{object.limit_on_members_number|inf_display}} {% else %}Not set{% endif %} </dd> <dt>Member join policy</dt> @@ -145,29 +174,35 @@ <dd> {{ object.member_leave_policy_display|title }} </dd> - {% if owner_mode and project_view %} - {% if project.is_alive %} - <dt><a href="{% url project_approved_members object.chain_id %}" title="view approved members">Approved members</a></dt> + {% if owner_mode %} + {% if object.is_alive %} + <dt><a href="{% url project_approved_members project.uuid %}" title="view approved members">Approved members</a></dt> <dd>{{ approved_members_count }} - <span class="faint"> + <span class="faint"> {% if remaining_memberships_count != None %} + {% if not object.has_infinite_members_limit %} ({{ remaining_memberships_count }} membership{{ remaining_memberships_count|pluralize }} remain{{ remaining_memberships_count|pluralize:"s," }}) + {% else %}(Unlimited memberships remain){% endif %} {% else %} {% endif %} </span> </dd> - <dt><a href="{% url project_pending_members object.chain_id %}" title="view pending members">Members pending approval</a></dt> + <dt><a href="{% url project_pending_members project.uuid %}" title="view pending members">Members pending approval</a></dt> <dd>{{ pending_members_count }}</dd> - {% if not project.is_deactivated %} + {% if not object.is_deactivated %} </dl> - {% include 'im/projects/addmembers_form.html' %} - + {% if not is_application %} + {% include 'im/projects/addmembers_form.html' %} + {% endif %} {% endif %} {% endif %} {% endif %} - </div> + </div> + {% endif %} + {% endblock page.members %} + {% endblock inner_project %} <div class="full-dotted"> <p> @@ -175,5 +210,4 @@ </p> </div> </div> -{% endwith %} {% endblock %} diff --git a/snf-astakos-app/astakos/im/templates/im/projects/project_list.html b/snf-astakos-app/astakos/im/templates/im/projects/project_list.html index 02d47990b37d44e39e1cd17d2cbb281492ce5506..c7782e11f171b3d59241cde5690c548f55ac0b30 100644 --- a/snf-astakos-app/astakos/im/templates/im/projects/project_list.html +++ b/snf-astakos-app/astakos/im/templates/im/projects/project_list.html @@ -6,7 +6,7 @@ {% block page.body %} <div class="maincol {% block innerpage.class %}{% endblock %}"> <div class="projects"> - {% if form %} + {% if form %} {% include "im/projects/search_form.html" %} {% else %} <h2>PROJECTS</h2> diff --git a/snf-astakos-app/astakos/im/templates/im/projects/project_members.html b/snf-astakos-app/astakos/im/templates/im/projects/project_members.html index 45c9b4af2dd3ec0341ed8af655b5db05c950a3a4..277e5782b496efa0fa9baf86bb9409c93903277f 100644 --- a/snf-astakos-app/astakos/im/templates/im/projects/project_members.html +++ b/snf-astakos-app/astakos/im/templates/im/projects/project_members.html @@ -3,11 +3,10 @@ {% load astakos_tags filters django_tables2 %} -{% block bottom_link %}<a href="{% url project_detail object.chain_id %}" title="Back to project ">< Back to Project</a>{% endblock bottom_link %} +{% block bottom_link %}<a href="{% url project_detail object.id %}" title="Back to project ">< Back to Project</a>{% endblock bottom_link %} {% block inner_project %} - - {% if owner_mode or admin_mode and project_view %} - {% if object.project.is_alive %} + {% if owner_mode or admin_mode %} + {% if object.is_alive %} <div class="full-dotted{% if members_status_filter == None %} all{% endif %}{% if members_status_filter == 1 %} approved {% endif %}{% if members_status_filter == 0 %} pending{% endif %}"> <h3> @@ -15,34 +14,32 @@ MEMBERS <div class="project-actions"> - <a href="{% url project_members object.chain_id %}" {% if members_status_filter == None %}class="inactive"{% endif %}>ALL</a> - - <a href="{% url project_approved_members object.chain_id %}" {% if members_status_filter == 1 %}class="inactive"{% endif %}>APPROVED</a> - - <a href="{% url project_pending_members object.chain_id %}" {% if members_status_filter == 0 %}class="inactive"{% endif %}>PENDING</a> + <a href="{% url project_members project.uuid %}" {% if members_status_filter == None %}class="inactive"{% endif %}>ALL</a> - + <a href="{% url project_approved_members project.uuid %}" {% if members_status_filter == 1 %}class="inactive"{% endif %}>APPROVED</a> - + <a href="{% url project_pending_members project.uuid %}" {% if members_status_filter == 0 %}class="inactive"{% endif %}>PENDING</a> </div> </h3> - {% if members_table %} {% render_table members_table %} <div class="form-actions inactive clearfix"> - {% confirm_link "Reject selected" "Reject selected members ?" "project_members_reject" chain_id "" "OK" 1 "reject members-batch-action" %} - {% confirm_link "Accept selected" "Accept selected members ?" "project_members_accept" chain_id "" "OK" 1 "accept members-batch-action" %} - {% confirm_link "Remove selected" "Remove selected members ?" "project_members_remove" chain_id "" "OK" 1 "remove members-batch-action" %} + {% confirm_link "Reject selected" "Reject selected members ?" "project_members_reject" project.uuid "" "OK" 1 "reject members-batch-action" %} + {% confirm_link "Accept selected" "Accept selected members ?" "project_members_accept" project.uuid "" "OK" 1 "accept members-batch-action" %} + {% confirm_link "Remove selected" "Remove selected members ?" "project_members_remove" project.uuid "" "OK" 1 "remove members-batch-action" %} </div> {% endif %} - {% if not project.is_deactivated %} + {% if not object.is_deactivated %} <div class="full-dotted" id="members-add-form"> {% include 'im/projects/addmembers_form.html' %} </div> {% endif %} {% endif %} - diff --git a/snf-astakos-app/astakos/im/templates/im/projects/project_modification_notification.txt b/snf-astakos-app/astakos/im/templates/im/projects/project_modification_notification.txt new file mode 100644 index 0000000000000000000000000000000000000000..1d1bf83588f5b76860878433e44b6210fe263cdd --- /dev/null +++ b/snf-astakos-app/astakos/im/templates/im/projects/project_modification_notification.txt @@ -0,0 +1,26 @@ +{% extends "im/email.txt" %} +{% load filters %} + +{% block content %} +The following application for a project modification has been submitted: + +Id: {{object.id}} +Project: {{object.chain.uuid}} +Applicant: {{object.applicant}} +Issue date: {{object.issue_date|date:"d/m/Y"}} + +Name: {{object.name|default_if_none:"[no change]"}} +Start date: {{object.start_date|date:"d/m/Y"|default:"[no change]"}} +End date: {{object.end_date|date:"d/m/Y"|default:"[no change]"}} +Member Join Policy: {{object.member_join_policy_display|default_if_none:"[no change]"}} +Member Leave Policy: {{object.member_leave_policy_display|default_if_none:"[no change]"}} +Owner: {{object.owner|default_if_none:"[no change]"}} +Max members: {{object.limit_on_members_number|format_inf|default_if_none:"[no change]"}} +Quota limits (changes only): +{% for rp in object.projectresourcegrant_set.all %} + {{rp}} +{% endfor %} + +For approving it you can use the command line tool: +snf-manage project-control --approve {{object.id}} +{% endblock content %} diff --git a/snf-astakos-app/astakos/im/templates/im/projects/project_reinstatement_notification.txt b/snf-astakos-app/astakos/im/templates/im/projects/project_reinstatement_notification.txt index f22e2a9d87dd39923b7a7b9842f7183e790cdf63..0b4d4a5d5d46b55058c02fd7ef2cea573c0ab803 100644 --- a/snf-astakos-app/astakos/im/templates/im/projects/project_reinstatement_notification.txt +++ b/snf-astakos-app/astakos/im/templates/im/projects/project_reinstatement_notification.txt @@ -1,5 +1,5 @@ {% extends "im/email.txt" %} {% block content %} -Your terminated project ({{object.name}}) has been reinstated. +Your terminated project {{object.realname}} has been reinstated. {% endblock content %} diff --git a/snf-astakos-app/astakos/im/templates/im/projects/project_suspension_notification.txt b/snf-astakos-app/astakos/im/templates/im/projects/project_suspension_notification.txt index 27eeb0377a305cd503567c4b36a6d267806d5be0..ad51f03903728827c12cf57d0d6c395d9e7aa8f4 100644 --- a/snf-astakos-app/astakos/im/templates/im/projects/project_suspension_notification.txt +++ b/snf-astakos-app/astakos/im/templates/im/projects/project_suspension_notification.txt @@ -1,5 +1,5 @@ {% extends "im/email.txt" %} {% block content %} -Your project ({{object.name}}) has been suspended. +Your project {{object.realname}} has been suspended. {% endblock content %} diff --git a/snf-astakos-app/astakos/im/templates/im/projects/project_termination_notification.txt b/snf-astakos-app/astakos/im/templates/im/projects/project_termination_notification.txt index 644ebe82e1b7cc6bf5efad37c88f626cd88d9d5c..231078ac6b37230a366a58490a41abdaf1a66daa 100644 --- a/snf-astakos-app/astakos/im/templates/im/projects/project_termination_notification.txt +++ b/snf-astakos-app/astakos/im/templates/im/projects/project_termination_notification.txt @@ -1,5 +1,5 @@ {% extends "im/email.txt" %} {% block content %} -Your project ({{object.application.name}}) has been terminated. +Your project {{object.realname}} has been terminated. {% endblock content %} diff --git a/snf-astakos-app/astakos/im/templates/im/projects/project_unsuspension_notification.txt b/snf-astakos-app/astakos/im/templates/im/projects/project_unsuspension_notification.txt index 10dbe4c2af0829df34e3638859e7851aa48dad5c..fff4b674f12194aa8617a6bbbcad223a2703764a 100644 --- a/snf-astakos-app/astakos/im/templates/im/projects/project_unsuspension_notification.txt +++ b/snf-astakos-app/astakos/im/templates/im/projects/project_unsuspension_notification.txt @@ -1,5 +1,5 @@ {% extends "im/email.txt" %} {% block content %} -Your suspended project ({{object.name}}) has been resumed. +Your suspended project {{object.realname}} has been resumed. {% endblock content %} diff --git a/snf-astakos-app/astakos/im/templates/im/projects/projectapplication_form.html b/snf-astakos-app/astakos/im/templates/im/projects/projectapplication_form.html index 432d4ea22ce9200bfc901a09dcdcc950313ecd62..cf38914f0d03e5e20d22b9316ef731d857579539 100644 --- a/snf-astakos-app/astakos/im/templates/im/projects/projectapplication_form.html +++ b/snf-astakos-app/astakos/im/templates/im/projects/projectapplication_form.html @@ -8,179 +8,85 @@ {% endblock %} {% block page.body %} -<h2> -{% if update_form %}REQUEST PROJECT MODIFICATION{% else %}REQUEST PROJECT{% endif %} -</h2> +<h2>{% block page.heading %}REQUEST PROJECT{% endblock %}</h2> -<form action="?verify=1" method="post" class="withlabels quotas-form" id="group_create_form">{% csrf_token %} +<form action="?verify=1" + method="post" + class="withlabels quotas-form" + id="group_create_form"> + + {% csrf_token %} - <fieldset class="with-info" id="top"> - <legend> - 1. PROJECT DETAILS - <span class="info"> - <em>more info</em> - <span> To create a new Project, first enter the following - required fields. The information you enter, except - <i>Comments for review</i>, will be visible to all {{ BRANDING_SERVICE_NAME }} - users. </span> - </span> - </legend> + <fieldset class="with-info {% if form.instance.is_base %}hidden-contents{% endif %}" id="top"> + <legend> + {% block form_details_title %}1. PROJECT DETAILS{% endblock %} + {% block form_details_description %} + <span class="info"> + <em>more info</em> + <span>To create a new Project, first enter the following + required fields. The information you enter, except + <i>Comments for review</i>, will be visible to all + {{ BRANDING_SERVICE_NAME }} users.</span> + </span> + {% endblock %} + </legend> + {% block form_errors %} {% for key, err in form.errors.items %} {% if key == "__all__" %} <div class="form-error">{{ err }}</div> {% endif %} {% endfor %} - + {% endblock %} {% for field in form %} - {% if field.name in details_fields %} - <div class="form-row {% if field.errors|length %}with-errors - {% endif %} - {% if field.is_hidden %}with-hidden{% endif %}"> - {{ field.errors }} - <p class="clearfix {% if field.blank %}required{% endif %}"> - {{ field.label_tag }} - {{ field|safe }} - <span class="extra-img"> </span> - {% if field.help_text %} - <span class="info"> - <em>more info</em> - <span>{{ field.help_text|safe }}</span> - </span> - {% endif %} - </p> - </div> - {% endif %} - {% endfor %} - - - - - {% for g, resources in resource_catalog %} - {% for r in resources %} - {% with r.str_repr as rname %} - {% with object|resource_grants|lookup:rname as value %} - <input type="hidden" id="{{'id_'|add:rname|add:'_uplimit'}}" name="{{rname|add:'_uplimit'}}" {% if value %}value="{{value}}"{% endif %} /> - {% endwith %} - {% endwith %} - {% endfor %} + {% with filter_fields=details_fields %} + {% include "im/projects/_form_field.html" %} + {% endwith %} {% endfor %} + <div class="system-warning"> + <p>Project details of system projects cannot be modified.</p> + </div> </fieldset> - <fieldset class="with-info"> + + <fieldset class="with-info {% if object.is_base %}hidden-contents{% endif %}"> <legend> 2. MEMBERSHIP OPTIONS <span class="info"> <em>more info</em> - <span> Membership options </span> + <span>Membership options</span> </span> </legend> - {% for field in form %} - - {% if field.name in membership_fields %} - <div class="form-row {% if field.errors|length %}with-errors{% endif %} {% if field.is_hidden %}with-hidden{% endif %}"> - {{ field.errors }} - <p class="clearfix {% if field.blank %}required{% endif %}"> - {{ field.label_tag }} - {{ field|safe }} - <span class="extra-img"> </span> - {% if field.help_text %} - <span class="info"> - <em>more info</em> - <span>{{ field.help_text|safe }}</span> - </span> - {% endif %} - </p> - </div> - {% endif %} - {% endfor %} - + <!-- membership fields --> + {% for field in form %} + {% with filter_fields=membership_fields %} + {% include "im/projects/_form_field.html" %} + {% endwith %} + {% endfor %} + <div class="system-warning"> + <p>Membership options of system projects cannot be modified.</p> + </div> </fieldset> + <fieldset id="icons"> <legend> 3. RESOURCES <span class="info"> <em>more info</em> - <span>Here you add resources to your Project. Each resource you specify here, will be granted to *EACH* user of this Project. So the total resources will be: <Total number of members> * <amount_of_resource> for each resource. </span> + <span>Here you add resources to your Project. Each resource you + specify here, will be granted to *EACH* user of this + Project. So the total resources will be: <Total number of + members> * <amount_of_resource> for each resource. + </span> </span> - </legend> - <ul class="clearfix"> - {% with object|resource_groups as groups %} - {% for g, group_info in resource_groups.items %} - {% if g %} - <li> - <a href="#{{ g }}" - id="{{'group_'|add:g}}" - {% if g in groups %}class="selected"{% endif %}> - <img src="{{ IM_STATIC_URL }}images/create-{{ g }}.png" alt="vm"/></a> - <input type="hidden" name="proxy_{{ 'is_selected_'|add:g }}" - id="proxy_{{ 'id_is_selected_'|add:g }}" {% if g in groups %}value="1"{% else %}value="0"{% endif %}> - <input type="hidden" name="{{ 'is_selected_'|add:g }}" id="{{ 'id_is_selected_'|add:g }}" {% if g in groups %}value="1"{% else %}value="0"{% endif %}> - <p class="msg">{{ group_info.help_text }}</p> - </li> - {% endif %} - {% endfor %} - {% endwith %} - </ul> - + </legend> + {% include "im/projects/_form_resource_groups_buttons.html" %} </fieldset> - <div class="visible"> </div> - <div class="not-visible"> - {% for gname, resources in resource_catalog %} - <div class="group {{'group_'|add:gname}}" id="{{ gname }}"> - <a href="#icons" class="delete">X remove resource</a> - {% for rdata in resources %} - {% with rdata.str_repr as rname %} - <fieldset class="quota"> - - <legend> - {% if rdata.is_abbreviation %} - {{ rdata.verbose_name|upper }} - {% else %} - {{ rdata.verbose_name|capfirst }} - {% endif %} - <span class="info"> - <em>more info</em> - <span>{{ rdata.help_text }}</span> - </span> - </legend> - <div class="form-row"> - <p class="clearfix"> - <label for="{{'id_'|add:rname|add:'_uplimit'}}_proxy" > - Total {{rdata.pluralized_display_name}} per user - </label> - <input type="text" - id="{{'id_'|add:rname|add:'_uplimit'}}_proxy" - name="{{rname|add:'_uplimit'}}_proxy" - placeholder="{{ rdata.placeholder}} " - {% if rdata.unit == 'bytes' %} - class="dehumanize" - {% endif %} - {% if request.POST %} - {% with rname|add:'_uplimit' as input_value %} - value = "{{ request.POST|lookup:input_value }}" - {% endwith %} - {% else %} - value = "{% get_grant_value rname form %}" - {% endif %} - autocomplete="off"> - <span class="extra-img"> </span> - <span class="info"><em>more info</em><span>{{ rdata.help_text_input_each }}</span></span> - </p> - <p class="error-msg">Invalid format</p> - <p class="msg"></p> - </div> - </fieldset> - {% endwith %} - {% endfor %} - </div> - - {% endfor %} - </div> - + {% include "im/projects/_form_resources_fields.html" %} <input type="hidden" name="user" id="id_user" value="{{user.id}}"> <div class="form-row submit"> <input type="submit" value="CONTINUE" class="submit altcol" > - </div> + </div> + </form> {% endblock %} diff --git a/snf-astakos-app/astakos/im/templates/im/projects/projectapplication_form_summary.html b/snf-astakos-app/astakos/im/templates/im/projects/projectapplication_form_summary.html index 4180899c313366d0f7adf722ba96c622bde16212..1dcb2958860101d73c9a9858f1e0da75354f3f90 100644 --- a/snf-astakos-app/astakos/im/templates/im/projects/projectapplication_form_summary.html +++ b/snf-astakos-app/astakos/im/templates/im/projects/projectapplication_form_summary.html @@ -8,12 +8,19 @@ {% block page.body %} <h2> -{% if update_form %}REQUEST PROJECT MODIFICATION{% else %}REQUEST PROJECT{% endif %} +{% block page.heading %} +REQUEST PROJECT +{% endblock %} </h2> -<p>These are the specifications of the Project you want to create. If you hit the "Submit" button -this form will be officially sent to {{ BRANDING_COMPANY_NAME }} for review. Please make sure the following reflect -exactly your request.</p> + +<p> +{% block page.description %} +These are the specifications of the Project you want to create. If you hit the +"Submit" button this form will be officially sent to {{ BRANDING_COMPANY_NAME }} +for review. Please make sure the following reflect exactly your request. +{% endblock %} +</p> <div class="projects summary"> <form action="?edit=0&verify=0" method="post" class="quotas-form">{% csrf_token %} @@ -22,65 +29,99 @@ exactly your request.</p> <input type="hidden" name="{{k}}" id="{{'id_'|add:k}}" value="{{v}}"> {% endif %} {% endfor %} - <div class="full-dotted"> - <h3>PROJECT DETAILS</h3> + <div class="full-dotted {% if project.is_base %}hidden{% endif %}"> + <h3>{% block page.project_details.heading %}PROJECT DETAILS{% endblock %}</h3> <p class="restricted">{{ form_data.desc }}</p> - <dl class="alt-style"> + <dl class="alt-style"> + {% block page.project_details %} <dt>Name</dt> <dd>{{ form_data.name }} </dd> <dt>Homepage Url</dt> <dd>{{ form_data.homepage }} </dd> <dt>Description</dt> - <dd>{{ form_data.description }} </dd> + <dd>{{ form_data.description }} </dd> + {% block form.start_date %} <dt>Start date</dt> - <dd>{{ form_data.start_date|date:"d/m/Y"}} </dd> + <dd>{{ form_data.start_date|date:"d/m/Y"}} </dd> + {% endblock %} <dt>End Date</dt> <dd>{{ form_data.end_date|date:"d/m/Y"}} </dd> <dt>Comments</dt> <dd>{{ form_data.comments }} </dd> - + {% endblock %} </dl> </div> - <div class="full-dotted"> + <div class="full-dotted {% if form.instance.is_base %}hidden{% endif %}"> <h3>MEMBERSHIP OPTIONS</h3> <dl class="alt-style"> + {% block page.project_membership_details %} <dt>Member join policy</dt> <dd>{{ join_policies|lookup:form_data.member_join_policy|title }}</dd> <dt>Member leave policy</dt> <dd>{{ leave_policies|lookup:form_data.member_leave_policy|title }}</dd> <dt>Total number of members</dt> - <dd>{{ form_data.limit_on_members_number }}</dd> + <dd>{{ form_data.limit_on_members_number|inf_display }}</dd> + {% endblock %} </dl> </div> - - <div class="full-dotted"> - <h3>RESOURCES</h3> - <p>The following resources will be granted to each member of this Project:</p> - <dl class="alt-style"> - {% for rp in form.resource_policies %} - <dt> - {{rp.pluralized_display_name}} per user - </dt> - <dd> - {{ rp.display_uplimit }} - </dd> - {% empty %} - No resources - {% endfor %} - </dl> + <div class="resources-heading clearfix"> + <h3>{% block page.resources_heading %}RESOURCES{% endblock %}</h3> + </div> + <div class="resources-heading clearfix"> + <h3></h3> + {% if form.resource_policies %} + <div class="resource-label"> + <em>Max per member</em> + </div> + <div class="resource-label"> + <em>Total</em> + </div> + {% endif %} + </div> + <dl class="alt-style resources"> + {% for rp in form.resource_policies %} + <dt> + {{ rp.pluralized_display_name }} + </dt> + <dd class="clearfix"> + <div class="resource total"> + {{ rp.display_m_uplimit|default:"(not set)" }} + {% if rp.m_diff %} + {% with rp.m_diff as diff %} + <span class="policy-diff + {% if diff.increased %}green{% else %}red{% endif %}" + >({% if diff.diff_is_inf %}{{ diff.prev_display }} to {% else %}{{ diff.operator }}{% endif %}{{ diff.diff_display }})</span> + {% endwith %} + {% endif %} + </div> + <div class="per-user resource"> + {{ rp.display_p_uplimit|default:"(not set)" }} + {% if rp.p_diff %} + {% with rp.p_diff as diff %} + <span class="policy-diff + {% if diff.increased %}green{% else %}red{% endif %}"> + ({% if diff.diff_is_inf %}{{ diff.prev_display }} to {% else %}{{ diff.operator }}{% endif %}{{ diff.diff_display }}) + </span> + {% endwith %} + {% endif %} + </div> + </dd> + {% empty %} + No resources + {% endfor %} + </dl> </div> <div class="full-dotted"> - </div> <input type="hidden" name="user" id="id_user" value="{{user.id}}"> <div class="form-row submit"> - <input type="submit" value="BACK" class="submit lt" onclick='this.form.action="?edit=1&verify=0";'> - <input type="submit" value="SUBMIT" class="submit" > - <a href="{% url project_list %}" class="rt-link">CANCEL</a> + <input type="submit" value="BACK" class="submit lt" + onclick='this.form.action="?edit=1&verify=0";'> + <input type="submit" value="SUBMIT" class="submit"> </div> </form> diff --git a/snf-astakos-app/astakos/im/templates/im/projects/projectmodification_form.html b/snf-astakos-app/astakos/im/templates/im/projects/projectmodification_form.html new file mode 100644 index 0000000000000000000000000000000000000000..d69ed4f704db6a2ae460cb939527414d5e91a2b2 --- /dev/null +++ b/snf-astakos-app/astakos/im/templates/im/projects/projectmodification_form.html @@ -0,0 +1,10 @@ +{% extends "im/projects/projectapplication_form.html" %} +{% load filters %} + +{% block page.heading %} +{% if form.instance.is_base %} +REQUEST <em>{{ form.instance|project_name_for_user:user }}</em> PROJECT MODIFICATION +{% else %} +REQUEST PROJECT MODIFICATION +{% endif %} +{% endblock %} diff --git a/snf-astakos-app/astakos/im/templates/im/projects/projectmodification_form_summary.html b/snf-astakos-app/astakos/im/templates/im/projects/projectmodification_form_summary.html new file mode 100644 index 0000000000000000000000000000000000000000..852741c15b034de0bda496e61f2d864e4fd8c7cc --- /dev/null +++ b/snf-astakos-app/astakos/im/templates/im/projects/projectmodification_form_summary.html @@ -0,0 +1,37 @@ +{% extends "im/projects/projectapplication_form_summary.html" %} +{% load filters %} + +{% block page.heading %} +REQUEST PROJECT MODIFICATION +{% endblock %} + +{% block page.description %} +These are the modifications you want to apply to the Project. If you hit the +"Submit" button this form will be officially sent to {{ BRANDING_COMPANY_NAME }} +for review. Please make sure the following reflect exactly your modification +request. +{% endblock %} + +{% block page.project_details %} +<dt>Name</dt> +<dd>{{ form|display_modification_param:"name" }} </dd> +<dt>Homepage Url</dt> +<dd>{{ form|display_modification_param:"homepage" }} </dd> +<dt>Description</dt> +<dd>{{ form|display_modification_param:"description" }} </dd> +<dt>End Date</dt> +<dd>{{ form|display_date_modification_param:"end_date,d/m/Y" }} </dd> +<dt>Comments</dt> +<dd>{{ form_data.comments }} </dd> +{% endblock %} + +{% block page.project_membership_details %} +<dt>Member join policy</dt> +<dd>{{ form|display_modification_param:"member_join_policy" }} </dd> +<dt>Member leave policy</dt> +<dd>{{ form|display_modification_param:"member_leave_policy" }} </dd> +<dt>Total number of members</dt> +<dd>{{ form|display_modification_param_diff:"limit_on_members_number" }} </dd> +{% endblock %} + +{% block page.resources_heading %}RESOURCES{% endblock %} diff --git a/snf-astakos-app/astakos/im/templates/im/resource_usage.html b/snf-astakos-app/astakos/im/templates/im/resource_usage.html index 43c986f0be9ec6ecad6a0c98f8d5638ce8118bc2..d2941228d20007441823ae217dcffa9b6b533eb6 100644 --- a/snf-astakos-app/astakos/im/templates/im/resource_usage.html +++ b/snf-astakos-app/astakos/im/templates/im/resource_usage.html @@ -10,29 +10,50 @@ {% block page.title %}Usage{% endblock %} {% block page.body %} +<script id="projectQuotaTpl" type="text/template"> + {% verbatim %} + <div class="resource-bar project clearfix project-{{id}} {{ usage.cls }} + {{^system_project}}no-base-project{{/system_project}} + {{#system_project}}base-project{{/system_project}}" + data-project="{{ id }}"> + <div class="info {{#not_a_member}}warn{{/not_a_member}}" data-currvalue="" data-maxvalue=""> + <h3> + <a href="{{ details_url }}">{{{ display_name }}}</a> + {{#not_a_member}}<span class="warn-msg">[NOT A MEMBER]</span>{{/not_a_member}} + </h3> + <p> + <span class="currValue">{{ usage.curr }}</span> out of + <span class="maxValue">{{ usage.max }}</span> {{ report_desc }} + (<span class="leftValue">{{ usage.left }}</span> left) + </p> + </div> + <div class="bar"> + <i class="warn {{#usage.project_warn}}visible{{/usage.project_warn}}"></i> + <i class="warn-msg"> + {{ usage.project_warn_msg }} + </i> + <div> + <span style="width:{{ usage.ratio }}%" class="member"></span> + <span style="width:{{ usage.project_warn_ratio}}%" + class="project"></span> + <em class="value" + style="left: {{ usage.label_left }}; color: {{ usage.label_color }}"> + {{ usage.ratio }}% + </em> + </div> + </div> + </div> + {% endverbatim %} +</script> <script id="quotaTpl" type="text/template"> {% verbatim %} {{#resources}} <li class="clearfix {{ resource_name }} {{ usage.cls }}" - data-resource="{{ name }}"> + data-resource="{{ name }}"> <div class="img-wrap"> </div> - <div class="info" data-currvalue="" data-maxvalue=""> - <h3>{{ report_desc }}</h3> - <p> - <span class="currValue">{{ usage.curr }}</span> out of - <span class="maxValue">{{ usage.max }}</span> {{ report_desc }} - </p> - </div> - <div class="bar"> - <div> - <span style="width:{{ usage.perc }}%"></span> - <em - class="value" - style="left: {{ usage.label_left }}%; color: {{ usage.label_color }}"> - {{ usage.perc }}% - </em> - </div> + <h2>{{ display_name }}</h2> + <div class="resource-projects clearfix"> </div> </li> {{/resources}} @@ -41,20 +62,23 @@ <div class="maincol {% block innerpage.class %}{% endblock %}"> <h2>RESOURCE USAGE</h2> - <div id="quota-container"> + <div id="quota-container" class='filter-base'> </div> </div> <script> $(document).ready(function(){ var usageView = new UsageView({ 'url': '{% url astakos-api-quotas %}', + 'projects_url': '{% url astakos.api.projects.projects %}', 'cookie_name': '{{ token_cookie_name|safe }}', 'dataType': 'json', 'container': '#quota-container', - 'quotas': {{ current_usage|safe }}, + 'quotas': {{ user_quotas|safe }}, + 'project_url_tpl': '{% url project_detail project_uuid="UUID" %}', 'meta': { 'resources': {{ resource_catalog|safe }}, 'groups': {{ resource_groups|safe }}, + 'projects_details': {{ projects_details|safe }}, 'resources_order': {{ resources_order|safe }} } }); diff --git a/snf-astakos-app/astakos/im/templates/im/table_render.html b/snf-astakos-app/astakos/im/templates/im/table_render.html index 0cfa8cd6fa4cb3018062c8261d00d6178f502281..62d757ef1f2e3aeab2aae2ada428f06dec3283d2 100644 --- a/snf-astakos-app/astakos/im/templates/im/table_render.html +++ b/snf-astakos-app/astakos/im/templates/im/table_render.html @@ -4,10 +4,22 @@ {% if table.page %} <div class="table-container"> {% endif %} + {% block table %} + <table{% if table.attrs %} {{ table.attrs.as_html }}{% endif %}> {% if table.caption %} - <caption>{{ table.caption }}</caption> + <caption>{{ table.caption }} + +{% if table.links %}( +{% for link in table.links %} +<a href="{{ link.url }}#{{ table.attrs.id }}"> + {{ link.label }} +</a>{% if not forloop.last %} / {% endif %} +{% endfor %} +) +{% endif %} + </caption> {% endif %} {% nospaceless %} {% block table.thead %} diff --git a/snf-astakos-app/astakos/im/templatetags/astakos_tags.py b/snf-astakos-app/astakos/im/templatetags/astakos_tags.py index cfef47d010630c03a8ff677732f8014b6726e2d1..453a1a7f52baa9ff53f73cfd32f64787dc644725 100644 --- a/snf-astakos-app/astakos/im/templatetags/astakos_tags.py +++ b/snf-astakos-app/astakos/im/templatetags/astakos_tags.py @@ -1,35 +1,17 @@ -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import urllib @@ -48,14 +30,14 @@ from django.utils.safestring import mark_safe register = template.Library() MESSAGES_VIEWS_MAP = getattr(settings, 'ASTAKOS_MESSAGES_VIEWS_MAP', { - 'astakos.im.views.index': 'LOGIN_MESSAGES', - 'astakos.im.views.logout': 'LOGIN_MESSAGES', - 'astakos.im.views.login': 'LOGIN_MESSAGES', - 'astakos.im.views.signup': 'SIGNUP_MESSAGES', - 'astakos.im.views.edit_profile': 'PROFILE_MESSAGES', - 'astakos.im.views.change_password': 'PROFILE_MESSAGES', - 'astakos.im.views.invite': 'PROFILE_MESSAGES', - 'astakos.im.views.feedback': 'PROFILE_MESSAGES', + 'astakos.im.views.im.index': 'LOGIN_MESSAGES', + 'astakos.im.views.im.logout': 'LOGIN_MESSAGES', + 'astakos.im.views.im.login': 'LOGIN_MESSAGES', + 'astakos.im.views.im.signup': 'SIGNUP_MESSAGES', + 'astakos.im.views.im.edit_profile': 'PROFILE_MESSAGES', + 'astakos.im.views.im.change_password': 'PROFILE_MESSAGES', + 'astakos.im.views.im.invite': 'PROFILE_MESSAGES', + 'astakos.im.views.im.feedback': 'PROFILE_MESSAGES', }) @@ -177,14 +159,15 @@ class MessagesNode(template.Node): @register.simple_tag -def get_grant_value(rname, form): - grants = form.instance.grants - try: - r = form.instance.projectresourcegrant_set.get( - resource__name=rname).member_capacity - except Exception, e: - r = '' - return r +def get_grant_value(rname, project_or_app, for_project=True): + if not project_or_app: + return None + resource_set = project_or_app.grants + r = resource_set.get(resource__name=rname) + if for_project: + return r.project_capacity + else: + return r.member_capacity @register.tag(name="provider_login_url") @@ -233,6 +216,7 @@ CONFIRM_LINK_PROMPT_MAP = { 'project ?'), 'project_join': _('Are you sure you want to join this project ?'), 'project_leave': _('Are you sure you want to leave from the project ?'), + 'project_cancel_member': _('Are you sure you want to cancel your join request ?'), } @@ -250,8 +234,14 @@ def confirm_link(context, title, prompt='', url=None, urlarg=None, if isinstance(urlarg, basestring) and "," in urlarg: args = urlarg.split(",") for index, arg in enumerate(args): + property = None + if "." in arg: + arg, property = arg.split(".") if context.get(arg, None) is not None: - args[index] = context.get(arg) + val = context.get(arg) + if property: + val = getattr(val, property) + args[index] = val urlargs = args else: urlargs = (urlarg,) diff --git a/snf-astakos-app/astakos/im/templatetags/filters.py b/snf-astakos-app/astakos/im/templatetags/filters.py index ace8fbe81c346bd940363de92a676d8dbf547858..6fb7ab6aa0c3e29e7a46dab5928c4a3ad9e73f41 100644 --- a/snf-astakos-app/astakos/im/templatetags/filters.py +++ b/snf-astakos-app/astakos/im/templatetags/filters.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import calendar import datetime @@ -40,17 +22,33 @@ from collections import defaultdict from django import template from django.core.paginator import Paginator, EmptyPage from django.db.models.query import QuerySet +from django.utils.safestring import mark_safe +from django.template import defaultfilters from synnefo.lib.ordereddict import OrderedDict +from synnefo.util import units from astakos.im import settings -from astakos.im.models import ProjectResourceGrant +from astakos.im.models import ProjectResourceGrant, Project +from astakos.im.views import util as views_util +from astakos.im import util +from astakos.im import presentation +from astakos.im.models import AstakosUser + +from astakos.im import quotas register = template.Library() DELIM = ',' +def _is_inf(value): + try: + return value == units.PRACTICALLY_INFINITE + except: + return False + + @register.filter def monthssince(joined_date): now = datetime.datetime.now() @@ -180,30 +178,304 @@ def get_value_after_dot(value): @register.filter def truncatename(v, max=18, append="..."): - length = len(v) - if length > max: - return v[:max] + append - else: - return v + util.truncatename(v, max, append) @register.filter -def resource_groups(project_definition): - try: - grants = project_definition.projectresourcegrant_set - return grants.values_list('resource__group', flat=True) - except: - return () +def selected_resource_groups(project_or_app): + if not project_or_app: + return [] + + grants = project_or_app.resource_set + resources = grants.values_list('resource__name', flat=True) + return map(lambda r: r.split(".")[0], resources) @register.filter -def resource_grants(project_definition): +def resource_grants(project_or_app): try: - grants = project_definition.projectresourcegrant_set + grants = project_or_app.resource_set grants = grants.values_list( - 'resource__name', - 'member_capacity' - ) - return dict((e[0], e[1]) for e in grants) + 'resource__name', 'member_capacity', 'project_capacity') + return dict((e[0], {'member':e[1], 'project':e[2]}) for e in grants) except: return {} + + +def get_resource_grant(project_or_app, rname, capacity_for): + if project_or_app is None: + return None + + resource_set = project_or_app.resource_set + if not resource_set.filter(resource__name=rname).count(): + return None + + resource = resource_set.get(resource__name=rname) + return getattr(resource, '%s_capacity' % capacity_for) + + +@register.filter +def get_member_resource_grant_value(project_or_app, rname): + return get_resource_grant(project_or_app, rname, "member") + + +@register.filter +def get_project_resource_grant_value(project_or_app, rname): + return get_resource_grant(project_or_app, rname, "project") + + +@register.filter +def resource_diff(r, member_or_project): + if not hasattr(r, 'display_project_diff'): + return '' + + project, member = r.display_project_diff() + diff = dict(zip(['project', 'member'], + r.display_project_diff())).get(member_or_project) + + diff_disp = '' + if diff != '': + diff_disp = "(%s)" % diff + tpl = '<span class="policy-diff %s">%s</span>' + cls = 'red' if diff.startswith("-") else 'green' + return mark_safe(tpl % (cls, diff_disp)) + + +@register.filter +def sorted_resources(resources_set): + return views_util.sorted_resources(resources_set) + + +@register.filter +def display_resource_usage_for_project(resource, project): + usage_map = presentation.USAGE_TAG_MAP + quota = quotas.get_project_quota(project).get(resource.name, None) + + if not quota: + return "No usage" + + cls = '' + usage = quota['project_usage'] + limit = quota['project_limit'] + + if limit == 0 and usage == 0: + return "--" + + usage_perc = "%d" % ((float(usage) / limit) * 100) if limit else "100" + _keys = usage_map.keys() + _keys.reverse() + closest = filter(lambda x: int(x) <= int(usage_perc), _keys)[0] + cls = usage_map[closest] + + usage_display = units.show(usage, resource.unit) + usage_perc_display = "%s%%" % usage_perc + + resp = """<span class="%s policy-diff">%s (%s)</span>""" % \ + (cls, usage_perc_display, usage_display) + return mark_safe(resp) + + +@register.filter +def is_pending_app(app): + if not app: + return False + return app.state in [app.PENDING] + + +@register.filter +def is_denied_app(app): + if not app: + return False + return app.state in [app.DENIED] + + +def _member_policy_formatter(form_or_app, value, changed, mapping): + if changed: + changed = defaultfilters.title(mapping.get(changed)) + value = defaultfilters.title(mapping.get(value)) + return value, changed, None, None + + +def _owner_formatter(form_or_app, value, changed): + if not changed: + changed_name = None + else: + changed_name = changed.realname + return value.realname if value else None, changed_name, None, None + + +def _owner_admin_formatter(form_or_app, value, changed): + if not changed: + changed_name = None + else: + changed_name = changed.realname + " (%s)" % changed.email + return value.realname + " (%s)" % value.email if value else None, changed_name, None, None + + +def _owner_owner_formatter(form_or_app, value, changed): + if not changed: + changed_name = None + else: + changed_name = changed.realname + return "Me", changed_name, None, None + + +MODIFICATION_FORMATTERS = { + 'member_policy': _member_policy_formatter, + 'owner': _owner_formatter, + 'owner_admin': _owner_admin_formatter, + 'owner_owner': _owner_owner_formatter +} + + +@register.filter +def display_modification_param(form_or_app, param, formatter=None): + formatter_name = None + if "," in param: + param, formatter_name = param.split(",", 1) + + project_attr = param + + if hasattr(form_or_app, 'instance'): + # form + project = Project.objects.get(pk=form_or_app.instance.pk) + app_value = form_or_app.cleaned_data[param] + project_value = getattr(project, project_attr) + else: + # app + project = form_or_app.chain + app_value = getattr(form_or_app, project_attr) + project_value = getattr(project, project_attr) + if app_value == None: + app_value = project_value + + formatter_params = {} + + if param == "member_join_policy": + formatter_name = 'member_policy' + formatter_params = {'mapping': + presentation.PROJECT_MEMBER_JOIN_POLICIES} + + if param == "member_leave_policy": + formatter_name = 'member_policy' + formatter_params = {'mapping': + presentation.PROJECT_MEMBER_LEAVE_POLICIES} + + changed = False + changed_cls = "gray details" + if project_value != app_value: + changed = project_value + + if not formatter and formatter_name: + formatter = MODIFICATION_FORMATTERS.get(formatter_name) + + changed_prefix = "<span>current: </span>" + if formatter: + app_value, changed, cls, prefix = formatter(form_or_app, + app_value, changed, + **formatter_params) + if cls: + changed_cls = cls + + if prefix: + changed_prefix = prefix + + tpl = """%(value)s""" + if changed: + tpl += """<span class="policy-diff %(changed_cls)s">""" + \ + """%(changed_prefix)s%(changed)s</span>""" + + if not app_value: + app_value = "(not set)" + + return mark_safe(tpl % { + 'value': app_value, + 'changed': changed, + 'changed_cls': changed_cls, + 'changed_prefix': changed_prefix + }) + + +@register.filter +def display_modification_param_diff(form_or_app, param): + def formatter(form_or_app, value, changed): + if changed in [None, False]: + if _is_inf(value): + value = "Unlimited" + return value, changed, None, " " + + to_inf = _is_inf(value) + from_inf = _is_inf(changed) + + diff = value - changed + sign = "+" + cls = "green" + if diff < 0: + sign = "-" + diff = abs(diff) + cls = "red" + + if diff != 5: + if from_inf or to_inf: + if from_inf: + changed = "Unlimited" + diff = "from %s" % changed + else: + diff = sign + str(diff) + changed = "(%s)" % (diff,) + else: + changed = None + + if to_inf: + value = "Unlimited" + return value, changed, cls, " " + + return display_modification_param(form_or_app, param, formatter) + + +@register.filter +def display_date_modification_param(form_or_app, params): + param, date_format = params.split(",", 1) + + def formatter(form_or_app, value, changed): + if changed not in [None, False]: + changed = defaultfilters.date(changed, date_format) + formatted_value = defaultfilters.date(value, date_format) + return formatted_value, changed, None, None + + return display_modification_param(form_or_app, param, formatter) + + +@register.filter +def inf_display(value): + if value == units.PRACTICALLY_INFINITE: + return 'Unlimited' + return value + + +@register.filter +def inf_value_display(value): + if value == units.PRACTICALLY_INFINITE: + return 'Unlimited' + return value + + +@register.filter +def project_name_for_user(project, user): + return project.display_name_for_user(user) + + +@register.filter +def owner_by_uuid(uuid): + try: + user = AstakosUser.objects.get(uuid=uuid) + return "%s %s (%s)" % (user.first_name, user.last_name, user.email) + except AstakosUser.DoesNotExist: + return uuid + + +@register.filter +def format_inf(value): + if _is_inf(value): + return "Unlimited" + return value diff --git a/snf-astakos-app/astakos/im/templatetags/formatters.py b/snf-astakos-app/astakos/im/templatetags/formatters.py index c17d4bedff68185d0b4ef8a4abbc44d733fb9609..84459708bde45bc05cc50b43d4a4e3377d70a426 100644 --- a/snf-astakos-app/astakos/im/templatetags/formatters.py +++ b/snf-astakos-app/astakos/im/templatetags/formatters.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django import template diff --git a/snf-astakos-app/astakos/im/tests/__init__.py b/snf-astakos-app/astakos/im/tests/__init__.py index 3322f44a5574728e5fde97ce179c91e0555dfffb..f33f74b3b510f118e6e3affd38787cf69f987cde 100644 --- a/snf-astakos-app/astakos/im/tests/__init__.py +++ b/snf-astakos-app/astakos/im/tests/__init__.py @@ -1,38 +1,26 @@ -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# flake8: noqa from astakos.im.tests.auth import * from astakos.im.tests.projects import * from astakos.im.tests.api import * from astakos.im.tests.views import * from astakos.im.tests.services import * +from astakos.im.tests.user_logic import * +from astakos.im.tests.user_utils import * +from astakos.im.tests.management import (TestUserModification, + TestSendUserActivation) +from astakos.im.tests.transactions import * diff --git a/snf-astakos-app/astakos/im/tests/api.py b/snf-astakos-app/astakos/im/tests/api.py index 8b32d8f4aeee7e4d7e97e943bada8833813464a6..7887d8a290d0374b71cbcea1363b388a782070cf 100644 --- a/snf-astakos-app/astakos/im/tests/api.py +++ b/snf-astakos-app/astakos/im/tests/api.py @@ -1,35 +1,18 @@ -# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved. +# -*- coding: utf-8 -*- +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from astakos.im.tests.common import * from astakos.im.settings import astakos_services, BASE_HOST @@ -60,20 +43,20 @@ class QuotaAPITest(TestCase): component1 = Component.objects.create(name="comp1") register.add_service(component1, "service1", "type1", []) # custom service resources - resource11 = {"name": "service1.resource11", - "desc": "resource11 desc", + resource11 = {"name": u"service1.ÏίσοÏÏ‚11", + "desc": "ÏίσοÏÏ‚11 desc", "service_type": "type1", "service_origin": "service1", "ui_visible": True} r, _ = register.add_resource(resource11) - register.update_resources([(r, 100)]) + register.update_base_default(r, 100) resource12 = {"name": "service1.resource12", - "desc": "resource11 desc", + "desc": "ÏίσοÏÏ‚11 desc", "service_type": "type1", "service_origin": "service1", "unit": "bytes"} r, _ = register.add_resource(resource12) - register.update_resources([(r, 1024)]) + register.update_base_default(r, 1024) # create user user = get_local_user('test@grnet.gr') @@ -88,14 +71,14 @@ class QuotaAPITest(TestCase): register.add_service(component2, "service2", "type2", []) # create another service resource21 = {"name": "service2.resource21", - "desc": "resource11 desc", + "desc": "ÏίσοÏÏ‚11 desc", "service_type": "type2", "service_origin": "service2", "ui_visible": False} r, _ = register.add_resource(resource21) - register.update_resources([(r, 3)]) + register.update_base_default(r, 3) - resource_names = [r['name'] for r in + resource_names = [res['name'] for res in [resource11, resource12, resource21]] # get resources @@ -113,10 +96,10 @@ class QuotaAPITest(TestCase): r = client.get(u('quotas/'), **headers) self.assertEqual(r.status_code, 200) body = json.loads(r.content) - system_quota = body['system'] - assertIn('system', body) + assertIn(user.uuid, body) + base_quota = body[user.uuid] for name in resource_names: - assertIn(name, system_quota) + assertIn(name, base_quota) nmheaders = {'HTTP_X_AUTH_TOKEN': non_moderated_user.auth_token} r = client.get(u('quotas/'), **nmheaders) @@ -149,14 +132,14 @@ class QuotaAPITest(TestCase): "name": "my commission", "provisions": [ { - "holder": user.uuid, - "source": "system", + "holder": "user:" + user.uuid, + "source": "project:" + user.uuid, "resource": resource11['name'], "quantity": 1 }, { - "holder": user.uuid, - "source": "system", + "holder": "user:" + user.uuid, + "source": "project:" + user.uuid, "resource": resource12['name'], "quantity": 30000 }]} @@ -169,17 +152,17 @@ class QuotaAPITest(TestCase): commission_request = { "force": False, "auto_accept": False, - "name": "my commission", + "name": u"ναμε", "provisions": [ { - "holder": user.uuid, - "source": "system", + "holder": "user:" + user.uuid, + "source": "project:" + user.uuid, "resource": resource11['name'], "quantity": 1 }, { - "holder": user.uuid, - "source": "system", + "holder": "user:" + user.uuid, + "source": "project:" + user.uuid, "resource": resource12['name'], "quantity": 100 }]} @@ -218,19 +201,27 @@ class QuotaAPITest(TestCase): body = json.loads(r.content) self.assertEqual(body['serial'], serial1) assertIn('issue_time', body) + self.assertEqual(body["name"], u"ναμε") provisions = sorted(body['provisions'], key=lambda p: p['resource']) - self.assertEqual(provisions, commission_request['provisions']) + crp = sorted(commission_request['provisions'], + key=lambda p: p['resource']) + self.assertEqual(provisions, crp) self.assertEqual(body['name'], commission_request['name']) r = client.get(u('service_quotas?user=' + user.uuid), **s1_headers) self.assertEqual(r.status_code, 200) body = json.loads(r.content) user_quota = body[user.uuid] - system_quota = user_quota['system'] - r11 = system_quota[resource11['name']] + base_quota = user_quota[user.uuid] + r11 = base_quota[resource11['name']] self.assertEqual(r11['usage'], 3) self.assertEqual(r11['pending'], 3) + r = client.get(u('service_project_quotas'), **s1_headers) + self.assertEqual(r.status_code, 200) + body = json.loads(r.content) + assertIn(user.uuid, body) + # resolve pending commissions resolve_data = { "accept": [serial1, serial3], @@ -256,14 +247,14 @@ class QuotaAPITest(TestCase): "name": "my commission", "provisions": [ { - "holder": user.uuid, - "source": "system", + "holder": "user:" + user.uuid, + "source": "project:" + user.uuid, "resource": resource11['name'], "quantity": 1 }, { - "holder": user.uuid, - "source": "system", + "holder": "user:" + user.uuid, + "source": "project:" + user.uuid, "resource": resource12['name'], "quantity": 100 }]} @@ -285,8 +276,8 @@ class QuotaAPITest(TestCase): "name": "my commission", "provisions": [ { - "holder": user.uuid, - "source": "system", + "holder": "user:" + user.uuid, + "source": "project:" + user.uuid, "resource": resource11['name'], } ]} @@ -298,7 +289,7 @@ class QuotaAPITest(TestCase): commission_request = { "auto_accept": True, - "name": "my commission", + "name": "κομίσσιον", "provisions": "dummy"} post_data = json.dumps(commission_request) @@ -316,14 +307,14 @@ class QuotaAPITest(TestCase): "name": "my commission", "provisions": [ { - "holder": user.uuid, - "source": "system", + "holder": "user:" + user.uuid, + "source": "project:" + user.uuid, "resource": "non existent", "quantity": 1 }, { - "holder": user.uuid, - "source": "system", + "holder": "user:" + user.uuid, + "source": "project:" + user.uuid, "resource": resource12['name'], "quantity": 100 }]} @@ -337,8 +328,8 @@ class QuotaAPITest(TestCase): commission_request = { "provisions": [ { - "holder": user.uuid, - "source": "system", + "holder": "user:" + user.uuid, + "source": "project:" + user.uuid, "resource": resource11['name'], "quantity": -1 } @@ -357,7 +348,6 @@ class QuotaAPITest(TestCase): content_type='application/json', **s1_headers) self.assertEqual(r.status_code, 200) - reject_data = {'reject': ""} post_data = json.dumps(accept_data) r = client.post(u('commissions/' + str(serial) + '/action'), post_data, content_type='application/json', **s1_headers) @@ -368,8 +358,8 @@ class QuotaAPITest(TestCase): "force": True, "provisions": [ { - "holder": user.uuid, - "source": "system", + "holder": "user:" + user.uuid, + "source": "project:" + user.uuid, "resource": resource11['name'], "quantity": 100 }]} @@ -383,8 +373,8 @@ class QuotaAPITest(TestCase): "force": True, "provisions": [ { - "holder": user.uuid, - "source": "system", + "holder": "user:" + user.uuid, + "source": "project:" + user.uuid, "resource": resource11['name'], "quantity": -200 }]} @@ -397,8 +387,8 @@ class QuotaAPITest(TestCase): r = client.get(u('quotas'), **headers) self.assertEqual(r.status_code, 200) body = json.loads(r.content) - system_quota = body['system'] - r11 = system_quota[resource11['name']] + base_quota = body[user.uuid] + r11 = base_quota[resource11['name']] self.assertEqual(r11['usage'], 102) self.assertEqual(r11['pending'], 101) @@ -407,6 +397,22 @@ class QuotaAPITest(TestCase): self.assertEqual(r.status_code, 405) self.assertTrue('Allow' in r) + r = client.post(u('commissions'), "\"\xff\"", + content_type='application/json', **s1_headers) + self.assertEqual(r.status_code, 400) + + r = client.post(u('commissions'), "\"nodict\"", + content_type='application/json', **s1_headers) + self.assertEqual(r.status_code, 400) + + r = client.post(u('commissions/' + "123" + '/action'), "\"\xff\"", + content_type='application/json', **s1_headers) + self.assertEqual(r.status_code, 400) + + r = client.post(u('commissions/' + "123" + '/action'), "\"nodict\"", + content_type='application/json', **s1_headers) + self.assertEqual(r.status_code, 400) + class TokensApiTest(TestCase): def setUp(self): @@ -540,7 +546,8 @@ class TokensApiTest(TestCase): r = client.post(url, "not json", content_type='application/json') self.assertEqual(r.status_code, 400) body = json.loads(r.content) - self.assertEqual(body['badRequest']['message'], 'Invalid JSON data') + self.assertEqual(body['badRequest']['message'], + 'Could not decode request body as JSON') # Check auth with token post_data = """{"auth":{"token": {"id":"%s"}, @@ -735,7 +742,7 @@ class WrongPathAPITest(TestCase): response = self.client.get(path) self.assertEqual(response.status_code, 400) try: - error = json.loads(response.content) + json.loads(response.content) except ValueError: self.assertTrue(False) @@ -753,7 +760,7 @@ class WrongPathAPITest(TestCase): class ValidateAccessToken(TestCase): def setUp(self): self.oa2_backend = DjangoBackend() - self.user = AstakosUser.objects.create(username="user@synnefo.org") + self.user = get_local_user("user@synnefo.org") self.token = self.oa2_backend.token_model.create( code='12345', expires_at=datetime.now() + timedelta(seconds=5), diff --git a/snf-astakos-app/astakos/im/tests/auth.py b/snf-astakos-app/astakos/im/tests/auth.py index 214627ee299f725d47d7d61fcb9c79219752d144..226fca6d0b64af5d65fcfa21bf02acdbb27007ec 100644 --- a/snf-astakos-app/astakos/im/tests/auth.py +++ b/snf-astakos-app/astakos/im/tests/auth.py @@ -1,36 +1,18 @@ # -*- coding: utf-8 -*- -# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import urlparse import urllib @@ -39,6 +21,10 @@ from astakos.im.tests.common import * ui_url = lambda url: '/' + astakos_settings.BASE_PATH + '/ui/%s' % url +MANAGERS = (('Manager', 'manager@synnefo.org'),) +HELPDESK = (('Helpdesk', 'helpdesk@synnefo.org'),) +ADMINS = (('Admin', 'admin@synnefo.org'),) + class ShibbolethTests(TestCase): """ @@ -177,6 +163,8 @@ class ShibbolethTests(TestCase): user = AstakosUser.objects.get() provider = user.get_auth_provider("shibboleth") + first_login_date = provider._instance.last_login_at + self.assertFalse(provider._instance.last_login_at) headers = provider.provider_details['info']['headers'] self.assertEqual(headers.get('SHIB_CUSTOM_IDP_KEY'), 'test') @@ -230,8 +218,15 @@ class ShibbolethTests(TestCase): self.assertEqual(u.is_active, True) - # we see our profile + # we visit our profile view r = client.get(ui_url("login/shibboleth?"), follow=True) + user = r.context['request'].user + provider = user.get_auth_provider()._instance + # last login date updated + self.assertTrue(provider.last_login_at) + self.assertNotEqual(provider.last_login_at, first_login_date) + self.assertEqual(provider.last_login_at, user.last_login) + self.assertRedirects(r, ui_url('landing')) self.assertEqual(r.status_code, 200) @@ -444,7 +439,9 @@ class ShibbolethTests(TestCase): class TestLocal(TestCase): def setUp(self): - settings.ADMINS = (('admin', 'support@cloud.synnefo.org'),) + settings.ADMINS = ADMINS + settings.ACCOUNT_PENDING_MODERATION_RECIPIENTS = ADMINS + settings.ACCOUNT_ACTIVATED_RECIPIENTS = ADMINS settings.SERVER_EMAIL = 'no-reply@synnefo.org' self._orig_moderation = astakos_settings.MODERATION_ENABLED settings.ASTAKOS_MODERATION_ENABLED = True @@ -491,7 +488,7 @@ class TestLocal(TestCase): self.assertFalse(user.is_active) # user (but not admin) gets notified - self.assertEqual(len(get_mailbox('support@cloud.synnefo.org')), 0) + self.assertEqual(len(get_mailbox('admin@synnefo.org')), 0) self.assertEqual(len(get_mailbox('kpap@synnefo.org')), 1) astakos_settings.MODERATION_ENABLED = True @@ -529,7 +526,9 @@ class TestLocal(TestCase): form = forms.LocalUserCreationForm(data) self.assertFalse(form.is_valid()) - @im_settings(HELPDESK=(('support', 'support@synnefo.org'),), + @im_settings(HELPDESK=HELPDESK, + ACCOUNT_PENDING_MODERATION_RECIPIENTS=HELPDESK, + ACCOUNT_ACTIVATED_RECIPIENTS=HELPDESK, FORCE_PROFILE_UPDATE=False, MODERATION_ENABLED=True) def test_local_provider(self): self.helpdesk_email = astakos_settings.HELPDESK[0][1] @@ -671,7 +670,7 @@ class TestLocal(TestCase): self.assertFalse(r.context['request'].user.is_authenticated()) self.assertFalse(self.client.cookies.get('_pithos2_a').value) - #https://docs.djangoproject.com/en/dev/topics/testing/#persistent-state + # https://docs.djangoproject.com/en/dev/topics/testing/#persistent-state del self.client.cookies['_pithos2_a'] # user can login @@ -890,7 +889,9 @@ class TestAuthProviderViews(TestCase): @shibboleth_settings(CREATION_GROUPS_POLICY=['academic-login'], AUTOMODERATE_POLICY=True) @im_settings(IM_MODULES=['shibboleth', 'local'], MODERATION_ENABLED=True, - HELPDESK=(('support', 'support@synnefo.org'),), + HELPDESK=HELPDESK, + ACCOUNT_PENDING_MODERATION_RECIPIENTS=HELPDESK, + ACCOUNT_ACTIVATED_RECIPIENTS=HELPDESK, FORCE_PROFILE_UPDATE=False) def test_user(self): Profile = AuthProviderPolicyProfile @@ -1296,6 +1297,12 @@ class TestAuthProvidersAPI(TestCase): self.assertEqual(provider.get_not_active_msg, "'Academic login' is disabled.") + user = get_local_user(u'kpap@s\u1e6bbynnefo.org') + provider = auth_providers.get_provider('shibboleth', user, u'kpap@s\u1e6bynnefo.org') + self.assertEqual(provider.get_method_details_msg, u'Account: kpap@s\u1e6bynnefo.org') + self.assertEqual(provider.get_username_msg, u'kpap@s\u1e6bynnefo.org') + + @im_settings(IM_MODULES=['local', 'shibboleth']) @shibboleth_settings(LIMIT_POLICY=2) def test_templates(self): @@ -1347,12 +1354,13 @@ class TestActivationBackend(TestCase): self.assertEqual(user3.moderated, True) self.assertEqual(user3.accepted_policy, 'auth_provider_shibboleth') - @im_settings(MODERATION_ENABLED=False, - MANAGERS=(('Manager', - 'manager@synnefo.org'),), - HELPDESK=(('Helpdesk', - 'helpdesk@synnefo.org'),), - ADMINS=(('Admin', 'admin@synnefo.org'), )) + @im_settings( + MODERATION_ENABLED=False, + MANAGERS=MANAGERS, + HELPDESK=HELPDESK, + ADMINS=ADMINS, + ACCOUNT_PENDING_MODERATION_RECIPIENTS=MANAGERS+HELPDESK+ADMINS, + ACCOUNT_ACTIVATED_RECIPIENTS=MANAGERS+HELPDESK+ADMINS) def test_without_moderation(self): backend = activation_backends.get_backend() form = backend.get_signup_form('local') @@ -1400,12 +1408,13 @@ class TestActivationBackend(TestCase): self.assertEqual(user.email_verified, True) self.assertTrue(user.activation_sent) - @im_settings(MODERATION_ENABLED=True, - MANAGERS=(('Manager', - 'manager@synnefo.org'),), - HELPDESK=(('Helpdesk', - 'helpdesk@synnefo.org'),), - ADMINS=(('Admin', 'admin@synnefo.org'), )) + @im_settings( + MODERATION_ENABLED=True, + MANAGERS=MANAGERS, + HELPDESK=HELPDESK, + ADMINS=ADMINS, + ACCOUNT_PENDING_MODERATION_RECIPIENTS=HELPDESK+MANAGERS+ADMINS, + ACCOUNT_ACTIVATED_RECIPIENTS=HELPDESK+MANAGERS+ADMINS) def test_with_moderation(self): backend = activation_backends.get_backend() @@ -1537,7 +1546,7 @@ class TestWebloginRedirect(TestCase): AstakosUser.objects.get().auth_token) # does not contain uuid # reverted for 0.14.2 to support old pithos desktop clients - #self.assertFalse('uuid' in params) + # self.assertFalse('uuid' in params) # invalid cases r = self.client.get(invalid_scheme, follow=True) diff --git a/snf-astakos-app/astakos/im/tests/common.py b/snf-astakos-app/astakos/im/tests/common.py index 4fdee77087da6a652db99a25c091359ddf090f96..be864b05c3544a134c523f309d2b64b7e02af496 100644 --- a/snf-astakos-app/astakos/im/tests/common.py +++ b/snf-astakos-app/astakos/im/tests/common.py @@ -1,35 +1,17 @@ -# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from contextlib import contextmanager diff --git a/snf-astakos-app/astakos/im/tests/management/__init__.py b/snf-astakos-app/astakos/im/tests/management/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a4e86c4d66c31dd8b763be0284669e1b65bf3710 --- /dev/null +++ b/snf-astakos-app/astakos/im/tests/management/__init__.py @@ -0,0 +1,18 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# flake8: noqa +from .user_modify import * +from .user_activation_send import * diff --git a/snf-astakos-app/astakos/im/tests/management/common.py b/snf-astakos-app/astakos/im/tests/management/common.py new file mode 100644 index 0000000000000000000000000000000000000000..62426195938731dda8bfa6d6d937248d068aba91 --- /dev/null +++ b/snf-astakos-app/astakos/im/tests/management/common.py @@ -0,0 +1,87 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from StringIO import StringIO + +from django.test import TestCase +from django.core.management import call_command + +# Django has tests on user-defined management commands, which should be a good +# guide for our tests. +# +# These tests can be found here: +# https://github.com/django/django/blob/master/tests/user_commands/tests.py + +from astakos.im.auth import make_local_user +from astakos.im.models import AstakosUser + + +class SynnefoManagementTestCase(TestCase): + + """Base class for testing synnefo management commands. + + This class provides a few useful assertions and functions that should aid + in the testing of management commands. + """ + + def assertInLog(self, message, log): + """Assert if log has a string.""" + self.assertIn(message, log.getvalue()) + + def assertEmptyLog(self, log): + """Assert if log is empty.""" + self.assertEqual(log.getvalue(), "") + + def setUp(self): + """Common setup method for this test suite.""" + self.user1 = make_local_user("user1@synnefo.org") + + def tearDown(self): + """Common teardown method for this test suite.""" + AstakosUser.objects.all().delete() + + def reload_user(self): + """Reload a cached user instance from the DB. + + Model instances are cached, which means that if we get an instance + of a model and the model gets updated via other means, our instance's + fields will not change. + + The proposed solution is to fetch again the user from the database. + + For more info about this (common) scenario, see this ticket: + + https://code.djangoproject.com/ticket/901 + """ + self.user1 = AstakosUser.objects.get(pk=self.user1.pk) + + +def call_synnefo_command(command, *args, **options): + """Wrapper over Django's `call_command`. + + Its main purpose is to call a command and return its output (stdout, + stderr). + """ + out = StringIO() + err = StringIO() + # Despite calling it from script, a management command may throw a + # SystemExit exception. This exception can be ignored safely. + # + # Note: This has ben fixed in Django 1.5 (see Changelog). + try: + call_command(command, *args, stdout=out, stderr=err, **options) + except SystemExit: + pass + return out, err diff --git a/snf-astakos-app/astakos/im/tests/management/user_activation_send.py b/snf-astakos-app/astakos/im/tests/management/user_activation_send.py new file mode 100644 index 0000000000000000000000000000000000000000..a64ebe09ad559b7e9a5ab5abb56d5108a24e9bca --- /dev/null +++ b/snf-astakos-app/astakos/im/tests/management/user_activation_send.py @@ -0,0 +1,54 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from django.core import mail + +from astakos.im.user_logic import verify + +from .common import SynnefoManagementTestCase, call_synnefo_command + + +def snf_manage(user, **kwargs): + """An easy to use wrapper that simulates snf-manage.""" + id = str(user.pk) + return call_synnefo_command("user-activation-send", *(id,), **kwargs) + + +class TestSendUserActivation(SynnefoManagementTestCase): + + """Class to unit test the "user-activation-send" management command.""" + + def test_send_activation(self): + """Test if verification mail is send appropriately.""" + # Sending a verification mail to an unverified user should work. + out, err = snf_manage(self.user1) + self.reload_user() + self.assertInLog("Activation sent to '%s'" % self.user1.email, err) + + # Check if email is actually sent. + self.assertEqual(len(mail.outbox), 1) + body = mail.outbox[0].body + self.assertIn(self.user1.realname, body) + self.assertIn(self.user1.verification_code, body) + + # Verify the user. + self.assertEqual(len(mail.outbox), 1) + res = verify(self.user1, self.user1.verification_code) + self.assertFalse(res.is_error()) + + # Sending a verification mail to a verified user should fail. + out, err = snf_manage(self.user1) + self.assertInLog("User email already verified '%s'" % self.user1.email, + err) diff --git a/snf-astakos-app/astakos/im/tests/management/user_modify.py b/snf-astakos-app/astakos/im/tests/management/user_modify.py new file mode 100644 index 0000000000000000000000000000000000000000..f7e89bc608526d91cdfc65458d9d47c2b96dd8cd --- /dev/null +++ b/snf-astakos-app/astakos/im/tests/management/user_modify.py @@ -0,0 +1,124 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +from astakos.im.user_logic import (verify, accept, deactivate,) + +from .common import SynnefoManagementTestCase, call_synnefo_command + +actions = { + "reject": {"reject": True}, + "verify": {"verify": True}, + "accept": {"accept": True}, + "activate": {"active": True}, + "deactivate": {"inactive": True}, +} + + +def snf_manage(user, action, **kwargs): + """An easy to use wrapper that simulates snf-manage.""" + kwargs.update(actions[action]) + id = str(user.pk) + return call_synnefo_command("user-modify", *(id,), **kwargs) + + +class TestUserModification(SynnefoManagementTestCase): + + """Class to unit test the functionality of "user-modify".""" + + def test_verify(self): + """Test verification option.""" + # Verifying the user should work. + out, err = snf_manage(self.user1, "verify") + self.assertInLog("Account verified", err) + + # Verifying the user again should fail. + out, err = snf_manage(self.user1, "verify") + self.assertInLog("Failed to verify", err) + + def test_accept(self): + """Test accept option.""" + # Verify the user first. + res = verify(self.user1, self.user1.verification_code) + self.assertFalse(res.is_error()) + + # Accepting the user should work. + out, err = snf_manage(self.user1, "accept") + self.assertInLog("Account accepted and activated", err) + + # Accepting the user again should fail. + out, err = snf_manage(self.user1, "accept") + self.assertInLog("Failed to accept", err) + + def test_reject(self): + """Test reject option.""" + # Verify the user first + res = verify(self.user1, self.user1.verification_code) + self.assertFalse(res.is_error()) + + # Rejecting the user should work. + out, err = snf_manage(self.user1, "reject", reject_reason="Because") + self.assertInLog("Account rejected", err) + self.reload_user() + self.assertEqual(self.user1.rejected_reason, "Because") + + # Rejecting the user again should fail. + out, err = snf_manage(self.user1, "reject", + reject_reason="Oops, I did it again") + self.assertInLog("Failed to reject", err) + self.reload_user() + self.assertEqual(self.user1.rejected_reason, "Because") + + def test_deactivate(self): + """Test deactivate option.""" + # Verify and accept the user first + res = verify(self.user1, self.user1.verification_code) + self.assertFalse(res.is_error()) + res = accept(self.user1) + self.assertFalse(res.is_error()) + + # Deactivating the user should work. + out, err = snf_manage(self.user1, "deactivate", + inactive_reason="Because") + self.assertInLog("Account %s deactivated" % self.user1.username, err) + self.reload_user() + self.assertEqual(self.user1.deactivated_reason, "Because") + + # Deactivating the user again should also work. + out, err = snf_manage(self.user1, "deactivate", + inactive_reason="Oops, I did it again") + self.assertInLog("Account %s deactivated" % self.user1.username, err) + self.reload_user() + self.assertEqual(self.user1.deactivated_reason, "Oops, I did it again") + + def test_reactivate(self): + """Test activate option.""" + # Verify and accept the user first + res = verify(self.user1, self.user1.verification_code) + self.assertFalse(res.is_error()) + res = accept(self.user1) + self.assertFalse(res.is_error()) + + # Activating the user should fail. + out, err = snf_manage(self.user1, "activate") + self.assertInLog("Failed to activate", err) + + # Deactivate the user in order to reactivate him/her. + res = deactivate(self.user1) + self.assertFalse(res.is_error()) + + # Activating the user should work. + out, err = snf_manage(self.user1, "activate") + self.assertInLog("Account %s activated" % self.user1.username, err) diff --git a/snf-astakos-app/astakos/im/tests/projects.py b/snf-astakos-app/astakos/im/tests/projects.py index 4fb7237a69062e0585e538b1c0709f70c05c1c7a..e210371f2700ced63d7ad605cf33c0f7a346aeef 100644 --- a/snf-astakos-app/astakos/im/tests/projects.py +++ b/snf-astakos-app/astakos/im/tests/projects.py @@ -1,60 +1,58 @@ -# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved. +# -*- coding: utf-8 -*- +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from astakos.im.tests.common import * +NotFound = type('NotFound', (), {}) + + +def find(f, seq): + for item in seq: + if f(item): + return item + return NotFound + + +def get_pending_apps(user): + return quotas.get_user_quotas(user)\ + [user.base_project.uuid]['astakos.pending_app']['usage'] + + class ProjectAPITest(TestCase): def setUp(self): self.client = Client() component1 = Component.objects.create(name="comp1") - register.add_service(component1, "service1", "type1", []) + register.add_service(component1, "σÎÏβις1", "type1", []) # custom service resources - resource11 = {"name": "service1.resource11", - "desc": "resource11 desc", + resource11 = {"name": u"σÎÏβις1.ÏίσοÏÏ‚11", + "desc": u"ÏίσοÏÏ‚11 desc", "service_type": "type1", - "service_origin": "service1", + "service_origin": u"σÎÏβις1", "ui_visible": True} r, _ = register.add_resource(resource11) - register.update_resources([(r, 100)]) - resource12 = {"name": "service1.resource12", - "desc": "resource11 desc", + register.update_base_default(r, 100) + resource12 = {"name": u"σÎÏβις1.resource12", + "desc": "resource12 desc", "service_type": "type1", - "service_origin": "service1", + "service_origin": u"σÎÏβις1", "unit": "bytes"} r, _ = register.add_resource(resource12) - register.update_resources([(r, 1024)]) + register.update_base_default(r, 1024) # create user self.user1 = get_local_user("test@grnet.gr") @@ -73,9 +71,10 @@ class ProjectAPITest(TestCase): "ui_visible": False, "api_visible": False} r, _ = register.add_resource(pending_app) - register.update_resources([(r, 3)]) - accepted = AstakosUser.objects.accepted() - quotas.update_base_quota(accepted, r.name, 3) + register.update_base_default(r, 3) + request = {"resources": {r.name: {"member_capacity": 3, + "project_capacity": 3}}} + functions.modify_projects_in_bulk(Q(is_base=True), request) def create(self, app, headers): dump = json.dumps(app) @@ -87,27 +86,22 @@ class ProjectAPITest(TestCase): def modify(self, app, project_id, headers): dump = json.dumps(app) kwargs = {"project_id": project_id} - r = self.client.post(reverse("api_project", kwargs=kwargs), dump, - content_type="application/json", **headers) + r = self.client.put(reverse("api_project", kwargs=kwargs), dump, + content_type="application/json", **headers) body = json.loads(r.content) return r.status_code, body - def project_action(self, project_id, action, headers): - action = json.dumps({action: "reason"}) + def project_action(self, project_id, action, app_id=None, headers=None): + action_data = {"reason": ""} + if app_id is not None: + action_data["app_id"] = app_id + action = json.dumps({action: action_data}) r = self.client.post(reverse("api_project_action", kwargs={"project_id": project_id}), action, content_type="application/json", **headers) return r.status_code - def app_action(self, app_id, action, headers): - action = json.dumps({action: "reason"}) - r = self.client.post(reverse("api_application_action", - kwargs={"app_id": app_id}), - action, content_type="application/json", - **headers) - return r.status_code - def memb_action(self, memb_id, action, headers): action = json.dumps({action: "reason"}) r = self.client.post(reverse("api_membership_action", @@ -148,25 +142,24 @@ class ProjectAPITest(TestCase): r = client.get(reverse("api_project", kwargs={"project_id": 1}), **h_owner) self.assertEqual(r.status_code, 404) - r = client.get(reverse("api_application", kwargs={"app_id": 1}), - **h_owner) - self.assertEqual(r.status_code, 404) - r = client.get(reverse("api_membership", kwargs={"memb_id": 1}), + r = client.get(reverse("api_membership", kwargs={"memb_id": 100}), **h_owner) self.assertEqual(r.status_code, 404) status = self.memb_action(1, "accept", h_admin) - self.assertEqual(status, 404) + self.assertEqual(status, 409) app1 = {"name": "test.pr", - "end_date": "2013-5-5T20:20:20Z", + "description": u"δεσκÏίπτιον", + "end_date": "2113-5-5T20:20:20Z", "join_policy": "auto", "max_members": 5, - "resources": {"service1.resource11": { + "resources": {u"σÎÏβις1.ÏίσοÏÏ‚11": { + "project_capacity": 1024, "member_capacity": 512}} } - status, body = self.modify(app1, 1, h_owner) + status, body = self.modify(app1, 100, h_owner) self.assertEqual(status, 404) # Create @@ -182,12 +175,15 @@ class ProjectAPITest(TestCase): self.assertEqual(r.status_code, 200) body = json.loads(r.content) self.assertEqual(body["id"], project_id) - self.assertEqual(body["application"], app_id) - self.assertEqual(body["state"], "pending") + self.assertEqual(body["last_application"]["id"], app_id) + self.assertEqual(body["last_application"]["state"], "pending") + self.assertEqual(body["state"], "uninitialized") self.assertEqual(body["owner"], self.user1.uuid) + self.assertEqual(body["description"], u"δεσκÏίπτιον") # Approve forbidden - status = self.app_action(app_id, "approve", h_owner) + status = self.project_action(project_id, "approve", app_id=app_id, + headers=h_owner) self.assertEqual(status, 403) # Create another with the same name @@ -201,6 +197,7 @@ class ProjectAPITest(TestCase): app_p3["name"] = "new.pr" status, body = self.create(app_p3, h_owner) self.assertEqual(status, 201) + project3_id = body["id"] project3_app_id = body["application"] # No more pending allowed @@ -208,70 +205,96 @@ class ProjectAPITest(TestCase): self.assertEqual(status, 409) # Cancel - status = self.app_action(project3_app_id, "cancel", h_owner) + status = self.project_action(project3_id, "cancel", + app_id=project3_app_id, headers=h_owner) self.assertEqual(status, 200) - # Modify + # Get project + r = client.get(reverse("api_project", + kwargs={"project_id": project3_id}), + **h_owner) + body = json.loads(r.content) + self.assertEqual(body["state"], "deleted") + + # Modify of uninitialized failed app2 = {"name": "test.pr", "start_date": "2013-5-5T20:20:20Z", - "end_date": "2013-7-5T20:20:20Z", + "end_date": "2113-7-5T20:20:20Z", "join_policy": "moderated", "leave_policy": "auto", "max_members": 3, - "resources": {"service1.resource11": { + "resources": {u"σÎÏβις1.ÏίσοÏÏ‚11": { + "project_capacity": 1024, "member_capacity": 1024}} } - status, body = self.modify(app2, project_id, h_owner) + self.assertEqual(status, 409) + + # Create the project again + status, body = self.create(app2, h_owner) self.assertEqual(status, 201) - self.assertEqual(project_id, body["id"]) - app2_id = body["application"] - assertGreater(app2_id, app_id) + project_id = body["id"] + app_id = body["application"] # Dismiss failed - status = self.app_action(app2_id, "dismiss", h_owner) + status = self.project_action(project_id, "dismiss", app_id, + headers=h_owner) self.assertEqual(status, 409) # Deny - status = self.app_action(app2_id, "deny", h_admin) + status = self.project_action(project_id, "deny", app_id, + headers=h_admin) self.assertEqual(status, 200) - r = client.get(reverse("api_application", kwargs={"app_id": app2_id}), + # Get project + r = client.get(reverse("api_project", + kwargs={"project_id": project_id}), **h_owner) body = json.loads(r.content) - self.assertEqual(body["state"], "denied") + self.assertEqual(body["last_application"]["id"], app_id) + self.assertEqual(body["last_application"]["state"], "denied") + self.assertEqual(body["state"], "uninitialized") # Dismiss - status = self.app_action(app2_id, "dismiss", h_owner) + status = self.project_action(project_id, "dismiss", app_id, + headers=h_owner) self.assertEqual(status, 200) - # Resubmit - status, body = self.modify(app2, project_id, h_owner) + # Get project + r = client.get(reverse("api_project", + kwargs={"project_id": project_id}), + **h_owner) + body = json.loads(r.content) + self.assertEqual(body["last_application"]["id"], app_id) + self.assertEqual(body["last_application"]["state"], "dismissed") + self.assertEqual(body["state"], "deleted") + + # Create the project again + status, body = self.create(app2, h_owner) self.assertEqual(status, 201) - app3_id = body["application"] + project_id = body["id"] + app_id = body["application"] # Approve - status = self.app_action(app3_id, "approve", h_admin) + status = self.project_action(project_id, "approve", app_id, + headers=h_admin) self.assertEqual(status, 200) - # Get related apps - req = {"body": json.dumps({"project": project_id})} - r = client.get(reverse("api_applications"), req, **h_owner) - self.assertEqual(r.status_code, 200) - body = json.loads(r.content) - self.assertEqual(len(body), 3) - - # Get apps - r = client.get(reverse("api_applications"), **h_owner) - self.assertEqual(r.status_code, 200) + # Check memberships + r = client.get(reverse("api_memberships"), **h_plain) body = json.loads(r.content) - self.assertEqual(len(body), 5) + self.assertEqual(len(body), 1) # Enroll status, body = self.enroll(project_id, self.user3, h_owner) self.assertEqual(status, 200) m_plain_id = body["id"] + # Get project + r = client.get(reverse("api_project", + kwargs={"project_id": project_id}), + **h_owner) + body = json.loads(r.content) # Join status, body = self.join(project_id, h_owner) self.assertEqual(status, 200) @@ -280,14 +303,15 @@ class ProjectAPITest(TestCase): # Check memberships r = client.get(reverse("api_memberships"), **h_plain) body = json.loads(r.content) - self.assertEqual(len(body), 1) - m = body[0] + self.assertEqual(len(body), 2) + m = find(lambda m: m["project"] == project_id, body) + self.assertNotEqual(m, NotFound) self.assertEqual(m["user"], self.user3.uuid) self.assertEqual(m["state"], "accepted") r = client.get(reverse("api_memberships"), **h_owner) body = json.loads(r.content) - self.assertEqual(len(body), 2) + self.assertEqual(len(body), 3) # Check membership r = client.get(reverse("api_membership", kwargs={"memb_id": memb_id}), @@ -359,29 +383,34 @@ class ProjectAPITest(TestCase): ## Simple user mode r = client.get(reverse("api_projects"), **h_plain) body = json.loads(r.content) - self.assertEqual(len(body), 1) + self.assertEqual(len(body), 2) p = body[0] with assertRaises(KeyError): p["pending_application"] ## Owner mode - filters = {"filter": {"state": ["active", "cancelled"]}} - req = {"body": json.dumps(filters)} - r = client.get(reverse("api_projects"), req, **h_owner) + filters = {"state": "active"} + r = client.get(reverse("api_projects"), filters, **h_owner) body = json.loads(r.content) self.assertEqual(len(body), 2) - assertIn("pending_application", body[0]) - filters = {"filter": {"state": "pending"}} - req = {"body": json.dumps(filters)} - r = client.get(reverse("api_projects"), req, **h_owner) + filters = {"state": "deleted"} + r = client.get(reverse("api_projects"), filters, **h_owner) body = json.loads(r.content) - self.assertEqual(len(body), 1) - self.assertEqual(body[0]["id"], project2_id) + self.assertEqual(len(body), 2) + + filters = {"state": "uninitialized"} + r = client.get(reverse("api_projects"), filters, **h_owner) + body = json.loads(r.content) + self.assertEqual(len(body), 2) + + filters = {"name": "test.pr"} + r = client.get(reverse("api_projects"), filters, **h_owner) + body = json.loads(r.content) + self.assertEqual(len(body), 4) - filters = {"filter": {"name": "test.pr"}} - req = {"body": json.dumps(filters)} - r = client.get(reverse("api_projects"), req, **h_owner) + filters = {"mode": "member"} + r = client.get(reverse("api_projects"), filters, **h_owner) body = json.loads(r.content) self.assertEqual(len(body), 2) @@ -394,15 +423,15 @@ class ProjectAPITest(TestCase): self.assertEqual(status, 200) # Suspend failed - status = self.project_action(project_id, "suspend", h_owner) + status = self.project_action(project_id, "suspend", headers=h_owner) self.assertEqual(status, 403) # Unsuspend failed - status = self.project_action(project_id, "unsuspend", h_admin) + status = self.project_action(project_id, "unsuspend", headers=h_admin) self.assertEqual(status, 409) # Suspend - status = self.project_action(project_id, "suspend", h_admin) + status = self.project_action(project_id, "suspend", headers=h_admin) self.assertEqual(status, 200) # Cannot view project @@ -411,15 +440,16 @@ class ProjectAPITest(TestCase): self.assertEqual(r.status_code, 403) # Unsuspend - status = self.project_action(project_id, "unsuspend", h_admin) + status = self.project_action(project_id, "unsuspend", headers=h_admin) self.assertEqual(status, 200) # Cannot approve, project with same name exists - status = self.app_action(project2_app_id, "approve", h_admin) + status = self.project_action(project2_id, "approve", project2_app_id, + headers=h_admin) self.assertEqual(status, 409) # Terminate - status = self.project_action(project_id, "terminate", h_admin) + status = self.project_action(project_id, "terminate", headers=h_admin) self.assertEqual(status, 200) # Join failed @@ -427,7 +457,8 @@ class ProjectAPITest(TestCase): self.assertEqual(status, 409) # Can approve now - status = self.app_action(project2_app_id, "approve", h_admin) + status = self.project_action(project2_id, "approve", project2_app_id, + headers=h_admin) self.assertEqual(status, 200) # Join new project @@ -436,8 +467,8 @@ class ProjectAPITest(TestCase): m_project2 = body["id"] # Get memberships of project - body = {"body": json.dumps({"project": project2_id})} - r = client.get(reverse("api_memberships"), body, **h_owner) + filters = {"project": project2_id} + r = client.get(reverse("api_memberships"), filters, **h_owner) body = json.loads(r.content) self.assertEqual(len(body), 1) self.assertEqual(body[0]["id"], m_project2) @@ -447,7 +478,7 @@ class ProjectAPITest(TestCase): self.assertEqual(status, 200) # Reinstate failed - status = self.project_action(project_id, "reinstate", h_admin) + status = self.project_action(project_id, "reinstate", headers=h_admin) self.assertEqual(status, 409) # Rename @@ -461,29 +492,33 @@ class ProjectAPITest(TestCase): r = client.get(reverse("api_project", kwargs={"project_id": project_id}), **h_owner) body = json.loads(r.content) - self.assertEqual(body["application"], app3_id) - self.assertEqual(body["pending_application"], app2_renamed_id) + self.assertEqual(body["last_application"]["id"], app2_renamed_id) self.assertEqual(body["state"], "terminated") assertIn("deactivation_date", body) + self.assertEqual(body["last_application"]["state"], "pending") + self.assertEqual(body["last_application"]["name"], "new.name") + status = self.project_action(project_id, "approve", app2_renamed_id, + headers=h_admin) + self.assertEqual(r.status_code, 200) - # Get application - r = client.get(reverse("api_application", - kwargs={"app_id": app2_renamed_id}), **h_plain) - self.assertEqual(r.status_code, 403) + # Change homepage + status, body = self.modify({"homepage": "new.page"}, + project_id, h_owner) + self.assertEqual(status, 201) - r = client.get(reverse("api_application", - kwargs={"app_id": app2_renamed_id}), **h_owner) - self.assertEqual(r.status_code, 200) + r = client.get(reverse("api_project", + kwargs={"project_id": project_id}), **h_owner) body = json.loads(r.content) - self.assertEqual(body["state"], "pending") - self.assertEqual(body["name"], "new.name") - - # Approve (automatically reinstates) - action = json.dumps({"approve": ""}) - r = client.post(reverse("api_application_action", - kwargs={"app_id": app2_renamed_id}), - action, content_type="application/json", **h_admin) + self.assertEqual(body["homepage"], "") + self.assertEqual(body["last_application"]["homepage"], "new.page") + homepage_app = body["last_application"]["id"] + status = self.project_action(project_id, "approve", homepage_app, + headers=h_admin) self.assertEqual(r.status_code, 200) + r = client.get(reverse("api_project", + kwargs={"project_id": project_id}), **h_owner) + body = json.loads(r.content) + self.assertEqual(body["homepage"], "new.page") # Bad requests r = client.head(reverse("api_projects"), **h_admin) @@ -495,15 +530,11 @@ class ProjectAPITest(TestCase): self.assertEqual(r.status_code, 405) self.assertTrue('Allow' in r) - r = client.head(reverse("api_applications"), **h_admin) - self.assertEqual(r.status_code, 405) - self.assertTrue('Allow' in r) - r = client.head(reverse("api_memberships"), **h_admin) self.assertEqual(r.status_code, 405) self.assertTrue('Allow' in r) - status = self.project_action(1, "nonex", h_owner) + status = self.project_action(1, "nonex", headers=h_owner) self.assertEqual(status, 400) action = json.dumps({"suspend": "", "unsuspend": ""}) @@ -512,86 +543,194 @@ class ProjectAPITest(TestCase): action, content_type="application/json", **h_owner) self.assertEqual(r.status_code, 400) - ap = {"owner": "nonex", - "join_policy": "nonex", - "leave_policy": "nonex", - "start_date": "nonex", - "homepage": {}, - "max_members": -3, - "resources": [], - } + ap_base = { + "owner": self.user1.uuid, + "name": "domain.name", + "join_policy": "auto", + "leave_policy": "closed", + "start_date": "2113-01-01T0:0Z", + "end_date": "2114-01-01T0:0Z", + "max_members": 0, + "resources": { + u"σÎÏβις1.ÏίσοÏÏ‚11": { + "member_capacity": 512, + "project_capacity": 1024} + }, + } + status, body = self.create(ap_base, h_owner) + project_b_id = body["id"] + app_b_id = body["application"] + self.assertEqual(status, 201) + + # Cancel + status = self.project_action(project_b_id, "cancel", + app_id=app_b_id, headers=h_owner) + self.assertEqual(status, 200) + + ap = copy.deepcopy(ap_base) + ap["owner"] = "nonex" status, body = self.create(ap, h_owner) self.assertEqual(status, 400) self.assertEqual(body["badRequest"]["message"], "User does not exist.") - ap["owner"] = self.user1.uuid + ap = copy.deepcopy(ap_base) + ap.pop("name") + status, body = self.create(ap, h_owner) + self.assertEqual(status, 400) + + ap = copy.deepcopy(ap_base) + ap["name"] = "non_domain_name" + status, body = self.create(ap, h_owner) + self.assertEqual(status, 400) + + ap = copy.deepcopy(ap_base) + ap["name"] = 100 * "domain.name." + ".org" status, body = self.create(ap, h_owner) self.assertEqual(status, 400) - ap["name"] = "some.name" + ap = copy.deepcopy(ap_base) + ap["join_policy"] = "nonex" status, body = self.create(ap, h_owner) self.assertEqual(status, 400) - ap["join_policy"] = "auto" + ap = copy.deepcopy(ap_base) + ap["leave_policy"] = "nonex" status, body = self.create(ap, h_owner) self.assertEqual(status, 400) - ap["leave_policy"] = "closed" + ap = copy.deepcopy(ap_base) + ap.pop("end_date") status, body = self.create(ap, h_owner) self.assertEqual(status, 400) - ap["start_date"] = "2013-01-01T0:0Z" + ap = copy.deepcopy(ap_base) + ap["end_date"] = "2000-01-01T0:0Z" status, body = self.create(ap, h_owner) self.assertEqual(status, 400) - ap["end_date"] = "2014-01-01T0:0Z" + ap = copy.deepcopy(ap_base) + ap["start_date"] = "nonex" status, body = self.create(ap, h_owner) self.assertEqual(status, 400) - ap["max_members"] = 0 + ap = copy.deepcopy(ap_base) + ap["max_members"] = -3 status, body = self.create(ap, h_owner) self.assertEqual(status, 400) - ap["homepage"] = "a.stri.ng" + ap = copy.deepcopy(ap_base) + ap["max_members"] = 2**63 status, body = self.create(ap, h_owner) self.assertEqual(status, 400) + ap = copy.deepcopy(ap_base) + ap["homepage"] = 100 * "huge" + status, body = self.create(ap, h_owner) + self.assertEqual(status, 400) + + ap = copy.deepcopy(ap_base) ap["resources"] = {42: 42} status, body = self.create(ap, h_owner) self.assertEqual(status, 400) - ap["resources"] = {"service1.resource11": {"member_capacity": 512}} + ap = copy.deepcopy(ap_base) + ap["resources"] = {u"σÎÏβις1.ÏίσοÏÏ‚11": {"member_capacity": 512}} status, body = self.create(ap, h_owner) - self.assertEqual(status, 201) + self.assertEqual(status, 400) - ap["name"] = "non_domain_name" + ap = copy.deepcopy(ap_base) + ap["resources"] = {u"σÎÏβις1.ÏίσοÏÏ‚11": {"member_capacity": -512, + "project_capacity": 256}} status, body = self.create(ap, h_owner) self.assertEqual(status, 400) - ap["name"] = "domain.name" - ap.pop("max_members") + ap = copy.deepcopy(ap_base) + ap["resources"] = {u"σÎÏβις1.ÏίσοÏÏ‚11": {"member_capacity": 512, + "project_capacity": 256}} status, body = self.create(ap, h_owner) self.assertEqual(status, 400) - filters = {"filter": {"state": "nonex"}} - req = {"body": json.dumps(filters)} - r = client.get(reverse("api_projects"), req, **h_owner) + filters = {"state": "nonex"} + r = client.get(reverse("api_projects"), filters, **h_owner) self.assertEqual(r.status_code, 400) - filters = {"filter": {"nonex": "nonex"}} - req = {"body": json.dumps(filters)} - r = client.get(reverse("api_projects"), req, **h_owner) - self.assertEqual(r.status_code, 400) + app = {"max_members": 33, "name": "new.name"} + status, body = self.modify(app, self.user1.uuid, h_owner) + self.assertEqual(status, 403) - req = {"body": json.dumps({"project": "nonex"})} - r = client.get(reverse("api_applications"), req, **h_owner) + app = {"max_members": 33, "name": "new.name"} + status, body = self.modify(app, self.user1.uuid, h_admin) + self.assertEqual(status, 409) + + app = {"max_members": 33} + status, body = self.modify(app, self.user1.uuid, h_admin) + self.assertEqual(status, 201) + + # directly modify a base project + with assertRaises(functions.ProjectBadRequest): + functions.modify_project(self.user1.uuid, + {"description": "new description", + "member_join_policy": + functions.MODERATED_POLICY}) + functions.modify_project(self.user1.uuid, + {"member_join_policy": + functions.MODERATED_POLICY}) + r = client.get(reverse("api_project", + kwargs={"project_id": self.user1.uuid}), + **h_owner) + body = json.loads(r.content) + self.assertEqual(body["join_policy"], "moderated") + + r = self.client.post(reverse("api_projects"), "\xff", + content_type="application/json", **h_owner) self.assertEqual(r.status_code, 400) - req = {"body": json.dumps({"project": "nonex"})} - r = client.get(reverse("api_memberships"), req, **h_owner) + r = self.client.post(reverse("api_project_action", + kwargs={"project_id": "1234"}), + "\"nondict\"", content_type="application/json", + **h_owner) self.assertEqual(r.status_code, 400) + r = client.get(reverse("api_project", + kwargs={"project_id": u"Ï€Ïότζεκτ"}), + **h_owner) + self.assertEqual(r.status_code, 404) + + # Check pending app quota integrity + r = client.get(reverse("api_project", + kwargs={"project_id": project_id}), + **h_owner) + body = json.loads(r.content) + self.assertNotEqual(body['last_application']['state'], 'pending') + + admin_pa0 = get_pending_apps(self.user2) + owner_pa0 = get_pending_apps(self.user1) + + app = {"max_members": 11} + status, body = self.modify(app, project_id, h_admin) + self.assertEqual(status, 201) + + admin_pa1 = get_pending_apps(self.user2) + owner_pa1 = get_pending_apps(self.user1) + self.assertEqual(admin_pa1, admin_pa0+1) + self.assertEqual(owner_pa1, owner_pa0) + status, body = self.modify(app, project_id, h_owner) + self.assertEqual(status, 201) + + admin_pa2 = get_pending_apps(self.user2) + owner_pa2 = get_pending_apps(self.user1) + self.assertEqual(admin_pa2, admin_pa1-1) + self.assertEqual(owner_pa2, owner_pa1+1) + + status, body = self.modify(app, project_id, h_owner) + self.assertEqual(status, 201) + + admin_pa3 = get_pending_apps(self.user2) + owner_pa3 = get_pending_apps(self.user1) + self.assertEqual(admin_pa3, admin_pa2) + self.assertEqual(owner_pa3, owner_pa2) + class TestProjects(TestCase): """ @@ -601,6 +740,7 @@ class TestProjects(TestCase): # astakos resources self.resource = Resource.objects.create(name="astakos.pending_app", uplimit=0, + project_default=0, ui_visible=False, api_visible=False, service_type="astakos") @@ -608,6 +748,7 @@ class TestProjects(TestCase): # custom service resources self.resource = Resource.objects.create(name="service1.resource", uplimit=100, + project_default=0, service_type="service1") self.admin = get_local_user("projects-admin@synnefo.org") self.admin.uuid = 'uuid1' @@ -654,10 +795,10 @@ class TestProjects(TestCase): 'end_date': dto.strftime("%Y-%m-%d"), 'member_join_policy': 2, 'member_leave_policy': 1, - 'limit_on_members_number': 5, - 'service1.resource_uplimit': 100, + 'limit_on_members_number_0': 5, + 'service1.resource_m_uplimit': 100, 'is_selected_service1.resource': "1", - 'astakos.pending_app_uplimit': 100, + 'astakos.pending_app_m_uplimit': 100, 'is_selected_accounts': "1", 'user': self.user.pk } @@ -665,7 +806,7 @@ class TestProjects(TestCase): # form is invalid self.assertEqual(form.is_valid(), False) - del application_data['astakos.pending_app_uplimit'] + del application_data['astakos.pending_app_m_uplimit'] del application_data['is_selected_accounts'] form = forms.ProjectApplicationForm(data=application_data) self.assertEqual(form.is_valid(), True) @@ -673,7 +814,16 @@ class TestProjects(TestCase): @im_settings(PROJECT_ADMINS=['uuid1']) def test_applications(self): # let user have 2 pending applications - quotas.update_base_quota([self.user], 'astakos.pending_app', 2) + + # TODO figure this out + request = { + "resources": { + "astakos.pending_app": { + "member_capacity": 2, + "project_capacity": 2} + } + } + functions.modify_project(self.user.uuid, request) r = self.user_client.get(reverse('project_add'), follow=True) self.assertRedirects(r, reverse('project_add')) @@ -689,13 +839,17 @@ class TestProjects(TestCase): 'end_date': dto.strftime("%Y-%m-%d"), 'member_join_policy': 2, 'member_leave_policy': 1, - 'service1.resource_uplimit': 100, + 'limit_on_members_number_0': '5', + 'service1.resource_m_uplimit': 10, + 'service1.resource_p_uplimit': 100, 'is_selected_service1.resource': "1", 'user': self.user.pk } r = self.user_client.post(post_url, data=application_data, follow=True) self.assertEqual(r.status_code, 200) - self.assertEqual(r.context['form'].is_valid(), False) + form = r.context['form'] + form.is_valid() + self.assertEqual(r.context['form'].is_valid(), True) application_data['limit_on_members_number'] = 5 r = self.user_client.post(post_url, data=application_data, follow=True) @@ -724,39 +878,49 @@ class TestProjects(TestCase): self.assertContains(r, "You are not allowed to create a new project") # one project per application - self.assertEqual(Project.objects.count(), 2) + self.assertEqual(Project.objects.filter(is_base=False).count(), 2) # login self.admin_client.get(reverse("edit_profile")) + # admin approves r = self.admin_client.post(reverse('project_app_approve', - kwargs={'application_id': app1_id}), + kwargs={ + 'application_id': app1_id, + 'project_uuid': app1.chain.uuid}), follow=True) self.assertEqual(r.status_code, 200) - - Q_ACTIVE = Project.o_state_q(Project.O_ACTIVE) - self.assertEqual(Project.objects.filter(Q_ACTIVE).count(), 1) + self.assertEqual(Project.objects.filter(is_base=False, + state=Project.O_ACTIVE).count(), 1) # login self.member_client.get(reverse("edit_profile")) # cannot join project2 (not approved yet) - join_url = reverse("project_join", kwargs={'chain_id': project2_id}) + join_url = reverse("project_join", kwargs={ + 'project_uuid': app2.chain.uuid}) r = self.member_client.post(join_url, follow=True) # can join project1 self.member_client.get(reverse("edit_profile")) - join_url = reverse("project_join", kwargs={'chain_id': project1_id}) + join_url = reverse("project_join", kwargs={ + 'project_uuid': app1.chain.uuid}) r = self.member_client.post(join_url, follow=True) self.assertEqual(r.status_code, 200) - memberships = ProjectMembership.objects.all() + memberships = ProjectMembership.objects.filter(project__is_base=False) self.assertEqual(len(memberships), 1) memb_id = memberships[0].id reject_member_url = reverse('project_reject_member', - kwargs={'memb_id': memb_id}) + kwargs={ + 'project_uuid': app1.chain.uuid, + 'memb_id': memb_id + }) accept_member_url = reverse('project_accept_member', - kwargs={'memb_id': memb_id}) + kwargs={ + 'memb_id': memb_id, + 'project_uuid': app1.chain.uuid + }) # only project owner is allowed to reject r = self.member_client.post(reject_member_url, follow=True) @@ -765,38 +929,45 @@ class TestProjects(TestCase): # user (owns project) rejects membership r = self.user_client.post(reject_member_url, follow=True) - self.assertEqual(ProjectMembership.objects.any_accepted().count(), 0) + membs = ProjectMembership.objects.any_accepted().filter( + project__is_base=False) + self.assertEqual(membs.count(), 0) # user rejoins self.member_client.get(reverse("edit_profile")) - join_url = reverse("project_join", kwargs={'chain_id': project1_id}) + join_url = reverse("project_join", kwargs={'project_uuid': + app1.chain.uuid}) r = self.member_client.post(join_url, follow=True) self.assertEqual(r.status_code, 200) self.assertEqual(ProjectMembership.objects.requested().count(), 1) # user (owns project) accepts membership r = self.user_client.post(accept_member_url, follow=True) - self.assertEqual(ProjectMembership.objects.any_accepted().count(), 1) - membership = ProjectMembership.objects.get() + self.assertEqual(membs.count(), 1) + membership = membs.get() self.assertEqual(membership.state, ProjectMembership.ACCEPTED) - user_quotas = quotas.get_users_quotas([self.member]) + user_quotas = quotas.get_users_quotas([self.member]).get( + self.member.uuid).get(app1.chain.uuid) resource = 'service1.resource' - newlimit = user_quotas[self.member.uuid]['system'][resource]['limit'] - # 100 from initial uplimit + 100 from project - self.assertEqual(newlimit, 200) + newlimit = user_quotas[resource]['limit'] + self.assertEqual(newlimit, 10) remove_member_url = reverse('project_remove_member', - kwargs={'memb_id': membership.id}) + kwargs={ + 'project_uuid': app1.chain.uuid, + 'memb_id': membership.id + }) r = self.user_client.post(remove_member_url, follow=True) self.assertEqual(r.status_code, 200) - user_quotas = quotas.get_users_quotas([self.member]) + user_quotas = quotas.get_users_quotas([self.member]).get( + self.member.uuid).get(app1.chain.uuid) resource = 'service1.resource' - newlimit = user_quotas[self.member.uuid]['system'][resource]['limit'] - # 200 - 100 from project - self.assertEqual(newlimit, 100) + newlimit = user_quotas[resource]['limit'] + self.assertEqual(newlimit, 0) + # TODO: handy to be here, but should be moved to a separate test method # support email gets rendered in emails content for mail in get_mailbox('user@synnefo.org'): self.assertTrue(settings.CONTACT_EMAIL in diff --git a/snf-astakos-app/astakos/im/tests/services.py b/snf-astakos-app/astakos/im/tests/services.py index 0e8c82decc94828a974ad4c56f1be83a58ded012..8ab9c87ad2ee63547b5783118e9a42a264411d3f 100644 --- a/snf-astakos-app/astakos/im/tests/services.py +++ b/snf-astakos-app/astakos/im/tests/services.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from astakos.im.tests.common import * from snf_django.utils.testing import assertRaises diff --git a/snf-astakos-app/astakos/im/tests/transactions.py b/snf-astakos-app/astakos/im/tests/transactions.py new file mode 100644 index 0000000000000000000000000000000000000000..87e3a4dcbc13f855f550877928a7b0aaf37673e8 --- /dev/null +++ b/snf-astakos-app/astakos/im/tests/transactions.py @@ -0,0 +1,74 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from django.db import transaction as django_transaction +from django.test import TransactionTestCase +from django.conf import settings + +from astakos.im.models import AstakosUser +from astakos.im import transaction as astakos_transaction +from astakos.im.auth import make_local_user + + +class TransactionException(Exception): + + """A dummy exception specifically for the transaction tests.""" + + pass + + +class TransactionTest(TransactionTestCase): + + """Check if astakos transactions work properly. + + TODO: Add multi-db tests. + """ + + def good_transaction(self): + make_local_user("d@m.my") + + def bad_transaction(self): + self.good_transaction() + raise TransactionException + + def test_good_transaction(self): + django_transaction.commit_on_success(self.good_transaction)() + self.assertEqual(AstakosUser.objects.count(), 1) + + def test_bad_transaction(self): + with self.assertRaises(TransactionException): + django_transaction.commit_on_success(self.bad_transaction)() + self.assertEqual(AstakosUser.objects.count(), 0) + + def test_good_transaction_custom_decorator(self): + astakos_transaction.commit_on_success(self.good_transaction)() + self.assertEqual(AstakosUser.objects.count(), 1) + + def test_bad_transaction_custom_decorator(self): + with self.assertRaises(TransactionException): + astakos_transaction.commit_on_success(self.bad_transaction)() + self.assertEqual(AstakosUser.objects.count(), 0) + + def test_bad_transaction_custom_decorator_incorrect_dbs(self): + settings.DATABASES['astakos'] = settings.DATABASES['default'] + with self.assertRaises(TransactionException): + astakos_transaction.commit_on_success(self.bad_transaction)() + self.assertEqual(AstakosUser.objects.count(), 0) + settings.DATABASES.pop("astakos") + + def test_bad_transaction_custom_decorator_using(self): + with self.assertRaises(TransactionException): + astakos_transaction.commit_on_success(using="default")(self.bad_transaction)() + self.assertEqual(AstakosUser.objects.count(), 0) diff --git a/snf-astakos-app/astakos/im/tests/user_logic.py b/snf-astakos-app/astakos/im/tests/user_logic.py new file mode 100644 index 0000000000000000000000000000000000000000..a7626fcd74046a624b18b03536be0598a4e495f3 --- /dev/null +++ b/snf-astakos-app/astakos/im/tests/user_logic.py @@ -0,0 +1,218 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from django.test import TestCase +from astakos.im.user_logic import (validate_user_action, verify, accept, + activate, deactivate, reject, + send_verification_mail) +from astakos.im.auth import make_local_user +from astakos.im.models import AstakosUser +from snf_django.lib.api import faults +from django.core import mail + + +class TestUserActions(TestCase): + + """Testing various actions on user.""" + + def setUp(self): + """Common setup method for this test suite.""" + self.user1 = make_local_user("user1@synnefo.org") + + def tearDown(self): + """Common teardown method for this test suite.""" + AstakosUser.objects.all().delete() + + def test_verify(self): + """Test verification logic.""" + # Test if check function works properly for unverified user. + ok, _ = validate_user_action(self.user1, "VERIFY", 'badc0d3') + self.assertFalse(ok) + ok, _ = validate_user_action(self.user1, "VERIFY", + self.user1.verification_code) + self.assertTrue(ok) + + # Test if verify action works properly for unverified user. + res = verify(self.user1, 'badc0d3') + self.assertTrue(res.is_error()) + res = verify(self.user1, self.user1.verification_code) + self.assertFalse(res.is_error()) + self.assertEqual(len(mail.outbox), 0) + + # Test if check function fails properly for verified user. + ok, _ = validate_user_action(self.user1, "VERIFY", 'badc0d3') + self.assertFalse(ok) + ok, _ = validate_user_action(self.user1, "VERIFY", + self.user1.verification_code) + self.assertFalse(ok) + + # Test if verify action fails properly for verified user. + res = verify(self.user1, 'badc0d3') + self.assertTrue(res.is_error()) + res = verify(self.user1, self.user1.verification_code) + self.assertTrue(res.is_error()) + self.assertEqual(len(mail.outbox), 0) + + def test_accept(self): + """Test acceptance logic.""" + # Verify the user first. + res = verify(self.user1, self.user1.verification_code) + self.assertFalse(res.is_error()) + + # Test if check function works properly for unmoderated user. + ok, _ = validate_user_action(self.user1, "ACCEPT") + self.assertTrue(ok) + + # Test if accept action works properly for unmoderated user. + res = accept(self.user1) + self.assertFalse(res.is_error()) + self.assertEqual(len(mail.outbox), 1) + + # Test if check function fails properly for moderated user. + ok, _ = validate_user_action(self.user1, "ACCEPT") + self.assertFalse(ok) + + # Test if accept action fails properly for moderated user. + res = accept(self.user1) + self.assertTrue(res.is_error()) + self.assertEqual(len(mail.outbox), 1) + + # Test if the rest of the actions can apply on a moderated user. + # User cannot be rejected. + ok, _ = validate_user_action(self.user1, "REJECT") + self.assertFalse(ok) + res = reject(self.user1, 'Too late') + self.assertTrue(res.is_error()) + + # User cannot be reactivated. + ok, _ = validate_user_action(self.user1, "ACTIVATE") + self.assertFalse(ok) + res = activate(self.user1) + self.assertTrue(res.is_error()) + + def test_rejection(self): + """Test if rejections are handled properly.""" + # Verify the user first. + res = verify(self.user1, self.user1.verification_code) + self.assertFalse(res.is_error()) + + # Check rejection. + ok, _ = validate_user_action(self.user1, "REJECT") + self.assertTrue(ok) + res = reject(self.user1, reason="Because") + self.assertFalse(res.is_error()) + self.assertEqual(len(mail.outbox), 0) + + # Check if reason has been registered. + self.assertEqual(self.user1.rejected_reason, "Because") + + # We cannot reject twice. + ok, _ = validate_user_action(self.user1, "REJECT") + self.assertFalse(ok) + res = reject(self.user1, reason="Because") + self.assertTrue(res.is_error()) + self.assertEqual(len(mail.outbox), 0) + + # We cannot deactivate a rejected user. + ok, _ = validate_user_action(self.user1, "DEACTIVATE") + self.assertFalse(ok) + res = deactivate(self.user1) + self.assertTrue(res.is_error()) + + # We can, however, accept a rejected user. + ok, msg = validate_user_action(self.user1, "ACCEPT") + self.assertTrue(ok) + + # Test if accept action works on rejected users. + res = accept(self.user1) + self.assertFalse(res.is_error()) + self.assertEqual(len(mail.outbox), 1) + + def test_reactivation(self): + """Test activation/deactivation logic.""" + # Verify the user. + res = verify(self.user1, self.user1.verification_code) + self.assertFalse(res.is_error()) + + # We cannot deactivate an unmoderated user. + ok, _ = validate_user_action(self.user1, "DEACTIVATE") + self.assertFalse(ok) + res = deactivate(self.user1) + self.assertTrue(res.is_error()) + + # Accept the user. + res = accept(self.user1) + self.assertFalse(res.is_error()) + + # Check if we can deactivate properly an active user. + ok, _ = validate_user_action(self.user1, "DEACTIVATE") + self.assertTrue(ok) + res = deactivate(self.user1) + self.assertFalse(res.is_error()) + # This should be able to happen many times. + ok, _ = validate_user_action(self.user1, "DEACTIVATE") + self.assertTrue(ok) + res = deactivate(self.user1) + self.assertFalse(res.is_error()) + + # Check if we can activate properly an inactive user + ok, _ = validate_user_action(self.user1, "ACTIVATE") + self.assertTrue(ok) + res = activate(self.user1) + self.assertFalse(res.is_error()) + # This should be able to happen only once. + ok, _ = validate_user_action(self.user1, "ACTIVATE") + self.assertFalse(ok) + res = activate(self.user1) + self.assertTrue(res.is_error()) + + def test_exceptions(self): + """Test if exceptions are raised properly.""" + # For an unverified user, run validate_user_action and check if + # NotAllowed is raised for accept, activate, reject. + for action in ("ACCEPT", "ACTIVATE", "REJECT"): + with self.assertRaises(faults.NotAllowed) as cm: + validate_user_action(self.user1, action, silent=False) + + # Check if BadRequest is raised for a malformed action name. + with self.assertRaises(faults.BadRequest) as cm: + validate_user_action(self.user1, "BAD_ACTION", silent=False) + self.assertEqual(cm.exception.message, "Unknown action: BAD_ACTION.") + + def test_verification_mail(self): + """Test if verification mails are sent correctly.""" + # Check if we can send a verification mail to an unverified user. + ok, _ = validate_user_action(self.user1, "SEND_VERIFICATION_MAIL") + self.assertTrue(ok) + send_verification_mail(self.user1) + + # Check if any mail has been sent and if so, check if it has two + # important properties: the user's realname and his/her verification + # code + self.assertEqual(len(mail.outbox), 1) + body = mail.outbox[0].body + self.assertIn(self.user1.realname, body) + self.assertIn(self.user1.verification_code, body) + + # Verify the user. + res = verify(self.user1, self.user1.verification_code) + self.assertFalse(res.is_error()) + + # Check if we are prevented from sending a verification mail. + ok, _ = validate_user_action(self.user1, "SEND_VERIFICATION_MAIL") + self.assertFalse(ok) + with self.assertRaises(Exception) as cm: + send_verification_mail(self.user1) + self.assertEqual(cm.exception.message, "User email already verified.") diff --git a/snf-astakos-app/astakos/im/tests/user_utils.py b/snf-astakos-app/astakos/im/tests/user_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..728be621ba364e34d45adf219b9bede946e38866 --- /dev/null +++ b/snf-astakos-app/astakos/im/tests/user_utils.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from django.test import TestCase +from django.core import mail +from django.utils.translation import ugettext as _ + +from synnefo_branding.utils import render_to_string +from astakos.im import settings as astakos_settings +from astakos.im.models import AstakosUser +from astakos.im.user_utils import send_plain +from astakos.im.auth import make_local_user +import astakos.im.messages as astakos_messages + + +class TestUserUtils(TestCase): + + """Unit testing of various astakos user utilities.""" + + def setUp(self): + """Common setup method for this test suite.""" + self.user1 = make_local_user("user1@synnefo.org") + + def tearDown(self): + """Common teardown method for this test suite.""" + AstakosUser.objects.all().delete() + + def test_send_plain_email(self): + """Test if send_plain_email function works as intended.""" + def verify_sent_email(email_dict, mail): + """Helper function to verify that an email was sent properly.""" + sender = email_dict.get('sender', astakos_settings.SERVER_EMAIL) + subject = email_dict.get('subject', + _(astakos_messages.PLAIN_EMAIL_SUBJECT)) + self.assertEqual(sender, mail.from_email) + self.assertEqual(subject, mail.subject) + self.assertEqual(email_dict['text'], mail.body) + + # Common variables + template_name = 'im/plain_email.txt' + text = u"Δεσποινίς, που είναι η μπάλα; Ümlaut.)?" + expected_text = render_to_string(template_name, { + 'user': self.user1, + 'text': text, + 'baseurl': astakos_settings.BASE_URL, + 'site_name': astakos_settings.SITENAME, + 'support': astakos_settings.CONTACT_EMAIL}) + + # Test 1 - Check if a simple test mail is sent properly. + send_plain(self.user1, text=text) + self.assertEqual(len(mail.outbox), 1) + body = mail.outbox[0].body + self.assertEqual(expected_text, body) + + # Test 2 - Check if the email template can get overriden. + email_dict = { + 'template_name': None, + 'text': expected_text, + } + + send_plain(self.user1, **email_dict) + self.assertEqual(len(mail.outbox), 2) + verify_sent_email(email_dict, mail.outbox[1]) + + # Test 3 - Check if the email subject can get overriden. + email_dict.update({'subject': u"Το θÎμα μας είναι: Ümlaut."}) + send_plain(self.user1, **email_dict) + self.assertEqual(len(mail.outbox), 3) + verify_sent_email(email_dict, mail.outbox[2]) + + # Test 4 - Check if the email sender can get overriden. + email_dict.update({'sender': "someone@synnefo.org"}) + send_plain(self.user1, **email_dict) + self.assertEqual(len(mail.outbox), 4) + verify_sent_email(email_dict, mail.outbox[3]) diff --git a/snf-astakos-app/astakos/im/tests/views.py b/snf-astakos-app/astakos/im/tests/views.py index b7c843348628c89f84ed32911d64d78d2af1aa7e..7f6104656c96b3ef94923d741e543ce91d986db1 100644 --- a/snf-astakos-app/astakos/im/tests/views.py +++ b/snf-astakos-app/astakos/im/tests/views.py @@ -1,35 +1,17 @@ -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import astakos.im.messages as astakos_messages diff --git a/snf-astakos-app/astakos/im/transaction.py b/snf-astakos-app/astakos/im/transaction.py new file mode 100644 index 0000000000000000000000000000000000000000..c4ace84686db574568c609eec01c1d9e3837df8a --- /dev/null +++ b/snf-astakos-app/astakos/im/transaction.py @@ -0,0 +1,49 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +"""Astakos-specific support for transactions in multiple databases. + +This file provides the entry points for the following Django transaction +functions: + * commit_on_success + * commit_manually + * commit + * rollback +""" + +from django.db import transaction as django_transaction + +from snf_django.utils import transaction as snf_transaction +from snf_django.utils.db import select_db + + +def commit(using=None): + using = select_db("im") if using is None else using + django_transaction.commit(using=using) + + +def rollback(using=None): + using = select_db("im") if using is None else using + django_transaction.rollback(using=using) + + +def commit_on_success(using=None): + method = django_transaction.commit_on_success + return snf_transaction._transaction_func("im", method, using) + + +def commit_manually(using=None): + method = django_transaction.commit_manually + return snf_transaction._transaction_func("im", method, using) diff --git a/snf-astakos-app/astakos/im/urls.py b/snf-astakos-app/astakos/im/urls.py index 860e9af86c5c435e37100742942d873b7e99b1ac..356230538c1b70bc45178a389f164b078944b3c5 100644 --- a/snf-astakos-app/astakos/im/urls.py +++ b/snf-astakos-app/astakos/im/urls.py @@ -1,35 +1,17 @@ -# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.conf.urls import patterns, url from astakos.im.forms import ( @@ -66,50 +48,67 @@ urlpatterns = patterns( # url(r'^billing/?$', 'billing', {}, name='billing'), # url(r'^timeline/?$', 'timeline', {}, name='timeline'), - url(r'^projects/add/?$', 'project_add', {}, name='project_add'), - url(r'^projects/?$', 'project_list', {}, name='project_list'), - url(r'^projects/search/?$', 'project_search', {}, name='project_search'), - url(r'^projects/(?P<chain_id>\d+)/?$', 'project_detail', {}, - name='project_detail'), - url(r'^projects/(?P<chain_id>\d+)/join/?$', 'project_join', {}, - name='project_join'), - url(r'^projects/(?P<chain_id>\d+)/members/?$', 'project_members', {}, - name='project_members'), - url(r'^projects/(?P<chain_id>\d+)/members/approved/?$', 'project_members', - {'members_status_filter': 1}, name='project_approved_members'), - url(r'^projects/(?P<chain_id>\d+)/members/accept/?$', + # projects urls + url(r'^projects/?$', + 'project_list', {}, name='project_list'), + url(r'^projects/add/?$', + 'project_add_or_modify', {}, name='project_add'), + url(r'^projects/search/?$', + 'project_search', {}, name='project_search'), + url(r'^projects/(?P<project_uuid>[^/]+)/?$', + 'project_or_app_detail', {}, name='project_detail'), + + # user project actions + url(r'^projects/(?P<project_uuid>[^/]+)/join/?$', + 'project_join', name='project_join'), + url(r'^projects/(?P<project_uuid>[^/]+)/leave/?$', + 'project_leave', name='project_leave'), + url(r'^projects/(?P<project_uuid>[^/]+)/cancel-join-request/?$', + 'project_cancel_join', name='project_cancel_join'), + + # project members urls + url(r'^projects/(?P<project_uuid>[^/]+)/members/?$', + 'project_members', {}, name='project_members'), + url(r'^projects/(?P<project_uuid>[^/]+)/members/approved/?$', + 'project_members', {'members_status_filter': 1}, + name='project_approved_members'), + url(r'^projects/(?P<project_uuid>[^/]+)/members/pending/?$', + 'project_members', {'members_status_filter': 0}, + name='project_pending_members'), + + # project admin members actions (batch/single) + url(r'^projects/(?P<project_uuid>[^/]+)/members/accept/?$', 'project_members_action', {'action': 'accept'}, name='project_members_accept'), - url(r'^projects/(?P<chain_id>\d+)/members/remove/?$', + url(r'^projects/(?P<project_uuid>[^/]+)/members/remove/?$', 'project_members_action', {'action': 'remove'}, name='project_members_remove'), - url(r'^projects/(?P<chain_id>\d+)/members/reject/?$', + url(r'^projects/(?P<project_uuid>[^/]+)/members/reject/?$', 'project_members_action', {'action': 'reject'}, name='project_members_reject'), - url(r'^projects/(?P<chain_id>\d+)/members/pending/?$', 'project_members', - {'members_status_filter': 0}, name='project_pending_members'), - url(r'^projects/memberships/(?P<memb_id>\d+)/leave/?$', - 'project_leave', {}, name='project_leave'), - url(r'^projects/memberships/(?P<memb_id>\d+)/cancel/?$', - 'project_cancel_member', {}, name='project_cancel_member'), - url(r'^projects/memberships/(?P<memb_id>\d+)/accept/?$', - 'project_accept_member', {}, name='project_accept_member'), - url(r'^projects/memberships/(?P<memb_id>\d+)/reject/?$', - 'project_reject_member', {}, name='project_reject_member'), - url(r'^projects/memberships/(?P<memb_id>\d+)/remove/?$', - 'project_remove_member', {}, name='project_remove_member'), - url(r'^projects/app/(?P<application_id>\d+)/?$', 'project_app', {}, - name='project_app'), - url(r'^projects/app/(?P<application_id>\d+)/modify$', 'project_modify', {}, - name='project_modify'), - url(r'^projects/app/(?P<application_id>\d+)/approve$', + url(r'^projects/(?P<project_uuid>[^/]+)/memberships/(?P<memb_id>\d+)/accept/?$', + 'project_members_action', {'action': 'accept'}, + name='project_accept_member'), + url(r'^projects/(?P<project_uuid>[^/]+)/memberships/(?P<memb_id>\d+)/reject/?$', + 'project_members_action', {'action': 'reject'}, + name='project_reject_member'), + url(r'^projects/(?P<project_uuid>[^/]+)/memberships/(?P<memb_id>\d+)/remove/?$', + 'project_members_action', {'action': 'remove'}, + name='project_remove_member'), + + # project application urls + url(r'^projects/(?P<project_uuid>[^/]+)/app/(?P<app_id>\d+)/?$', + 'project_or_app_detail', {}, name='project_app'), + url(r'^projects/(?P<project_uuid>[^/]+)/modify$', 'project_add_or_modify', + {}, name='project_modify'), + url(r'^projects/(?P<project_uuid>[^/]+)/app/(?P<application_id>\d+)/approve?$', 'project_app_approve', {}, name='project_app_approve'), - url(r'^projects/app/(?P<application_id>\d+)/deny$', 'project_app_deny', {}, - name='project_app_deny'), - url(r'^projects/app/(?P<application_id>\d+)/dismiss$', + url(r'^projects/(?P<project_uuid>[^/]+)/app/(?P<application_id>\d+)/deny?$', + 'project_app_deny', {}, name='project_app_deny'), + url(r'^projects/(?P<project_uuid>[^/]+)/app/(?P<application_id>\d+)/dismiss?$', 'project_app_dismiss', {}, name='project_app_dismiss'), - url(r'^projects/app/(?P<application_id>\d+)/cancel$', 'project_app_cancel', - {}, name='project_app_cancel'), + url(r'^projects/(?P<project_uuid>[^/]+)/app/(?P<application_id>\d+)/cancel?$', + 'project_app_cancel', {}, name='project_app_cancel'), url(r'^projects/how_it_works/?$', 'how_it_works', {}, name='how_it_works'), url(r'^remove_auth_provider/(?P<pk>\d+)?$', 'remove_auth_provider', {}, diff --git a/snf-astakos-app/astakos/im/user_logic.py b/snf-astakos-app/astakos/im/user_logic.py new file mode 100644 index 0000000000000000000000000000000000000000..c9315b3c0d40ed558107f4ef0005a47926e62357 --- /dev/null +++ b/snf-astakos-app/astakos/im/user_logic.py @@ -0,0 +1,64 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +from astakos.im import activation_backends + +activation_backend = activation_backends.get_backend() +validate_user_action = activation_backend.validate_user_action + + +## +# Actions: The necessary logic for actions on a user. Uses extensively +# the activation_backends. +def reject(user, reason): + """Reject a user.""" + res = activation_backend.handle_moderation( + user, accept=False, reject_reason=reason) + activation_backend.send_result_notifications(res, user) + return res + + +def verify(user, verification_code, notify_user=False): + """Verify a user's mail.""" + res = activation_backend.handle_verification(user, verification_code) + if notify_user: + activation_backend.send_result_notifications(res, user) + return res + + +def accept(user): + """Accept a verified user.""" + res = activation_backend.handle_moderation(user, accept=True) + activation_backend.send_result_notifications(res, user) + return res + + +def activate(user): + """Activate an inactive user.""" + res = activation_backend.activate_user(user) + return res + + +def deactivate(user, reason=""): + """Deactivate an active user.""" + res = activation_backend.deactivate_user(user, reason=reason) + return res + + +def send_verification_mail(user): + """Send verification mail to an unverified user.""" + res = activation_backend.send_user_verification_email(user) + return res diff --git a/snf-astakos-app/astakos/im/user_utils.py b/snf-astakos-app/astakos/im/user_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..eab099b33ea50ca897dcf2d996e3866ac3b83740 --- /dev/null +++ b/snf-astakos-app/astakos/im/user_utils.py @@ -0,0 +1,218 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import logging +from django.core.mail import send_mail, get_connection +from django.core.urlresolvers import reverse +from django.contrib.auth import login as auth_login, logout as auth_logout +from django.utils.translation import ugettext as _ + +from synnefo_branding.utils import render_to_string +from synnefo.lib import join_urls + +from astakos.im import settings +from astakos.im.models import Invitation +import astakos.im.messages as astakos_messages + +logger = logging.getLogger(__name__) + + +def login(request, user): + auth_login(request, user) + from astakos.im.models import SessionCatalog + SessionCatalog( + session_key=request.session.session_key, + user=user + ).save() + logger.info('%s logged in.', user.log_display) + + +def logout(request, *args, **kwargs): + user = request.user + auth_logout(request, *args, **kwargs) + user.delete_online_access_tokens() + logger.info('%s logged out.', user.log_display) + + +def invite(inviter, email, realname): + inv = Invitation(inviter=inviter, username=email, realname=realname) + inv.save() + send_invitation(inv) + inviter.invitations = max(0, inviter.invitations - 1) + inviter.save() + + +def send_plain(user, sender=settings.SERVER_EMAIL, + subject=_(astakos_messages.PLAIN_EMAIL_SUBJECT), + template_name='im/plain_email.txt', text=None): + """Send mail to user with fully customizable sender, subject and body. + + If the function is provided with a `template name`, then it will be used + for rendering the mail. Any additional text should be provided in the + `text` parameter and it will be included in the main body of the mail. + + If the function is not provided with a `template name`, then it will use + the string provided in the `text` parameter as the mail body. + """ + if not template_name: + message = text + else: + message = render_to_string(template_name, { + 'user': user, + 'text': text, + 'baseurl': settings.BASE_URL, + 'site_name': settings.SITENAME, + 'support': settings.CONTACT_EMAIL}) + + send_mail(subject, message, sender, [user.email], + connection=get_connection()) + logger.info("Sent plain email to user: %s", user.log_display) + + +def send_verification(user, template_name='im/activation_email.txt'): + """ + Send email to user to verify his/her email and activate his/her account. + """ + index_url = reverse('index', urlconf="synnefo.webproject.urls") + url = join_urls(settings.BASE_HOST, + user.get_activation_url(nxt=index_url)) + message = render_to_string(template_name, { + 'user': user, + 'url': url, + 'baseurl': settings.BASE_URL, + 'site_name': settings.SITENAME, + 'support': settings.CONTACT_EMAIL}) + sender = settings.SERVER_EMAIL + send_mail(_(astakos_messages.VERIFICATION_EMAIL_SUBJECT), message, sender, + [user.email], + connection=get_connection()) + logger.info("Sent user verification email: %s", user.log_display) + + +def send_account_pending_moderation_notification( + user, + template_name='im/account_pending_moderation_notification.txt'): + """ + Notify 'ACCOUNT_PENDING_MODERATION_RECIPIENTS' that a new user has verified + his email address and moderation step is required to activate his account. + """ + subject = (_(astakos_messages.ACCOUNT_CREATION_SUBJECT) % + {'user': user.email}) + message = render_to_string(template_name, {'user': user}) + sender = settings.SERVER_EMAIL + recipient_list = [e[1] for e in + settings.ACCOUNT_PENDING_MODERATION_RECIPIENTS] + send_mail(subject, message, sender, recipient_list, + connection=get_connection()) + msg = 'Sent admin notification (account creation) for user %s' + logger.log(settings.LOGGING_LEVEL, msg, user.log_display) + + +def send_account_activated_notification( + user, + template_name='im/account_activated_notification.txt'): + """ + Send email to ACCOUNT_ACTIVATED_RECIPIENTS list to notify that a new + account has been accepted and activated. + """ + message = render_to_string( + template_name, + {'user': user} + ) + sender = settings.SERVER_EMAIL + recipient_list = [e[1] for e in + settings.ACCOUNT_ACTIVATED_RECIPIENTS] + send_mail(_(astakos_messages.HELPDESK_NOTIFICATION_EMAIL_SUBJECT) % + {'user': user.email}, + message, sender, recipient_list, connection=get_connection()) + msg = 'Sent helpdesk admin notification for %s' + logger.log(settings.LOGGING_LEVEL, msg, user.email) + + +def send_invitation(invitation, template_name='im/invitation.txt'): + """ + Send invitation email. + """ + subject = _(astakos_messages.INVITATION_EMAIL_SUBJECT) + index_url = reverse('index', urlconf="synnefo.webproject.urls") + url = '%s?code=%d' % (join_urls(settings.BASE_HOST, index_url, + invitation.code)) + message = render_to_string(template_name, { + 'invitation': invitation, + 'url': url, + 'baseurl': settings.BASE_URL, + 'site_name': settings.SITENAME, + 'support': settings.CONTACT_EMAIL}) + sender = settings.SERVER_EMAIL + send_mail(subject, message, sender, [invitation.username], + connection=get_connection()) + msg = 'Sent invitation %s' + logger.log(settings.LOGGING_LEVEL, msg, invitation) + inviter_invitations = invitation.inviter.invitations + invitation.inviter.invitations = max(0, inviter_invitations - 1) + invitation.inviter.save() + + +def send_greeting(user, email_template_name='im/welcome_email.txt'): + """ + Send welcome email to an accepted/activated user. + + Raises SMTPException, socket.error + """ + subject = _(astakos_messages.GREETING_EMAIL_SUBJECT) + index_url = reverse('index', urlconf="synnefo.webproject.urls") + message = render_to_string(email_template_name, { + 'user': user, + 'url': join_urls(settings.BASE_HOST, index_url), + 'baseurl': settings.BASE_URL, + 'site_name': settings.SITENAME, + 'support': settings.CONTACT_EMAIL}) + sender = settings.SERVER_EMAIL + send_mail(subject, message, sender, [user.email], + connection=get_connection()) + msg = 'Sent greeting %s' + logger.log(settings.LOGGING_LEVEL, msg, user.log_display) + + +def send_feedback(msg, data, user, email_template_name='im/feedback_mail.txt'): + """Send feedback to FEEDBACK_RECIPIENTS list.""" + subject = _(astakos_messages.FEEDBACK_EMAIL_SUBJECT) + from_email = settings.SERVER_EMAIL + recipient_list = [e[1] for e in settings.FEEDBACK_NOTIFICATIONS_RECIPIENTS] + content = render_to_string(email_template_name, { + 'message': msg, + 'data': data, + 'user': user}) + send_mail(subject, content, from_email, recipient_list, + connection=get_connection()) + msg = 'Sent feedback from %s' + logger.log(settings.LOGGING_LEVEL, msg, user.log_display) + + +def send_change_email(ec, request, email_template_name=( + 'registration/email_change_email.txt')): + url = ec.get_url() + url = request.build_absolute_uri(url) + c = {'url': url, + 'site_name': settings.SITENAME, + 'support': settings.CONTACT_EMAIL, + 'ec': ec} + message = render_to_string(email_template_name, c) + from_email = settings.SERVER_EMAIL + send_mail(_(astakos_messages.EMAIL_CHANGE_EMAIL_SUBJECT), message, + from_email, + [ec.new_email_address], connection=get_connection()) + msg = 'Sent change email for %s' + logger.log(settings.LOGGING_LEVEL, msg, ec.user.log_display) diff --git a/snf-astakos-app/astakos/im/util.py b/snf-astakos-app/astakos/im/util.py index bda81701673f7e3049b189f27aad353dfce1f753..d02808bc555b15914c47e6c0bc33881dd1924546 100644 --- a/snf-astakos-app/astakos/im/util.py +++ b/snf-astakos-app/astakos/im/util.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import logging import time @@ -48,7 +30,7 @@ from django.utils.encoding import iri_to_uri from django.utils.translation import ugettext as _ from astakos.im.models import AstakosUser, Invitation -from astakos.im.functions import login +from astakos.im.user_utils import login from astakos.im import settings import astakos.im.messages as astakos_messages @@ -177,6 +159,15 @@ def restrict_next(url, domain=None, allowed_schemes=()): return None +def restrict_reverse(*args, **kwargs): + """ + Like reverse, with an additional restrict_next call to the reverse result. + """ + domain = kwargs.pop('restrict_domain', settings.COOKIE_DOMAIN) + url = reverse(*args, **kwargs) + return restrict_next(url, domain=domain) + + def prepare_response(request, user, next='', renew=False): """Return the unique username and the token as 'X-Auth-User' and 'X-Auth-Token' headers, @@ -223,22 +214,6 @@ def prepare_response(request, user, next='', renew=False): return response -class lazy_string(object): - def __init__(self, function, *args, **kwargs): - self.function = function - self.args = args - self.kwargs = kwargs - - def __str__(self): - if not hasattr(self, 'str'): - self.str = self.function(*self.args, **self.kwargs) - return self.str - - -def reverse_lazy(*args, **kwargs): - return lazy_string(reverse, *args, **kwargs) - - def reserved_email(email): return AstakosUser.objects.user_exists(email) @@ -345,3 +320,11 @@ def redirect_back(request, default='index'): if referer and safe and not loops: return redirect(referer) return redirect(reverse(default)) + + +def truncatename(v, max=18, append="..."): + length = len(v) + if length > max: + return v[:max] + append + else: + return v diff --git a/snf-astakos-app/astakos/im/views/decorators.py b/snf-astakos-app/astakos/im/views/decorators.py index 2e11fd0a989a7112383985d98d94fb9c9e0db68d..7e002115043d408b2bafb531264c3fa218f04c50 100644 --- a/snf-astakos-app/astakos/im/views/decorators.py +++ b/snf-astakos-app/astakos/im/views/decorators.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from functools import wraps diff --git a/snf-astakos-app/astakos/im/views/im.py b/snf-astakos-app/astakos/im/views/im.py index 0b38afbf2b7b3dac948a9f47cfcd367469ec2ef0..480b601de00dd5352b3a8534f8473572f26b7a27 100644 --- a/snf-astakos-app/astakos/im/views/im.py +++ b/snf-astakos-app/astakos/im/views/im.py @@ -1,40 +1,19 @@ -# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import logging -import inflect - -engine = inflect.engine() from urllib import quote @@ -42,7 +21,8 @@ from django.shortcuts import get_object_or_404 from django.contrib import messages from django.contrib.auth.models import User from django.core.urlresolvers import reverse -from django.db import transaction +from astakos.im import transaction +from django.db.models import Q from django.http import HttpResponse, HttpResponseRedirect, Http404 from django.shortcuts import redirect from django.utils.translation import ugettext as _ @@ -56,16 +36,16 @@ from synnefo_branding import settings as branding_settings import astakos.im.messages as astakos_messages -from astakos.im import activation_backends +from astakos.im import activation_backends, user_logic from astakos.im.models import AstakosUser, ApprovalTerms, EmailChange, \ - AstakosUserAuthProvider, PendingThirdPartyUser, Component + AstakosUserAuthProvider, PendingThirdPartyUser, Component, Project from astakos.im.util import get_context, prepare_response, get_query, \ restrict_next from astakos.im.forms import LoginForm, InvitationForm, FeedbackForm, \ SignApprovalTermsForm, EmailChangeForm from astakos.im.forms import ExtendedProfileForm as ProfileForm from synnefo.lib.services import get_public_endpoint -from astakos.im.functions import send_feedback, logout as auth_logout, \ +from astakos.im.user_utils import send_feedback, logout as auth_logout, \ invite as invite_func from astakos.im import settings from astakos.im import presentation @@ -74,6 +54,8 @@ from astakos.im import quotas from astakos.im.views.util import render_response, _resources_catalog from astakos.im.views.decorators import cookie_fix, signed_terms_required,\ required_auth_methods_assigned, valid_astakos_user_required, login_required +from astakos.api import projects as projects_api +from astakos.api.util import _dthandler logger = logging.getLogger(__name__) @@ -471,7 +453,6 @@ def signup(request, template_name='im/signup.html', on_success='index', form = activation_backend.get_signup_form( provider, None, **form_kwargs) - if request.method == 'POST': form = activation_backend.get_signup_form( provider, @@ -647,9 +628,7 @@ def activate(request, greeting_email_template_name='im/welcome_email.txt', messages.error(request, message) return HttpResponseRedirect(reverse('index')) - backend = activation_backends.get_backend() - result = backend.handle_verification(user, token) - backend.send_result_notifications(result, user) + result = user_logic.verify(user, token, notify_user=True) next = settings.ACTIVATION_REDIRECT_URL or next or reverse('index') if user.is_active: response = prepare_response(request, user, next, renew=True) @@ -826,8 +805,7 @@ def send_activation(request, user_id, template_name='im/login.html', messages.error(request, _(astakos_messages.ACCOUNT_ALREADY_VERIFIED)) else: - activation_backend = activation_backends.get_backend() - activation_backend.send_user_verification_email(u) + user_logic.send_verification_mail(u) messages.success(request, astakos_messages.ACTIVATION_SENT) return HttpResponseRedirect(reverse('index')) @@ -840,9 +818,25 @@ def resource_usage(request): resources_meta = presentation.RESOURCES - current_usage = quotas.get_user_quotas(request.user) - current_usage = json.dumps(current_usage['system']) + # resolve uuids of projects the user consumes quota from + user = request.user + quota_filters = Q(usage_min__gt=0, limit__gt=0) + quota_uuids = map(lambda k: k[1], + quotas.get_users_quotas_counters([user], + flt=quota_filters)[0].keys(),) + # resolve uuids of projects the user is member to + user_memberships = request.user.projectmembership_set.actually_accepted() + membership_uuids = [m.project.uuid for m in user_memberships] + + # merge uuids + uuids = set(quota_uuids + membership_uuids) + uuid_refs = map(quotas.project_ref, uuids) + + user_quotas = quotas.get_user_quotas(request.user, sources=uuid_refs) + projects = Project.objects.filter(uuid__in=uuids) + user_projects = projects_api.get_projects_details(projects) resource_catalog, resource_groups = _resources_catalog() + if resource_catalog is False: # on fail resource_groups contains the result object result = resource_groups @@ -852,16 +846,19 @@ def resource_usage(request): resource_catalog = json.dumps(resource_catalog) resource_groups = json.dumps(resource_groups) resources_order = json.dumps(resources_meta.get('resources_order')) + projects_details = json.dumps(user_projects, default=_dthandler) + user_quotas = json.dumps(user_quotas) + interval = settings.USAGE_UPDATE_INTERVAL return render_response('im/resource_usage.html', context_instance=get_context(request), resource_catalog=resource_catalog, resource_groups=resource_groups, resources_order=resources_order, - current_usage=current_usage, + projects_details=projects_details, + user_quotas=user_quotas, token_cookie_name=settings.COOKIE_NAME, - usage_update_interval= - settings.USAGE_UPDATE_INTERVAL) + usage_update_interval=interval) # TODO: action only on POST and user should confirm the removal @@ -931,12 +928,11 @@ def get_menu(request, with_extra_links=False, with_signout=True): url=request.build_absolute_uri(reverse('resource_usage')), name="Usage")) - if settings.PROJECTS_VISIBLE: - append( - item( - url=request.build_absolute_uri( - reverse('project_list')), - name="Projects")) + append( + item( + url=request.build_absolute_uri( + reverse('project_list')), + name="Projects")) append(item(url=request.build_absolute_uri(reverse('feedback')), name="Contact")) diff --git a/snf-astakos-app/astakos/im/views/projects.py b/snf-astakos-app/astakos/im/views/projects.py index cc862c8b775b0dd0e94413a1b2c47fea56888ac1..6fe931536771b9d058e4dfd6854804a24f73a795 100644 --- a/snf-astakos-app/astakos/im/views/projects.py +++ b/snf-astakos-app/astakos/im/views/projects.py @@ -1,384 +1,298 @@ -# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import logging -import inflect - -engine = inflect.engine() +from functools import wraps from django_tables2 import RequestConfig -from django.shortcuts import get_object_or_404 +from django.shortcuts import get_object_or_404, render_to_response from django.contrib import messages from django.core.urlresolvers import reverse -from django.http import Http404 +from django.http import Http404, HttpResponse from django.shortcuts import redirect from django.utils.html import escape from django.utils.translation import ugettext as _ from django.views.generic.list_detail import object_list, object_detail from django.core.exceptions import PermissionDenied from django.views.decorators.http import require_http_methods -from django.db import transaction +from astakos.im import transaction +from django.template import RequestContext +from django.db.models import Q import astakos.im.messages as astakos_messages from astakos.im import tables from astakos.im.models import ProjectApplication, ProjectMembership, Project -from astakos.im.util import get_context, restrict_next +from astakos.im.util import get_context, restrict_next, restrict_reverse from astakos.im.forms import ProjectApplicationForm, AddProjectMembersForm, \ - ProjectSearchForm + ProjectSearchForm, ProjectModificationForm from astakos.im.functions import check_pending_app_quota, accept_membership, \ reject_membership, remove_membership, cancel_membership, leave_project, \ join_project, enroll_member, can_join_request, can_leave_request, \ get_related_project_id, approve_application, \ - deny_application, cancel_application, dismiss_application, ProjectError + deny_application, cancel_application, dismiss_application, ProjectError, \ + can_cancel_join_request, app_check_allowed, project_check_allowed, \ + ProjectForbidden from astakos.im import settings from astakos.im.util import redirect_back from astakos.im.views.util import render_response, _create_object, \ - _update_object, _resources_catalog, ExceptionHandler + _update_object, _resources_catalog, ExceptionHandler, \ + get_user_projects_table, handle_valid_members_form, redirect_to_next from astakos.im.views.decorators import cookie_fix, signed_terms_required,\ valid_astakos_user_required, login_required +from astakos.api import projects as api +from astakos.im import functions as project_actions + logger = logging.getLogger(__name__) -@cookie_fix -def how_it_works(request): - return render_response( - 'im/how_it_works.html', - context_instance=get_context(request)) +def no_transaction(func): + return func -@require_http_methods(["GET", "POST"]) -@cookie_fix -@valid_astakos_user_required -def project_add(request): - user = request.user - if not user.is_project_admin(): - ok, limit = check_pending_app_quota(user) - if not ok: - m = _(astakos_messages.PENDING_APPLICATION_LIMIT_ADD) % limit - messages.error(request, m) - next = reverse('astakos.im.views.project_list') - next = restrict_next(next, domain=settings.COOKIE_DOMAIN) - return redirect(next) +def handles_project_errors(func): - details_fields = ["name", "homepage", "description", "start_date", - "end_date", "comments"] - membership_fields = ["member_join_policy", "member_leave_policy", - "limit_on_members_number"] - resource_catalog, resource_groups = _resources_catalog() - if resource_catalog is False: - # on fail resource_groups contains the result object - result = resource_groups - messages.error(request, 'Unable to retrieve system resources: %s' % - result.reason) - extra_context = { - 'resource_catalog': resource_catalog, - 'resource_groups': resource_groups, - 'show_form': True, - 'details_fields': details_fields, - 'membership_fields': membership_fields} + @wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except ProjectForbidden: + raise PermissionDenied + return wrapper - response = None - with ExceptionHandler(request): - response = create_app_object(request, extra_context=extra_context) - - if response is not None: - return response - - next = reverse('astakos.im.views.project_list') - next = restrict_next(next, domain=settings.COOKIE_DOMAIN) - return redirect(next) - - -@transaction.commit_on_success -def create_app_object(request, extra_context=None): - try: - summary = 'im/projects/projectapplication_form_summary.html' - return _create_object( - request, - template_name='im/projects/projectapplication_form.html', - summary_template_name=summary, - extra_context=extra_context, - post_save_redirect=reverse('project_list'), - form_class=ProjectApplicationForm, - msg=_("The %(verbose_name)s has been received and " - "is under consideration.")) - except ProjectError as e: - messages.error(request, e) - - -def get_user_projects_table(projects, user, prefix): - apps = ProjectApplication.objects.pending_per_project(projects) - memberships = user.projectmembership_set.one_per_project() - objs = ProjectMembership.objects - accepted_ms = objs.any_accepted_per_project(projects) - requested_ms = objs.requested_per_project(projects) - return tables.UserProjectsTable(projects, user=user, - prefix=prefix, - pending_apps=apps, - memberships=memberships, - accepted=accepted_ms, - requested=requested_ms) - - -@require_http_methods(["GET"]) -@cookie_fix -@valid_astakos_user_required -def project_list(request): - projects = Project.objects.user_accessible_projects(request.user) - table = (get_user_projects_table(projects, user=request.user, - prefix="my_projects_") - if list(projects) else None) - return object_list( - request, - projects, - template_name='im/projects/project_list.html', - extra_context={ - 'is_search': False, - 'table': table, - }) +def project_view(get=True, post=False, transaction=False): + methods = [] + if get: + methods.append("GET") + if post: + methods.append("POST") + if transaction: + transaction_method = transaction.commit_on_success + else: + transaction_method = no_transaction -@require_http_methods(["POST"]) -@cookie_fix -@valid_astakos_user_required -def project_app_cancel(request, application_id): - next = request.GET.get('next') - chain_id = None + def wrapper(func): + return \ + wraps(func)( + require_http_methods(methods)( + cookie_fix( + valid_astakos_user_required( + transaction_method( + handles_project_errors(func)))))) - with ExceptionHandler(request): - chain_id = _project_app_cancel(request, application_id) + return wrapper - if not next: - if chain_id: - next = reverse('astakos.im.views.project_detail', args=(chain_id,)) - else: - next = reverse('astakos.im.views.project_list') - next = restrict_next(next, domain=settings.COOKIE_DOMAIN) - return redirect(next) +@project_view() +def how_it_works(request): + return render_response('im/how_it_works.html', + context_instance=get_context(request)) -@transaction.commit_on_success -def _project_app_cancel(request, application_id): - chain_id = None - try: - application_id = int(application_id) - chain_id = get_related_project_id(application_id) - cancel_application(application_id, request.user) - except ProjectError as e: - messages.error(request, e) +@project_view() +def project_list(request, template_name="im/projects/project_list.html"): + query = api.make_project_query({}) + show_base = request.GET.get('show_base', False) - else: - msg = _(astakos_messages.APPLICATION_CANCELLED) - messages.success(request, msg) - return chain_id + # exclude base projects by default for admin users + if not show_base and request.user.is_project_admin(): + query = query & ~Q(Q(is_base=True) & \ + ~Q(realname="system:%s" % request.user.uuid)) + query = query & ~Q(state=Project.DELETED) + mode = "default" + if not request.user.is_project_admin(): + mode = "related" -@require_http_methods(["GET", "POST"]) -@cookie_fix -@valid_astakos_user_required -def project_modify(request, application_id): + projects = api._get_projects(query, mode=mode, request_user=request.user) + + table = None + if projects.count(): + table = get_user_projects_table(projects, user=request.user, + prefix="my_projects_", request=request) + + context = {'is_search': False, 'table': table} + return object_list(request, projects, template_name=template_name, + extra_context=context) - try: - app = ProjectApplication.objects.get(id=application_id) - except ProjectApplication.DoesNotExist: - raise Http404 +@project_view(post=True) +def project_add_or_modify(request, project_uuid=None): user = request.user - if not (user.owns_application(app) or user.is_project_admin(app.id)): - m = _(astakos_messages.NOT_ALLOWED) - raise PermissionDenied(m) + # only check quota for non project admin users if not user.is_project_admin(): - owner = app.owner - ok, limit = check_pending_app_quota(owner, project=app.chain) + ok, limit = check_pending_app_quota(user) if not ok: - m = _(astakos_messages.PENDING_APPLICATION_LIMIT_MODIFY) % limit + m = _(astakos_messages.PENDING_APPLICATION_LIMIT_ADD) % limit messages.error(request, m) - next = reverse('astakos.im.views.project_list') - next = restrict_next(next, domain=settings.COOKIE_DOMAIN) - return redirect(next) + return redirect(restrict_reverse( + 'astakos.im.views.project_list')) + + project = None + if project_uuid: + project = get_object_or_404(Project, uuid=project_uuid) + + if not user.owns_project(project) and not user.is_project_admin(): + m = _(astakos_messages.NOT_ALLOWED) + raise PermissionDenied(m) details_fields = ["name", "homepage", "description", "start_date", "end_date", "comments"] membership_fields = ["member_join_policy", "member_leave_policy", "limit_on_members_number"] + resource_catalog, resource_groups = _resources_catalog() - if resource_catalog is False: - # on fail resource_groups contains the result object - result = resource_groups - messages.error(request, 'Unable to retrieve system resources: %s' % - result.reason) + resource_catalog_dict, resource_groups_dict = \ + _resources_catalog(as_dict=True) + extra_context = { 'resource_catalog': resource_catalog, 'resource_groups': resource_groups, + 'resource_catalog_dict': resource_catalog_dict, + 'resource_groups_dict': resource_groups_dict, 'show_form': True, 'details_fields': details_fields, - 'update_form': True, - 'membership_fields': membership_fields + 'membership_fields': membership_fields, + 'object': project } - response = None - with ExceptionHandler(request): - response = update_app_object(request, application_id, - extra_context=extra_context) - - if response is not None: - return response - - next = reverse('astakos.im.views.project_list') - next = restrict_next(next, domain=settings.COOKIE_DOMAIN) - return redirect(next) - - -@transaction.commit_on_success -def update_app_object(request, object_id, extra_context=None): - try: - summary = 'im/projects/projectapplication_form_summary.html' - return _update_object( - request, - object_id=object_id, - template_name='im/projects/projectapplication_form.html', - summary_template_name=summary, - extra_context=extra_context, - post_save_redirect=reverse('project_list'), - form_class=ProjectApplicationForm, - msg=_("The %(verbose_name)s has been received and is under " - "consideration.")) - except ProjectError as e: - messages.error(request, e) - - -@require_http_methods(["GET", "POST"]) -@cookie_fix -@valid_astakos_user_required -def project_app(request, application_id): - return common_detail(request, application_id, project_view=False) - - -@require_http_methods(["GET", "POST"]) -@cookie_fix -@valid_astakos_user_required -def project_detail(request, chain_id): - return common_detail(request, chain_id) + with transaction.commit_on_success(): + template_name = 'im/projects/projectapplication_form.html' + summary_template_name = \ + 'im/projects/projectapplication_form_summary.html' + success_msg = _("The project application has been received and " + "is under consideration.") + form_class = ProjectApplicationForm + if project: + template_name = 'im/projects/projectmodification_form.html' + summary_template_name = \ + 'im/projects/projectmodification_form_summary.html' + success_msg = _("The project modification has been received and " + "is under consideration.") + form_class = ProjectModificationForm + details_fields.remove('start_date') + + extra_context['edit'] = 0 + if request.method == 'POST': + form = form_class(request.POST, request.FILES, instance=project) + if form.is_valid(): + verify = request.GET.get('verify') + edit = request.GET.get('edit') + if verify == '1': + extra_context['show_form'] = False + extra_context['form_data'] = form.cleaned_data + template_name = summary_template_name + elif edit == '1': + extra_context['show_form'] = True + else: + new_object = form.save() + messages.success(request, success_msg, + fail_silently=True) + return redirect(restrict_reverse('project_list')) + else: + form = form_class(instance=project) -@transaction.commit_on_success -def addmembers(request, chain_id, addmembers_form): - if addmembers_form.is_valid(): - try: - chain_id = int(chain_id) - map(lambda u: enroll_member(chain_id, - u, - request_user=request.user), - addmembers_form.valid_users) - except ProjectError as e: - messages.error(request, e) - + extra_context['form'] = form + return render_to_response(template_name, extra_context, + context_instance=RequestContext(request)) -MEMBERSHIP_STATUS_FILTER = { - 0: lambda x: x.requested(), - 1: lambda x: x.any_accepted(), -} +@project_view(get=False, post=True) +def project_app_cancel(request, project_uuid, application_id): + with ExceptionHandler(request): + with transaction.commit_on_success(): + cancel_application(application_id, project_uuid, + request_user=request.user) + messages.success(request, + _(astakos_messages.APPLICATION_CANCELLED)) + return redirect(reverse('project_list')) -def common_detail(request, chain_or_app_id, project_view=True, - template_name='im/projects/project_detail.html', - members_status_filter=None): - project = None - approved_members_count = 0 - pending_members_count = 0 - remaining_memberships_count = None - if project_view: - chain_id = chain_or_app_id - if request.method == 'POST': - addmembers_form = AddProjectMembersForm( - request.POST, - chain_id=int(chain_id), - request_user=request.user) - with ExceptionHandler(request): - addmembers(request, chain_id, addmembers_form) - - if addmembers_form.is_valid(): - addmembers_form = AddProjectMembersForm() # clear form data - else: - addmembers_form = AddProjectMembersForm() # initialize form - project = get_object_or_404(Project, pk=chain_id) - application = project.application - if project: - members = project.projectmembership_set - approved_members_count = project.members_count() - pending_members_count = project.count_pending_memberships() - _limit = application.limit_on_members_number - if _limit is not None: - remaining_memberships_count = \ - max(0, _limit - approved_members_count) - flt = MEMBERSHIP_STATUS_FILTER.get(members_status_filter) - if flt is not None: - members = flt(members) - else: - members = members.associated() - members = members.select_related() - members_table = tables.ProjectMembersTable(project, - members, - user=request.user, - prefix="members_") - else: - members_table = None +@project_view(post=True) +def project_or_app_detail(request, project_uuid, app_id=None): + + project = get_object_or_404(Project, uuid=project_uuid) + application = None + if app_id: + application = get_object_or_404(ProjectApplication, id=app_id) + app_check_allowed(application, request.user) + if request.method == "POST": + raise PermissionDenied + + if project.state in [Project.O_PENDING] and not application and \ + project.last_application: + return redirect(reverse('project_app', + args=(project.uuid, + project.last_application.id,))) + + members = project.projectmembership_set + + # handle members form submission + if request.method == 'POST' and not application: + project_check_allowed(project, request.user) + addmembers_form = AddProjectMembersForm( + request.POST, + project_id=project.pk, + request_user=request.user) + with ExceptionHandler(request): + handle_valid_members_form(request, project.pk, addmembers_form) + if addmembers_form.is_valid(): + addmembers_form = AddProjectMembersForm() # clear form data else: - # is application - application_id = chain_or_app_id - application = get_object_or_404(ProjectApplication, pk=application_id) - members_table = None - addmembers_form = None + addmembers_form = AddProjectMembersForm() # initialize form + + approved_members_count = project.members_count() + pending_members_count = project.count_pending_memberships() + _limit = project.limit_on_members_number + remaining_memberships_count = (max(0, _limit - approved_members_count) + if _limit is not None else None) + members = members.associated() + members = members.select_related() + members_table = tables.ProjectMembersTable(project, + members, + user=request.user, + prefix="members_") + paginate = {"per_page": settings.PAGINATE_BY} + RequestConfig(request, paginate=paginate).configure(members_table) user = request.user - is_project_admin = user.is_project_admin(application_id=application.id) - is_owner = user.owns_application(application) - if not (is_owner or is_project_admin) and not project_view: + owns_base = False + if project and project.is_base and \ + project.realname == "system:%s" % request.user.uuid: + owns_base = True + is_project_admin = user.is_project_admin() + is_owner = user.owns_project(project) + is_applicant = False + last_pending_app = project.last_pending_application() + if last_pending_app: + is_applicant = last_pending_app and \ + last_pending_app.applicant.pk == user.pk + + if not (is_owner or is_project_admin) and \ + not user.non_owner_can_view(project): m = _(astakos_messages.NOT_ALLOWED) raise PermissionDenied(m) - if ( - not (is_owner or is_project_admin) and project_view and - not user.non_owner_can_view(project) - ): + if project and project.is_base and not (owns_base or is_project_admin): m = _(astakos_messages.NOT_ALLOWED) raise PermissionDenied(m) @@ -387,31 +301,62 @@ def common_detail(request, chain_or_app_id, project_view=True, mem_display = user.membership_display(project) if project else None can_join_req = can_join_request(project, user) if project else False can_leave_req = can_leave_request(project, user) if project else False + can_cancel_req = \ + can_cancel_join_request(project, user) if project else False + + is_modification = application.is_modification() if application else False + + queryset = Project.objects.select_related() + object_id = project.pk + resources_set = project.resource_set + template_name = "im/projects/project_detail.html" + if application: + queryset = ProjectApplication.objects.select_related() + object_id = application.pk + is_applicant = application.applicant.pk == user.pk + resources_set = application.resource_set + template_name = "im/projects/project_application_detail.html" + + display_usage = False + if (owns_base or is_owner or membership or is_project_admin) \ + and not app_id: + display_usage = True return object_detail( request, - queryset=ProjectApplication.objects.select_related(), - object_id=application.id, + queryset=queryset, + object_id=object_id, template_name=template_name, extra_context={ - 'project_view': project_view, - 'chain_id': chain_or_app_id, + 'project': project, 'application': application, + 'is_application': bool(application), + 'display_usage': display_usage, + 'is_modification': is_modification, 'addmembers_form': addmembers_form, 'approved_members_count': approved_members_count, 'pending_members_count': pending_members_count, 'members_table': members_table, 'owner_mode': is_owner, 'admin_mode': is_project_admin, + 'applicant_mode': is_applicant, 'mem_display': mem_display, 'membership_id': membership_id, 'can_join_request': can_join_req, 'can_leave_request': can_leave_req, - 'members_status_filter': members_status_filter, - 'remaining_memberships_count': remaining_memberships_count, + 'can_cancel_join_request': can_cancel_req, + 'resources_set': resources_set, + 'last_app': None if application else project.last_application, + 'remaining_memberships_count': remaining_memberships_count }) +MEMBERSHIP_STATUS_FILTER = { + 0: {'state': ProjectMembership.REQUESTED}, + 1: {'state__in': ProjectMembership.ACCEPTED_STATES} +} + + @require_http_methods(["GET", "POST"]) @cookie_fix @valid_astakos_user_required @@ -430,14 +375,9 @@ def project_search(request): if q is None: projects = Project.objects.none() else: - accepted = request.user.projectmembership_set.filter( - state__in=ProjectMembership.ACCEPTED_STATES).values_list( - 'project', flat=True) - - projects = Project.objects.search_by_name(q) - projects = projects.filter(Project.o_state_q(Project.O_ACTIVE)) - projects = projects.exclude(id__in=accepted).select_related( - 'application', 'application__owner', 'application__applicant') + query = ~Q(state=Project.DELETED) + projects = api._get_projects(query, mode="active", + request_user=request.user) table = get_user_projects_table(projects, user=request.user, prefix="my_projects_") @@ -458,295 +398,213 @@ def project_search(request): }) -@require_http_methods(["POST"]) -@cookie_fix -@valid_astakos_user_required -def project_join(request, chain_id): - next = request.GET.get('next') - if not next: - next = reverse('astakos.im.views.project_detail', - args=(chain_id,)) - +@project_view(get=False, post=True) +def project_join(request, project_uuid): + project = get_object_or_404(Project, uuid=project_uuid) with ExceptionHandler(request): - _project_join(request, chain_id) - - next = restrict_next(next, domain=settings.COOKIE_DOMAIN) - return redirect(next) - - -@transaction.commit_on_success -def _project_join(request, chain_id): - try: - chain_id = int(chain_id) - membership = join_project(chain_id, request.user) - if membership.state != membership.REQUESTED: - m = _(astakos_messages.USER_JOINED_PROJECT) - else: - m = _(astakos_messages.USER_JOIN_REQUEST_SUBMITTED) - messages.success(request, m) - except ProjectError as e: - messages.error(request, e) - - -@require_http_methods(["POST"]) -@cookie_fix -@valid_astakos_user_required -def project_leave(request, memb_id): - next = request.GET.get('next') - if not next: - next = reverse('astakos.im.views.project_list') - - with ExceptionHandler(request): - _project_leave(request, memb_id) - - next = restrict_next(next, domain=settings.COOKIE_DOMAIN) - return redirect(next) - - -@transaction.commit_on_success -def _project_leave(request, memb_id): - try: - memb_id = int(memb_id) - auto_accepted = leave_project(memb_id, request.user) - if auto_accepted: - m = _(astakos_messages.USER_LEFT_PROJECT) - else: - m = _(astakos_messages.USER_LEAVE_REQUEST_SUBMITTED) - messages.success(request, m) - except ProjectError as e: - messages.error(request, e) - + with transaction.commit_on_success(): + membership = join_project(project_uuid, request.user) + if membership.state != membership.REQUESTED: + m = _(astakos_messages.USER_JOINED_PROJECT) + else: + m = _(astakos_messages.USER_JOIN_REQUEST_SUBMITTED) + messages.success(request, m) + return redirect_to_next(request, 'project_detail', args=(project.uuid,)) -@require_http_methods(["POST"]) -@cookie_fix -@valid_astakos_user_required -def project_cancel_member(request, memb_id): - next = request.GET.get('next') - if not next: - next = reverse('astakos.im.views.project_list') +@project_view(get=False, post=True) +def project_leave(request, project_uuid): + project = get_object_or_404(Project, uuid=project_uuid) with ExceptionHandler(request): - _project_cancel_member(request, memb_id) - - next = restrict_next(next, domain=settings.COOKIE_DOMAIN) - return redirect(next) - - -@transaction.commit_on_success -def _project_cancel_member(request, memb_id): - try: - cancel_membership(memb_id, request.user) - m = _(astakos_messages.USER_REQUEST_CANCELLED) - messages.success(request, m) - except ProjectError as e: - messages.error(request, e) - + with transaction.commit_on_success(): + memb_id = request.user.get_membership(project).pk + auto_accepted = leave_project(memb_id, request.user) + if auto_accepted: + m = _(astakos_messages.USER_LEFT_PROJECT) + else: + m = _(astakos_messages.USER_LEAVE_REQUEST_SUBMITTED) + messages.success(request, m) + return redirect_to_next(request, 'project_detail', args=(project.uuid,)) -@require_http_methods(["POST"]) -@cookie_fix -@valid_astakos_user_required -def project_accept_member(request, memb_id): +@project_view(get=False, post=True) +def project_cancel_join(request, project_uuid): + project = get_object_or_404(Project, uuid=project_uuid) with ExceptionHandler(request): - _project_accept_member(request, memb_id) - - return redirect_back(request, 'project_list') + with transaction.commit_on_success(): + project = get_object_or_404(Project, uuid=project_uuid) + memb_id = request.user.get_membership(project).pk + cancel_membership(memb_id, request.user) + m = _(astakos_messages.USER_REQUEST_CANCELLED) + messages.success(request, m) + return redirect_to_next(request, 'project_detail', args=(project.uuid,)) -@transaction.commit_on_success -def _project_accept_member(request, memb_id): - try: - memb_id = int(memb_id) - m = accept_membership(memb_id, request.user) - except ProjectError as e: - messages.error(request, e) - - else: - email = escape(m.person.email) - msg = _(astakos_messages.USER_MEMBERSHIP_ACCEPTED) % email - messages.success(request, msg) - - -@require_http_methods(["POST"]) -@cookie_fix -@valid_astakos_user_required -def project_remove_member(request, memb_id): - +@project_view(get=False, post=True) +def project_app_approve(request, project_uuid, application_id): with ExceptionHandler(request): - _project_remove_member(request, memb_id) - - return redirect_back(request, 'project_list') - - -@transaction.commit_on_success -def _project_remove_member(request, memb_id): - try: - memb_id = int(memb_id) - m = remove_membership(memb_id, request.user) - except ProjectError as e: - messages.error(request, e) - else: - email = escape(m.person.email) - msg = _(astakos_messages.USER_MEMBERSHIP_REMOVED) % email - messages.success(request, msg) - + with transaction.commit_on_success(): + approve_application(application_id, project_uuid, + request_user=request.user) + messages.success(request, _(astakos_messages.APPLICATION_APPROVED)) + return redirect(reverse('project_detail', args=(project_uuid,))) -@require_http_methods(["POST"]) -@cookie_fix -@valid_astakos_user_required -def project_reject_member(request, memb_id): +@project_view(get=False, post=True) +def project_app_deny(request, project_uuid, application_id): with ExceptionHandler(request): - _project_reject_member(request, memb_id) - - return redirect_back(request, 'project_list') - + reason = request.POST.get("reason", "") + with transaction.commit_on_success(): + deny_application(application_id, project_uuid, + request_user=request.user, reason=reason) + messages.success(request, _(astakos_messages.APPLICATION_DENIED)) + return redirect(reverse("project_list")) -@transaction.commit_on_success -def _project_reject_member(request, memb_id): - try: - memb_id = int(memb_id) - m = reject_membership(memb_id, request.user) - except ProjectError as e: - messages.error(request, e) - else: - email = escape(m.person.email) - msg = _(astakos_messages.USER_MEMBERSHIP_REJECTED) % email - messages.success(request, msg) - - -@require_http_methods(["POST"]) -@signed_terms_required -@login_required -@cookie_fix -def project_app_approve(request, application_id): - - if not request.user.is_project_admin(): - m = _(astakos_messages.NOT_ALLOWED) - raise PermissionDenied(m) - - try: - ProjectApplication.objects.get(id=application_id) - except ProjectApplication.DoesNotExist: - raise Http404 +@project_view(get=False, post=True) +def project_app_dismiss(request, project_uuid, application_id): with ExceptionHandler(request): - _project_app_approve(request, application_id) - - chain_id = get_related_project_id(application_id) - if not chain_id: - return redirect_back(request, 'project_list') - return redirect(reverse('project_detail', args=(chain_id,))) - - -@transaction.commit_on_success -def _project_app_approve(request, application_id): - approve_application(application_id) - - -@require_http_methods(["POST"]) -@signed_terms_required -@login_required -@cookie_fix -def project_app_deny(request, application_id): - - reason = request.POST.get('reason', None) - if not reason: - reason = None - - if not request.user.is_project_admin(): - m = _(astakos_messages.NOT_ALLOWED) - raise PermissionDenied(m) + with transaction.commit_on_success(): + dismiss_application(application_id, project_uuid, + request_user=request.user) + messages.success(request, + _(astakos_messages.APPLICATION_DISMISSED)) + return redirect(reverse("project_list")) - try: - ProjectApplication.objects.get(id=application_id) - except ProjectApplication.DoesNotExist: - raise Http404 - with ExceptionHandler(request): - _project_app_deny(request, application_id, reason) - - return redirect(reverse('project_list')) +@project_view(post=True) +def project_members(request, project_uuid, members_status_filter=None, + template_name='im/projects/project_members.html'): + project = get_object_or_404(Project, uuid=project_uuid) + user = request.user + if not user.owns_project(project) and not user.is_project_admin(): + return redirect(reverse('index')) -@transaction.commit_on_success -def _project_app_deny(request, application_id, reason): - deny_application(application_id, reason=reason) + if request.method == 'POST': + addmembers_form = AddProjectMembersForm( + request.POST, + chain_id=int(chain_id), + request_user=request.user) + with ExceptionHandler(request): + handle_valid_members_form(request, chain_id, addmembers_form) + if addmembers_form.is_valid(): + addmembers_form = AddProjectMembersForm() # clear form data + else: + addmembers_form = AddProjectMembersForm() # initialize form + + query = api.make_membership_query({'project': project_uuid}) + members = api._get_memberships(query, request_user=user) + approved_members_count = project.members_count() + pending_members_count = project.count_pending_memberships() + _limit = project.limit_on_members_number + if _limit is not None: + remaining_memberships_count = \ + max(0, _limit - approved_members_count) + flt = MEMBERSHIP_STATUS_FILTER.get(members_status_filter) + if flt is not None: + members = members.filter(**flt) + else: + members = members.filter(state__in=ProjectMembership.ASSOCIATED_STATES) -@require_http_methods(["POST"]) -@signed_terms_required -@login_required -@cookie_fix -def project_app_dismiss(request, application_id): - try: - app = ProjectApplication.objects.get(id=application_id) - except ProjectApplication.DoesNotExist: - raise Http404 + members = members.select_related() + members_table = tables.ProjectMembersTable(project, + members, + user=request.user, + prefix="members_") + RequestConfig(request, paginate={"per_page": settings.PAGINATE_BY} + ).configure(members_table) - if not request.user.owns_application(app): + user = request.user + is_project_admin = user.is_project_admin() + is_owner = user.owns_application(project) + if ( + not (is_owner or is_project_admin) and + not user.non_owner_can_view(project) + ): m = _(astakos_messages.NOT_ALLOWED) raise PermissionDenied(m) - with ExceptionHandler(request): - _project_app_dismiss(request, application_id) - - chain_id = None - chain_id = get_related_project_id(application_id) - if chain_id: - next = reverse('project_detail', args=(chain_id,)) - else: - next = reverse('project_list') - return redirect(next) - - -def _project_app_dismiss(request, application_id): - # XXX: dismiss application also does authorization - dismiss_application(application_id, request_user=request.user) - - -@require_http_methods(["GET", "POST"]) -@valid_astakos_user_required -def project_members(request, chain_id, members_status_filter=None, - template_name='im/projects/project_members.html'): - project = get_object_or_404(Project, pk=chain_id) - - user = request.user - if not user.owns_project(project) and not user.is_project_admin(): - return redirect(reverse('index')) + membership = user.get_membership(project) if project else None + membership_id = membership.id if membership else None + mem_display = user.membership_display(project) if project else None + can_join_req = can_join_request(project, user) if project else False + can_leave_req = can_leave_request(project, user) if project else False - return common_detail(request, chain_id, - members_status_filter=members_status_filter, - template_name=template_name) + return object_detail( + request, + queryset=Project.objects.select_related(), + object_id=project.id, + template_name='im/projects/project_members.html', + extra_context={ + 'addmembers_form': addmembers_form, + 'approved_members_count': approved_members_count, + 'pending_members_count': pending_members_count, + 'members_table': members_table, + 'owner_mode': is_owner, + 'admin_mode': is_project_admin, + 'mem_display': mem_display, + 'membership_id': membership_id, + 'can_join_request': can_join_req, + 'can_leave_request': can_leave_req, + 'members_status_filter': members_status_filter, + 'project': project, + 'remaining_memberships_count': remaining_memberships_count, + }) -@require_http_methods(["POST"]) -@valid_astakos_user_required -def project_members_action(request, chain_id, action=None, redirect_to=''): +@project_view(get=False, post=True) +def project_members_action(request, project_uuid, action=None, redirect_to='', + memb_id=None): actions_map = { - 'remove': _project_remove_member, - 'accept': _project_accept_member, - 'reject': _project_reject_member + 'remove': { + 'method': 'remove_membership', + 'msg': _(astakos_messages.USER_MEMBERSHIP_REMOVED) + }, + 'accept': { + 'method': 'accept_membership', + 'msg': _(astakos_messages.USER_MEMBERSHIP_ACCEPTED) + }, + 'reject': { + 'method': 'reject_membership', + 'msg': _(astakos_messages.USER_MEMBERSHIP_REJECTED) + } } if not action in actions_map.keys(): raise PermissionDenied - member_ids = request.POST.getlist('members') - project = get_object_or_404(Project, pk=chain_id) + if memb_id: + member_ids = [memb_id] + else: + member_ids = request.POST.getlist('members') + + project = get_object_or_404(Project, uuid=project_uuid) user = request.user if not user.owns_project(project) and not user.is_project_admin(): + messages.error(request, astakos_messages.NOT_ALLOWED) return redirect(reverse('index')) - logger.info("Batch members action from %s (chain: %r, action: %s, " - "members: %r)", user.log_display, chain_id, action, member_ids) + logger.info("Member(s) action from %s (project: %r, action: %s, " + "members: %r)", user.log_display, project.uuid, action, + member_ids) - action_func = actions_map.get(action) + action = actions_map.get(action) + action_func = getattr(project_actions, action.get('method')) for member_id in member_ids: member_id = int(member_id) with ExceptionHandler(request): - action_func(request, member_id) + with transaction.commit_on_success(): + try: + m = action_func(member_id, request.user) + except ProjectError as e: + messages.error(request, e) + else: + email = escape(m.person.email) + msg = action.get('msg') % email + messages.success(request, msg) return redirect_back(request, 'project_list') diff --git a/snf-astakos-app/astakos/im/views/target/__init__.py b/snf-astakos-app/astakos/im/views/target/__init__.py index c749650076bcc16ab050aa16527b6d712b39bdd6..1eacf3a14fd7c80231b853e3f977069751495081 100644 --- a/snf-astakos-app/astakos/im/views/target/__init__.py +++ b/snf-astakos-app/astakos/im/views/target/__init__.py @@ -1,43 +1,26 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import json +import datetime from django.contrib import messages from django.http import HttpResponseRedirect from django.core.urlresolvers import reverse from django.core.validators import ValidationError -from django.db import transaction +from astakos.im import transaction from astakos.im.models import PendingThirdPartyUser, AstakosUser from astakos.im.util import get_query, login_url @@ -252,6 +235,7 @@ def handle_third_party_login(request, provider_module, identifier, third_party_key = get_pending_key(request) provider = user.get_auth_provider(provider_module, identifier) + if user.is_active: if not provider.get_login_policy: messages.error(request, provider.get_login_disabled_msg) @@ -264,6 +248,7 @@ def handle_third_party_login(request, provider_module, identifier, messages.success(request, provider.get_login_success_msg) add_pending_auth_provider(request, third_party_key, provider) response.set_cookie('astakos_last_login_method', provider_module) + provider.update_last_login_at() return response else: message = user.get_inactive_message(provider_module, identifier) diff --git a/snf-astakos-app/astakos/im/views/target/google.py b/snf-astakos-app/astakos/im/views/target/google.py index 455dfa9e458eb4cc74edc1701fbe6d0e55a57775..53eb0c61d77ea5b1d523a24978749ef5ca630921 100644 --- a/snf-astakos-app/astakos/im/views/target/google.py +++ b/snf-astakos-app/astakos/im/views/target/google.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import json import logging diff --git a/snf-astakos-app/astakos/im/views/target/linkedin.py b/snf-astakos-app/astakos/im/views/target/linkedin.py index 3d98ff070730f23f134299e5815ea98125d94d45..2950559614c123eb2918e8cc5bdac7c5e9dd1259 100644 --- a/snf-astakos-app/astakos/im/views/target/linkedin.py +++ b/snf-astakos-app/astakos/im/views/target/linkedin.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import json import logging diff --git a/snf-astakos-app/astakos/im/views/target/local.py b/snf-astakos-app/astakos/im/views/target/local.py index e015ac5036651323d7ff119fef28f0188e3e6cc6..406d70acb49b7e116838d56009e37656909b2ae2 100644 --- a/snf-astakos-app/astakos/im/views/target/local.py +++ b/snf-astakos-app/astakos/im/views/target/local.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.http import HttpResponseRedirect from django.shortcuts import render_to_response @@ -132,6 +114,8 @@ def login(request, on_failure='im/login.html'): provider = user.get_auth_provider('local') messages.success(request, provider.get_login_success_msg) response.set_cookie('astakos_last_login_method', 'local') + provider.update_last_login_at() + return response diff --git a/snf-astakos-app/astakos/im/views/target/redirect.py b/snf-astakos-app/astakos/im/views/target/redirect.py index 8754edb69631bee57570a2a927628fb9e3020185..14832f2e2b71abfff8416127a812290b0beb84ed 100644 --- a/snf-astakos-app/astakos/im/views/target/redirect.py +++ b/snf-astakos-app/astakos/im/views/target/redirect.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _ @@ -43,7 +25,7 @@ from django.views.decorators.http import require_http_methods from urlparse import urlunsplit, urlsplit, parse_qsl from astakos.im.util import restrict_next -from astakos.im.functions import login as auth_login, logout +from astakos.im.user_utils import login as auth_login, logout from astakos.im.views.decorators import cookie_fix import astakos.im.messages as astakos_messages @@ -58,8 +40,9 @@ logger = logging.getLogger(__name__) @cookie_fix def login(request): """ - If there is no ``next`` request parameter redirects to astakos index page - displaying an error message. + If there is no `next` request parameter returns 400 (BAD REQUEST). + Otherwise, if `next` request parameter is not among the allowed schemes, + returns 403 (Forbidden). If the request user is authenticated and has signed the approval terms, redirects to `next` request parameter. If not, redirects to approval terms in order to return back here after agreeing with the terms. diff --git a/snf-astakos-app/astakos/im/views/target/shibboleth.py b/snf-astakos-app/astakos/im/views/target/shibboleth.py index ed395e338ac382b7a9296ac24039381896ca754c..9c036f1fd5bd2b86ae79cb110b3a2e49012f04bc 100644 --- a/snf-astakos-app/astakos/im/views/target/shibboleth.py +++ b/snf-astakos-app/astakos/im/views/target/shibboleth.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.conf import settings as global_settings from django.utils.translation import ugettext as _ diff --git a/snf-astakos-app/astakos/im/views/target/twitter.py b/snf-astakos-app/astakos/im/views/target/twitter.py index b9a0183bccf43331326209ef439ac8d21bf03668..6af4d9627e259ceabf2c495e1f1758923b3817d1 100644 --- a/snf-astakos-app/astakos/im/views/target/twitter.py +++ b/snf-astakos-app/astakos/im/views/target/twitter.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import logging import oauth2 as oauth diff --git a/snf-astakos-app/astakos/im/views/util.py b/snf-astakos-app/astakos/im/views/util.py index b6deaf73a4ec0084c8d3494f328cd1e4cacaba65..31e4af41882d14eeecc34d99c229bfad336968b9 100644 --- a/snf-astakos-app/astakos/im/views/util.py +++ b/snf-astakos-app/astakos/im/views/util.py @@ -1,36 +1,22 @@ -# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import logging +import astakos.im.messages as astakos_messages +from astakos.im import settings from django.contrib import messages from django.contrib.auth.views import redirect_to_login from django.core.xheaders import populate_xheaders @@ -40,14 +26,16 @@ from django.template import RequestContext, loader as template_loader from django.utils.translation import ugettext as _ from django.views.generic.create_update import apply_extra_context, \ get_model_and_form_class, lookup_object +from astakos.im import transaction from synnefo.lib.ordereddict import OrderedDict from astakos.im import presentation from astakos.im.util import model_to_dict -from astakos.im.models import Resource -import astakos.im.messages as astakos_messages -import logging +from astakos.im import tables +from astakos.im.models import Resource, ProjectApplication, ProjectMembership +from astakos.im import functions +from astakos.im.util import get_context, restrict_next, restrict_reverse logger = logging.getLogger(__name__) @@ -102,6 +90,7 @@ def _create_object(request, model=None, template_name=None, extra_context['edit'] = 0 if request.method == 'POST': form = form_class(request.POST, request.FILES) + if form.is_valid(): verify = request.GET.get('verify') edit = request.GET.get('edit') @@ -167,8 +156,7 @@ def _update_object(request, model=None, object_id=None, slug=None, else: obj = form.save() if not msg: - msg = _( - "The %(verbose_name)s was created successfully.") + msg = _("The %(verbose_name)s was created successfully.") msg = msg % model._meta.__dict__ messages.success(request, msg, fail_silently=True) return redirect(post_save_redirect, obj) @@ -190,7 +178,20 @@ def _update_object(request, model=None, object_id=None, slug=None, return response -def _resources_catalog(): +def sorted_resources(resource_grant_or_quota_set): + meta = presentation.RESOURCES + order = meta.get('resources_order', []) + resources = list(resource_grant_or_quota_set) + + def order_key(item): + name = item.resource.name + if name in order: + return order.index(name) + return -1 + return sorted(resources, key=order_key) + + +def _resources_catalog(as_dict=False): """ `resource_catalog` contains a list of tuples. Each tuple contains the group key the resource is assigned to and resources list of dicts that contain @@ -264,4 +265,54 @@ def _resources_catalog(): resource_groups.pop(group) else: resource_catalog_new.append((group, resources)) + + if as_dict: + resource_catalog_new = OrderedDict(resource_catalog_new) + for name, resources in resource_catalog_new.iteritems(): + _rs = OrderedDict() + for resource in resources: + _rs[resource.get('name')] = resource + resource_catalog_new[name] = _rs + resource_groups = OrderedDict(resource_groups) + return resource_catalog_new, resource_groups + + +def get_user_projects_table(projects, user, prefix, request=None): + apps = ProjectApplication.objects.pending_per_project(projects) + memberships = user.projectmembership_set.one_per_project() + objs = ProjectMembership.objects + accepted_ms = objs.any_accepted_per_project(projects) + requested_ms = objs.requested_per_project(projects) + return tables.UserProjectsTable(projects, user=user, + prefix=prefix, + pending_apps=apps, + memberships=memberships, + accepted=accepted_ms, + requested=requested_ms, + request=request) + + +@transaction.commit_on_success +def handle_valid_members_form(request, project_id, addmembers_form): + if addmembers_form.is_valid(): + try: + users = addmembers_form.valid_users + for user in users: + functions.enroll_member_by_email(project_id, user.email, + request_user=request.user) + except functions.ProjectError as e: + messages.error(request, e) + + +def redirect_to_next(request, default_resolve, *args, **kwargs): + next = kwargs.pop('next', None) + if not next: + default = restrict_reverse(default_resolve, *args, + restrict_domain=settings.COOKIE_DOMAIN, + **kwargs) + next = request.GET.get('next', default) + + next = restrict_next(next, domain=settings.COOKIE_DOMAIN) + return redirect(next) + diff --git a/snf-astakos-app/astakos/im/weblogin_urls.py b/snf-astakos-app/astakos/im/weblogin_urls.py index 9b23783af6fad098dd94a66f1c64edcc79445e12..6bf987d57c7aaeae82d15480e1c832b81258e971 100644 --- a/snf-astakos-app/astakos/im/weblogin_urls.py +++ b/snf-astakos-app/astakos/im/weblogin_urls.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.conf.urls import url diff --git a/snf-astakos-app/astakos/im/widgets.py b/snf-astakos-app/astakos/im/widgets.py index 1e807f4982eed463a61aa8f44d514cfce63f0c5e..c7fdcf2ee7c7f4554f363cd345c66efffa5e4abd 100644 --- a/snf-astakos-app/astakos/im/widgets.py +++ b/snf-astakos-app/astakos/im/widgets.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import recaptcha.client.captcha as captcha diff --git a/snf-astakos-app/astakos/oa2/backends/base.py b/snf-astakos-app/astakos/oa2/backends/base.py index 725f24b3abedbae50050926c301e281b185b51d9..84e85d8adfcc109817538b12884a5106aeedfbef 100644 --- a/snf-astakos-app/astakos/oa2/backends/base.py +++ b/snf-astakos-app/astakos/oa2/backends/base.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import urlparse import uuid diff --git a/snf-astakos-app/astakos/oa2/backends/djangobackend.py b/snf-astakos-app/astakos/oa2/backends/djangobackend.py index 584fe1b88d7432e419a9cfcef1a1526b591bd0d8..54d2454ec1ad5328e694d8b0f89011e3ce061e33 100644 --- a/snf-astakos-app/astakos/oa2/backends/djangobackend.py +++ b/snf-astakos-app/astakos/oa2/backends/djangobackend.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import astakos.oa2.models as oa2_models diff --git a/snf-astakos-app/astakos/oa2/management/commands/oauth2-client-add.py b/snf-astakos-app/astakos/oa2/management/commands/oauth2-client-add.py index 6a6fe477c1fa70750ad731fbe52336fc59bba146..189b291f5ced6eaa6b2bb23f664f452f87fef899 100644 --- a/snf-astakos-app/astakos/oa2/management/commands/oauth2-client-add.py +++ b/snf-astakos-app/astakos/oa2/management/commands/oauth2-client-add.py @@ -1,42 +1,23 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option -from django.db import transaction -from django.core.management.base import CommandError +from astakos.im import transaction -from snf_django.management.commands import SynnefoCommand +from snf_django.management.commands import SynnefoCommand, CommandError from astakos.oa2.models import Client, RedirectUrl from astakos.oa2 import settings diff --git a/snf-astakos-app/astakos/oa2/management/commands/oauth2-client-list.py b/snf-astakos-app/astakos/oa2/management/commands/oauth2-client-list.py index b542ec543a45a79f5eef66e4118d066f37eefd1f..3e0af2fe24fb644e4e4c21fecd3debf2594f10fc 100644 --- a/snf-astakos-app/astakos/oa2/management/commands/oauth2-client-list.py +++ b/snf-astakos-app/astakos/oa2/management/commands/oauth2-client-list.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option diff --git a/snf-astakos-app/astakos/oa2/management/commands/oauth2-client-remove.py b/snf-astakos-app/astakos/oa2/management/commands/oauth2-client-remove.py index a4075c2782757075bf6fd6a0fa9b5dfd8c1e32f9..f8dad8ec3a289e84fee080a9ab124cc028ecf062 100644 --- a/snf-astakos-app/astakos/oa2/management/commands/oauth2-client-remove.py +++ b/snf-astakos-app/astakos/oa2/management/commands/oauth2-client-remove.py @@ -1,42 +1,25 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from snf_django.management.commands import SynnefoCommand, CommandError +from astakos.im import transaction -from django.core.management.base import BaseCommand, CommandError -from django.db import transaction from astakos.oa2.models import Client -class Command(BaseCommand): +class Command(SynnefoCommand): args = "<client ID or identifier>" help = "Remove an oauth2 client along with its registered redirect urls" diff --git a/snf-astakos-app/astakos/oa2/models.py b/snf-astakos-app/astakos/oa2/models.py index 44a8e1d631264bd1df9181e5b7ea4e8e4b550f65..de739b02a6890bea210d2c2cf4b640e3c072fd75 100644 --- a/snf-astakos-app/astakos/oa2/models.py +++ b/snf-astakos-app/astakos/oa2/models.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import datetime diff --git a/snf-astakos-app/astakos/oa2/services.py b/snf-astakos-app/astakos/oa2/services.py index bfbbf45c1c0a3e1820431cbec8a923ae0d837451..898c78b66e5f3f1ea06f148925c423a7249bab43 100644 --- a/snf-astakos-app/astakos/oa2/services.py +++ b/snf-astakos-app/astakos/oa2/services.py @@ -1,35 +1,17 @@ -# Copyright (C) 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from astakos.oa2 import settings diff --git a/snf-astakos-app/astakos/oa2/tests/djangobackend.py b/snf-astakos-app/astakos/oa2/tests/djangobackend.py index 0d29e139a591bdbd9b8d22105e5a683e2f48ba6f..9e353d16af0ec141419ad598092f9a1ef5936d06 100644 --- a/snf-astakos-app/astakos/oa2/tests/djangobackend.py +++ b/snf-astakos-app/astakos/oa2/tests/djangobackend.py @@ -1,36 +1,18 @@ # -*- coding: utf8 -*- -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import urllib import urlparse diff --git a/snf-astakos-app/astakos/oa2/tests/simple.py b/snf-astakos-app/astakos/oa2/tests/simple.py index 4905edb8ec0a85846f5e96785b8c5fbd2fcbbb8f..423329236797e9c68cb4174724647b83899e0d74 100644 --- a/snf-astakos-app/astakos/oa2/tests/simple.py +++ b/snf-astakos-app/astakos/oa2/tests/simple.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import unittest import base64 diff --git a/snf-astakos-app/astakos/oa2/urls.py b/snf-astakos-app/astakos/oa2/urls.py index 28b4c67775dd03410c0e11664e8039c580ca07d2..ca0d20879c9fe70dba9c236ed37ffbcd4c783f36 100644 --- a/snf-astakos-app/astakos/oa2/urls.py +++ b/snf-astakos-app/astakos/oa2/urls.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from astakos.oa2.backends import DjangoBackend diff --git a/snf-astakos-app/astakos/quotaholder_app/callpoint.py b/snf-astakos-app/astakos/quotaholder_app/callpoint.py index d8865953a0a43a5b695925e8981eaf58cf349fe4..3344e683eb1eeb9713d8a00f2e1cc9e86af506fa 100644 --- a/snf-astakos-app/astakos/quotaholder_app/callpoint.py +++ b/snf-astakos-app/astakos/quotaholder_app/callpoint.py @@ -1,44 +1,26 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from datetime import datetime from django.db.models import Q from astakos.quotaholder_app.exception import ( QuotaholderError, NoCommissionError, - CorruptedError, InvalidDataError, + CorruptedError, NoHoldingError, - DuplicateError) +) from astakos.quotaholder_app.commission import ( Import, Release, Operations, finalize, undo) @@ -75,6 +57,13 @@ def get_quota(holders=None, sources=None, resources=None, flt=None): return quotas +def delete_quota(keys): + for holder, source, resource in keys: + Holding.objects.filter(holder=holder, + source=source, + resource=resource).delete() + + def _get_holdings_for_update(holding_keys, resource=None, delete=False): flt = Q(resource=resource) if resource is not None else Q() holders = set(holder for (holder, source, resource) in holding_keys) @@ -132,30 +121,28 @@ def set_quota(quotas, resource=None): Holding.objects.bulk_create(new_holdings.values()) +def _merge_same_keys(provisions): + prov_dict = _partition_by(lambda t: t[0], provisions, lambda t: t[1]) + tuples = [] + for key, values in prov_dict.iteritems(): + tuples.append((key, sum(values))) + return tuples + + def issue_commission(clientkey, provisions, name="", force=False): operations = Operations() provisions_to_create = [] + provisions = _merge_same_keys(provisions) keys = [key for (key, value) in provisions] holdings = _get_holdings_for_update(keys) try: - checked = [] for key, quantity in provisions: - if not isinstance(quantity, (int, long)): - raise InvalidDataError("Malformed provision") - - if key in checked: - m = "Duplicate provision for %s" % str(key) - provision = _mkProvision(key, quantity) - raise DuplicateError(m, - provision=provision) - checked.append(key) - # Target try: th = holdings[key] except KeyError: - m = ("There is no such holding %s" % str(key)) + m = ("There is no such holding %s" % unicode(key)) provision = _mkProvision(key, quantity) raise NoHoldingError(m, provision=provision) @@ -169,7 +156,6 @@ def issue_commission(clientkey, provisions, name="", force=False): holdings[key] = th provisions_to_create.append((key, quantity)) - except QuotaholderError: operations.revert() raise @@ -177,12 +163,14 @@ def issue_commission(clientkey, provisions, name="", force=False): commission = Commission.objects.create(clientkey=clientkey, name=name, issue_datetime=datetime.now()) + ps = [] for (holder, source, resource), quantity in provisions_to_create: - Provision.objects.create(serial=commission, - holder=holder, - source=source, - resource=resource, - quantity=quantity) + ps.append(Provision(serial=commission, + holder=holder, + source=source, + resource=resource, + quantity=quantity)) + Provision.objects.bulk_create(ps) return commission.serial @@ -204,7 +192,7 @@ def _log_provision(commission, provision, holding, log_datetime, reason): 'reason': reason, } - ProvisionLog.objects.create(**kwargs) + return ProvisionLog(**kwargs) def _get_commissions_for_update(clientkey, serials): @@ -217,12 +205,14 @@ def _get_commissions_for_update(clientkey, serials): return commissions -def _partition_by(f, l): +def _partition_by(f, l, convert=None): + if convert is None: + convert = lambda x: x d = {} for x in l: group = f(x) group_l = d.get(group, []) - group_l.append(x) + group_l.append(convert(x)) d[group] = group_l return d @@ -263,12 +253,15 @@ def resolve_pending_commissions(clientkey, accept_set=None, reject_set=None, accepted.append(serial) if accept else rejected.append(serial) ps = provisions.get(serial, []) + provision_ids = [] + plog = [] for pv in ps: key = pv.holding_key() h = holdings.get(key) if h is None: - raise CorruptedError("Corrupted provision") + raise CorruptedError("Corrupted provision '%s'" % key) + provision_ids.append(pv.id) quantity = pv.quantity action = finalize if accept else undo if quantity >= 0: @@ -278,8 +271,10 @@ def resolve_pending_commissions(clientkey, accept_set=None, reject_set=None, prefix = 'ACCEPT:' if accept else 'REJECT:' comm_reason = prefix + reason[-121:] - _log_provision(commission, pv, h, log_datetime, comm_reason) - pv.delete() + plog.append( + _log_provision(commission, pv, h, log_datetime, comm_reason)) + Provision.objects.filter(id__in=provision_ids).delete() + ProvisionLog.objects.bulk_create(plog) commission.delete() return accepted, rejected, notFound, conflicting diff --git a/snf-astakos-app/astakos/quotaholder_app/commission.py b/snf-astakos-app/astakos/quotaholder_app/commission.py index adec36c3b27bbd1bbc1dc87130f9b2b1fc2011ca..5b856705485c5f4ff85e4a773c8a6b5666bbaff8 100644 --- a/snf-astakos-app/astakos/quotaholder_app/commission.py +++ b/snf-astakos-app/astakos/quotaholder_app/commission.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from astakos.quotaholder_app.exception import NoCapacityError, NoQuantityError diff --git a/snf-astakos-app/astakos/quotaholder_app/exception.py b/snf-astakos-app/astakos/quotaholder_app/exception.py index 7ac209e862573d34367a0b2d03b260da4612a2e0..fea572d582c3b2d1541abc85de5133af53fe2fe9 100644 --- a/snf-astakos-app/astakos/quotaholder_app/exception.py +++ b/snf-astakos-app/astakos/quotaholder_app/exception.py @@ -1,35 +1,17 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. class QuotaholderError(Exception): @@ -80,7 +62,3 @@ class NoQuantityError(OverLimitError): class NoHoldingError(CommissionException): pass - - -class DuplicateError(CommissionException): - pass diff --git a/snf-astakos-app/astakos/quotaholder_app/management/commands/commission-list.py b/snf-astakos-app/astakos/quotaholder_app/management/commands/commission-list.py index fdb6a2e8e9524647b6068bab2be18a841b0689f2..04cfcd10cee7768f019e0be3ea778596f1d3fb52 100644 --- a/snf-astakos-app/astakos/quotaholder_app/management/commands/commission-list.py +++ b/snf-astakos-app/astakos/quotaholder_app/management/commands/commission-list.py @@ -1,38 +1,19 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. -from django.core.management.base import CommandError -from snf_django.management.commands import ListCommand +from snf_django.management.commands import ListCommand, CommandError from optparse import make_option from astakos.quotaholder_app.models import Commission diff --git a/snf-astakos-app/astakos/quotaholder_app/management/commands/commission-show.py b/snf-astakos-app/astakos/quotaholder_app/management/commands/commission-show.py index d3ecf0aae47dd548ae19f3560603c9965c8ef8fe..4752fe81be6f3c025b8df775a35e5dc41489caae 100644 --- a/snf-astakos-app/astakos/quotaholder_app/management/commands/commission-show.py +++ b/snf-astakos-app/astakos/quotaholder_app/management/commands/commission-show.py @@ -1,38 +1,19 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. -from django.core.management.base import CommandError -from snf_django.management.commands import SynnefoCommand +from snf_django.management.commands import SynnefoCommand, CommandError from snf_django.management import utils from astakos.quotaholder_app.models import Commission, Provision diff --git a/snf-astakos-app/astakos/quotaholder_app/migrations/0012_project_holdings.py b/snf-astakos-app/astakos/quotaholder_app/migrations/0012_project_holdings.py new file mode 100644 index 0000000000000000000000000000000000000000..24f7c54cd9ce651b62130db6bef3f1a32c78b5fd --- /dev/null +++ b/snf-astakos-app/astakos/quotaholder_app/migrations/0012_project_holdings.py @@ -0,0 +1,466 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +def _partition_by(f, l): + d = {} + for x in l: + group = f(x) + group_l = d.get(group, []) + group_l.append(x) + d[group] = group_l + return d + +NORMAL = 1 +SUSPENDED = 10 +TERMINATED = 100 +INITIALIZED_STATES = [NORMAL, SUSPENDED, TERMINATED] +ACTUALLY_ACCEPTED = [1, 5] + + +class Migration(DataMigration): + + depends_on = ( + ("im", "0080_initialized_memberships"), + ) + + def project_holder(self, project): + return "project:" + project.uuid + + def user_holder(self, user): + return "user:" + user.uuid + + def find_resource(self, holdings, resource): + if holdings is None: + return None + hs = [h for h in holdings if h.resource == resource] + assert len(hs) < 2 + if not hs: + return None + h = hs[0] + return (h.usage_min, h.usage_max) + + def forwards(self, orm): + AstakosUser = orm["im.astakosuser"] + users = AstakosUser.objects.filter(moderated=True, is_rejected=False) + base_projects = {} + for user in users: + base_projects[user.base_project_id] = user + + holdings = orm.Holding.objects.all() + holdings = _partition_by(lambda h: h.holder, holdings) + + Project = orm["im.project"] + projects = Project.objects.filter(state__in=INITIALIZED_STATES) + + ProjectResourceQuota = orm["im.projectresourcequota"] + objs = ProjectResourceQuota.objects.select_related("resource") + grants_l = objs.all() + grants_d = _partition_by(lambda g: g.project_id, grants_l) + + # Memberships + ProjectMembership = orm["im.projectmembership"] + objs = ProjectMembership.objects.select_related("person", "project") + memberships_l = objs.filter(initialized=True) + memberships = _partition_by(lambda m: m.project_id, memberships_l) + + new_holdings = [] + for project in projects: + base_user = base_projects.get(project.id) + base_holdings = (holdings[base_user.uuid] + if base_user is not None else None) + + grants = grants_d[project.id] + for grant in grants: + resource = grant.resource.name + values = self.find_resource(base_holdings, resource) + (u_min, u_max) = values if values else (0, 0) + plimit = (grant.project_capacity + if project.state == NORMAL else 0) + h = orm.Holding( + holder=self.project_holder(project), + source=None, + resource=resource, + limit=plimit, + usage_min=u_min, + usage_max=u_max) + new_holdings.append(h) + + for membership in memberships.get(project.id, []): + user = membership.person + mlimit = (grant.member_capacity + if project.state == NORMAL and + membership.state in ACTUALLY_ACCEPTED else 0) + (u_min, u_max) = ((u_min, u_max) + if user == base_user else (0, 0)) + h = orm.Holding( + holder=self.user_holder(user), + source=self.project_holder(project), + resource=resource, + limit=mlimit, + usage_min=u_min, + usage_max=u_max) + new_holdings.append(h) + + orm.Holding.objects.bulk_create(new_holdings) + orm.Holding.objects.filter(source="system").delete() + + def backwards(self, orm): + "Write your backwards methods here." + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'im.additionalmail': { + 'Meta': {'object_name': 'AdditionalMail'}, + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.approvalterms': { + 'Meta': {'object_name': 'ApprovalTerms'}, + 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.astakosuser': { + 'Meta': {'object_name': 'AstakosUser', '_ormbases': ['auth.User']}, + 'accepted_email': ('django.db.models.fields.EmailField', [], {'default': 'None', 'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'accepted_policy': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'activation_sent': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'base_project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'base_user'", 'to': "orm['im.Project']"}), + 'date_signed_terms': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deactivated_reason': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}), + 'disturbed_quota': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'email_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_credits': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_signed_terms': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'invitations': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'is_rejected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'level': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'moderated_at': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'moderated_data': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.AstakosUserQuota']", 'symmetrical': 'False'}), + 'rejected_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {}), + 'user_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'primary_key': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'verification_code': ('django.db.models.fields.CharField', [], {'max_length': '255', 'unique': 'True', 'null': 'True'}), + 'verified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.astakosuserauthprovider': { + 'Meta': {'ordering': "('module', 'created')", 'unique_together': "(('identifier', 'module', 'user'),)", 'object_name': 'AstakosUserAuthProvider'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'auth_backend': ('django.db.models.fields.CharField', [], {'default': "'astakos'", 'max_length': '255'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'info_data': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'module': ('django.db.models.fields.CharField', [], {'default': "'local'", 'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'auth_providers'", 'to': "orm['im.AstakosUser']"}) + }, + 'im.astakosuserquota': { + 'Meta': {'unique_together': "(('resource', 'user'),)", 'object_name': 'AstakosUserQuota'}, + 'capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}) + }, + 'im.authproviderpolicyprofile': { + 'Meta': {'ordering': "['priority']", 'object_name': 'AuthProviderPolicyProfile'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['auth.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_exclusive': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'policy_add': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_automoderate': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_create': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_limit': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}), + 'policy_login': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_remove': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_required': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'policy_switch': ('django.db.models.fields.NullBooleanField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'priority': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authpolicy_profiles'", 'symmetrical': 'False', 'to': "orm['im.AstakosUser']"}) + }, + 'im.chain': { + 'Meta': {'object_name': 'Chain'}, + 'chain': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.component': { + 'Meta': {'object_name': 'Component'}, + 'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'base_url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}) + }, + 'im.emailchange': { + 'Meta': {'object_name': 'EmailChange'}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'requested_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'emailchanges'", 'unique': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.endpoint': { + 'Meta': {'object_name': 'Endpoint'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'service': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'endpoints'", 'to': "orm['im.Service']"}) + }, + 'im.endpointdata': { + 'Meta': {'unique_together': "(('endpoint', 'key'),)", 'object_name': 'EndpointData'}, + 'endpoint': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'data'", 'to': "orm['im.Endpoint']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '1024'}) + }, + 'im.invitation': { + 'Meta': {'object_name': 'Invitation'}, + 'code': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'consumed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inviter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invitations_sent'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'is_consumed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'realname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.pendingthirdpartyuser': { + 'Meta': {'unique_together': "(('provider', 'third_party_identifier'),)", 'object_name': 'PendingThirdPartyUser'}, + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'info': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'third_party_identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'token': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'im.project': { + 'Meta': {'object_name': 'Project'}, + 'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'end_date': ('django.db.models.fields.DateTimeField', [], {}), + 'homepage': ('django.db.models.fields.URLField', [], {'max_length': '255'}), + 'id': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_column': "'id'"}), + 'is_base': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_application': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'last_of_project'", 'null': 'True', 'to': "orm['im.ProjectApplication']"}), + 'limit_on_members_number': ('django.db.models.fields.BigIntegerField', [], {}), + 'member_join_policy': ('django.db.models.fields.IntegerField', [], {}), + 'member_leave_policy': ('django.db.models.fields.IntegerField', [], {}), + 'members': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.AstakosUser']", 'through': "orm['im.ProjectMembership']", 'symmetrical': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True', 'null': 'True', 'db_index': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projs_owned'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'private': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'realname': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'resource_grants': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.ProjectResourceQuota']", 'blank': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'im.projectapplication': { + 'Meta': {'unique_together': "(('chain', 'id'),)", 'object_name': 'ProjectApplication'}, + 'applicant': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_applied'", 'to': "orm['im.AstakosUser']"}), + 'chain': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'chained_apps'", 'db_column': "'chain'", 'to': "orm['im.Project']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'end_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'homepage': ('django.db.models.fields.URLField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'issue_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'limit_on_members_number': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'member_join_policy': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'member_leave_policy': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'null': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects_owned'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'private': ('django.db.models.fields.NullBooleanField', [], {'default': 'False', 'null': 'True', 'blank': 'True'}), + 'resource_grants': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.ProjectResourceGrant']", 'blank': 'True'}), + 'response': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'response_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'responded_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'response_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'start_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}), + 'waive_actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'waived_apps'", 'null': 'True', 'to': "orm['im.AstakosUser']"}), + 'waive_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'waive_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) + }, + 'im.projectlock': { + 'Meta': {'object_name': 'ProjectLock'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'im.projectlog': { + 'Meta': {'object_name': 'ProjectLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.Project']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectmembership': { + 'Meta': {'unique_together': "(('person', 'project'),)", 'object_name': 'ProjectMembership'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'initialized': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Project']"}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}) + }, + 'im.projectmembershiplog': { + 'Meta': {'object_name': 'ProjectMembershipLog'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']", 'null': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'from_state': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'membership': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['im.ProjectMembership']"}), + 'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'to_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'im.projectresourcegrant': { + 'Meta': {'unique_together': "(('resource', 'project_application'),)", 'object_name': 'ProjectResourceGrant'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'member_capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'project_application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.ProjectApplication']"}), + 'project_capacity': ('django.db.models.fields.BigIntegerField', [], {}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}) + }, + 'im.projectresourcequota': { + 'Meta': {'unique_together': "(('resource', 'project'),)", 'object_name': 'ProjectResourceQuota'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'member_capacity': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Project']"}), + 'project_capacity': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}) + }, + 'im.resource': { + 'Meta': {'object_name': 'Resource'}, + 'api_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'desc': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'project_default': ('django.db.models.fields.BigIntegerField', [], {}), + 'service_origin': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'service_type': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'ui_visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'unit': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'uplimit': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) + }, + 'im.service': { + 'Meta': {'object_name': 'Service'}, + 'component': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Component']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'im.sessioncatalog': { + 'Meta': {'object_name': 'SessionCatalog'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'to': "orm['im.AstakosUser']"}) + }, + 'im.usersetting': { + 'Meta': {'unique_together': "(('user', 'setting'),)", 'object_name': 'UserSetting'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'setting': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"}), + 'value': ('django.db.models.fields.IntegerField', [], {}) + }, + 'quotaholder_app.commission': { + 'Meta': {'object_name': 'Commission'}, + 'clientkey': ('django.db.models.fields.CharField', [], {'max_length': '4096'}), + 'issue_datetime': ('django.db.models.fields.DateTimeField', [], {}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '4096'}), + 'serial': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'quotaholder_app.holding': { + 'Meta': {'unique_together': "(('holder', 'source', 'resource'),)", 'object_name': 'Holding'}, + 'holder': ('django.db.models.fields.CharField', [], {'max_length': '4096', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'limit': ('django.db.models.fields.BigIntegerField', [], {}), + 'resource': ('django.db.models.fields.CharField', [], {'max_length': '4096'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '4096', 'null': 'True'}), + 'usage_max': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'usage_min': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) + }, + 'quotaholder_app.provision': { + 'Meta': {'object_name': 'Provision'}, + 'holder': ('django.db.models.fields.CharField', [], {'max_length': '4096', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'quantity': ('django.db.models.fields.BigIntegerField', [], {}), + 'resource': ('django.db.models.fields.CharField', [], {'max_length': '4096'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'provisions'", 'to': "orm['quotaholder_app.Commission']"}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '4096', 'null': 'True'}) + }, + 'quotaholder_app.provisionlog': { + 'Meta': {'object_name': 'ProvisionLog'}, + 'delta_quantity': ('django.db.models.fields.BigIntegerField', [], {}), + 'holder': ('django.db.models.fields.CharField', [], {'max_length': '4096'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'issue_time': ('django.db.models.fields.CharField', [], {'max_length': '4096'}), + 'limit': ('django.db.models.fields.BigIntegerField', [], {}), + 'log_time': ('django.db.models.fields.CharField', [], {'max_length': '4096'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '4096'}), + 'reason': ('django.db.models.fields.CharField', [], {'max_length': '4096'}), + 'resource': ('django.db.models.fields.CharField', [], {'max_length': '4096'}), + 'serial': ('django.db.models.fields.BigIntegerField', [], {}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '4096', 'null': 'True'}), + 'usage_max': ('django.db.models.fields.BigIntegerField', [], {}), + 'usage_min': ('django.db.models.fields.BigIntegerField', [], {}) + } + } + + complete_apps = ['im', 'quotaholder_app'] + symmetrical = True diff --git a/snf-astakos-app/astakos/quotaholder_app/models.py b/snf-astakos-app/astakos/quotaholder_app/models.py index 0f28e54d40609fb33505897c54c5e01158833b14..e50193d49f6eda4bf55b118f2c1a9ab24c78005c 100644 --- a/snf-astakos-app/astakos/quotaholder_app/models.py +++ b/snf-astakos-app/astakos/quotaholder_app/models.py @@ -1,35 +1,17 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.db.models import (Model, BigIntegerField, CharField, DateTimeField, ForeignKey, AutoField) diff --git a/snf-astakos-app/astakos/quotaholder_app/tests.py b/snf-astakos-app/astakos/quotaholder_app/tests.py index 946734bd7667f98d204d5649787fb5a313e9575d..4098130bcf1ab30e908ba90457de59663cbe6d32 100644 --- a/snf-astakos-app/astakos/quotaholder_app/tests.py +++ b/snf-astakos-app/astakos/quotaholder_app/tests.py @@ -1,35 +1,17 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.test import TestCase @@ -37,12 +19,11 @@ from snf_django.utils.testing import assertGreater, assertIn, assertRaises from astakos.quotaholder_app import models import astakos.quotaholder_app.callpoint as qh from astakos.quotaholder_app.exception import ( - InvalidDataError, NoCommissionError, NoQuantityError, NoCapacityError, NoHoldingError, - DuplicateError) +) class QuotaholderTest(TestCase): @@ -102,6 +83,12 @@ class QuotaholderTest(TestCase): with assertRaises(NoCommissionError): qh.get_commission(self.client, s1+1) + r = qh.get_quota() + self.assertEqual(r, + {(holder, source, resource1): (limit1, 0, limit1/2), + (holder, source, resource2): (limit2, 0, limit2), + }) + # commission exceptions with assertRaises(NoCapacityError) as cm: @@ -140,21 +127,6 @@ class QuotaholderTest(TestCase): self.assertEqual(provision['resource'], resource1) self.assertEqual(provision['quantity'], 1) - with assertRaises(DuplicateError) as cm: - self.issue_commission([((holder, source, resource1), 1), - ((holder, source, resource1), 2)]) - - e = cm.exception - provision = e.data['provision'] - self.assertEqual(provision['holder'], holder) - self.assertEqual(provision['source'], source) - self.assertEqual(provision['resource'], resource1) - self.assertEqual(provision['quantity'], 2) - - with assertRaises(InvalidDataError): - self.issue_commission([((holder, source, resource1), 1), - ((holder, source, resource1), "nan")]) - r = qh.get_quota(holders=[holder]) quotas = {(holder, source, resource1): (limit1, 0, limit1/2), (holder, source, resource2): (limit2, 0, limit2), @@ -166,6 +138,7 @@ class QuotaholderTest(TestCase): r = qh.get_pending_commissions(clientkey=self.client) self.assertEqual(len(r), 1) serial = r[0] + self.assertEqual(serial, s1) r = qh.resolve_pending_commission(self.client, serial) self.assertEqual(r, True) r = qh.get_pending_commissions(clientkey=self.client) @@ -179,6 +152,13 @@ class QuotaholderTest(TestCase): } self.assertEqual(r, quotas) + logs = models.ProvisionLog.objects.filter(serial=serial) + self.assertEqual(len(logs), 2) + log1 = filter(lambda p: p.resource == resource1 + and p.delta_quantity == limit1/2 + and p.usage_min == limit1/2, logs) + self.assertEqual(len(log1), 1) + # resolve commissions serial = self.issue_commission([((holder, source, resource1), 1), diff --git a/snf-astakos-app/astakos/scripts/snf-component-register b/snf-astakos-app/astakos/scripts/snf-component-register index 1a168d6d443a0a7168287e7b78a7e809e744eab7..e35d9dce38796128d0229ca768d60f8694778430 100755 --- a/snf-astakos-app/astakos/scripts/snf-component-register +++ b/snf-astakos-app/astakos/scripts/snf-component-register @@ -1,37 +1,20 @@ -# Copyright 2014 GRNET S.A. All rights reserved. -# -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +#!/bin/bash # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# Copyright (C) 2010-2014 GRNET S.A. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. +# 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 General Public License for more details. # -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. -#!/bin/bash declare -A types types[astakos]=identity @@ -142,7 +125,7 @@ fi if [ $changed -eq 1 ]; then echo 'Done with registering services and their resources.' echo 'Now run ' - echo " snf-manage resource-modify --default-quota-interactive" + echo " snf-manage resource-modify <resource_name> --system-default <limit>" echo 'to specify the default base quota for each resource provided by' \ 'the services.' fi diff --git a/snf-astakos-app/astakos/scripts/snf_service_export.py b/snf-astakos-app/astakos/scripts/snf_service_export.py index 28b04e9a171a78c398d855a89e0da8ea93a2a15b..67ac603f379b238168f2038b8c0e3e29cd01fee0 100644 --- a/snf-astakos-app/astakos/scripts/snf_service_export.py +++ b/snf-astakos-app/astakos/scripts/snf_service_export.py @@ -231,6 +231,18 @@ cyclades_services = { ], 'resources': {}, }, + + 'cyclades_volume': { + 'type': 'volume', + 'component': 'cyclades', + 'prefix': 'volume', + 'public': True, + 'endpoints': [ + {'versionId': 'v2.0', + 'publicURL': None}, + ], + 'resources': {}, + }, } pithos_services = { diff --git a/snf-astakos-app/astakos/synnefo_settings.py b/snf-astakos-app/astakos/synnefo_settings.py index ce02b5ffe3011dc76fb4954ac17f74d86c7f86b4..27f16ac5ad6d6f4ffbea001933b1cd226a657368 100644 --- a/snf-astakos-app/astakos/synnefo_settings.py +++ b/snf-astakos-app/astakos/synnefo_settings.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # """ diff --git a/snf-astakos-app/astakos/test/stress.py b/snf-astakos-app/astakos/test/stress.py index 89f8879d0803d5d4807f8bf555fa1aedf9bc3b85..761a3599f433fc7e9497f849b987619cfd277687 100755 --- a/snf-astakos-app/astakos/test/stress.py +++ b/snf-astakos-app/astakos/test/stress.py @@ -1,38 +1,20 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import os from optparse import OptionParser @@ -46,7 +28,7 @@ path = os.path.dirname(os.path.realpath(__file__)) os.environ['SYNNEFO_SETTINGS_DIR'] = path + '/settings' os.environ['DJANGO_SETTINGS_MODULE'] = 'synnefo.settings' -from django.db import transaction +from astakos.im import transaction from astakos.im.models import AstakosUser from astakos.im.functions import ProjectError from astakos.im import auth diff --git a/snf-astakos-app/astakos/test/views.py b/snf-astakos-app/astakos/test/views.py index 549394556525129877d1be0c92f0047e09e4f886..f1236e7a5d58d219bafdb3b3f6b447c5d769afeb 100644 --- a/snf-astakos-app/astakos/test/views.py +++ b/snf-astakos-app/astakos/test/views.py @@ -1,39 +1,21 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from datetime import datetime, timedelta -from django.db import transaction +from astakos.im import transaction from astakos.im.models import AstakosUser, Project from astakos.im.functions import (join_project, leave_project, submit_application, approve_application, @@ -64,7 +46,8 @@ def submit(name, user_id, project_id=None): if not ok: raise ProjectForbidden('Limit %s reached', limit) - resource_policies = {'cyclades.network.private': {'member_capacity': 5}} + resource_policies = {'cyclades.network.private': {'member_capacity': 5, + 'project_capacity': 10}} data = {'owner': owner, 'name': name, 'project_id': project_id, diff --git a/snf-astakos-app/astakos/urls.py b/snf-astakos-app/astakos/urls.py index 12e05db104ae2c27fc8e600ac2f27ccea82757df..c20dff90627157b09ab4a3f968ea479616fce58c 100644 --- a/snf-astakos-app/astakos/urls.py +++ b/snf-astakos-app/astakos/urls.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.conf.urls import include, patterns diff --git a/snf-astakos-app/conf/20-snf-astakos-oa2-app-settings.py b/snf-astakos-app/conf/20-snf-astakos-app-oa2.conf similarity index 100% rename from snf-astakos-app/conf/20-snf-astakos-oa2-app-settings.py rename to snf-astakos-app/conf/20-snf-astakos-app-oa2.conf diff --git a/snf-astakos-app/conf/20-snf-astakos-app-settings.conf b/snf-astakos-app/conf/20-snf-astakos-app-settings.conf index 10b054f9d28b5c5611e2724ea0bc94b2c5cbde74..934a254e4f719c3340eb9354381d199983ca4598 100644 --- a/snf-astakos-app/conf/20-snf-astakos-app-settings.conf +++ b/snf-astakos-app/conf/20-snf-astakos-app-settings.conf @@ -124,9 +124,6 @@ #ASTAKOS_LINKEDIN_TOKEN = '' #ASTAKOS_LINKEDIN_SECRET = '' -# Whether or not to display projects in astakos menu -# ASTAKOS_PROJECTS_VISIBLE = False - # A way to extend the components presentation metadata # ASTAKOS_COMPONENTS_META = {} @@ -145,3 +142,6 @@ ## Timeout in seconds for caching visible resources in GET /quotas # ASTAKOS_RESOURCE_CACHE_TIMEOUT = 60 + +## Astakos groups that have access to users admin api endpoints +# ASTAKOS_ADMIN_STATS_PERMITTED_GROUPS = ["admin-stats"] diff --git a/snf-astakos-app/setup.py b/snf-astakos-app/setup.py index 90fed20bce0bbdb4debb2796f6b40b6b4bb10987..33e16538c472aecbd041fe94ee3a90da8b311970 100644 --- a/snf-astakos-app/setup.py +++ b/snf-astakos-app/setup.py @@ -1,37 +1,19 @@ #!/usr/bin/env python -# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import distribute_setup distribute_setup.use_setuptools() @@ -60,7 +42,7 @@ CLASSIFIERS = [ 'Operating System :: OS Independent', 'Programming Language :: Python', 'Topic :: Utilities', - 'License :: OSI Approved :: BSD License', + 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', ] # Package requirements @@ -68,12 +50,12 @@ INSTALL_REQUIRES = [ 'Django>=1.4, <1.5', 'South>=0.7.3', 'httplib2>=0.6.0', + 'python-dateutil>=1.4.1', 'snf-common', 'django-tables2', 'recaptcha-client>=1.0.5', 'django-ratelimit==0.1', 'requests', - 'inflect', 'snf-django-lib', 'snf-branding', 'snf-webproject', @@ -182,7 +164,7 @@ def find_package_data( setup( name='snf-astakos-app', version=VERSION, - license='BSD', + license='GNU GPLv3', url='http://www.synnefo.org/', description=SHORT_DESCRIPTION, classifiers=CLASSIFIERS, diff --git a/snf-branding/MANIFEST.in b/snf-branding/MANIFEST.in index e330573ca511c167b7b9e3d87ec679d3d90f3922..1d10d61f638611c568d173836bfab9c964a16f9c 100644 --- a/snf-branding/MANIFEST.in +++ b/snf-branding/MANIFEST.in @@ -1,2 +1,2 @@ -include distribute_setup.py +include distribute_setup.py README.md recursive-include synnefo_branding/static * diff --git a/snf-branding/README.md b/snf-branding/README.md new file mode 100644 index 0000000000000000000000000000000000000000..c8fbfb7832fede647f2b0ca651d21ae127b0c7ee --- /dev/null +++ b/snf-branding/README.md @@ -0,0 +1,27 @@ +snf-branding +============ + +Overview +-------- + +This is Synnefo's snf-branding component. Please see the [official Synnefo +site](http://www.synnefo.org) for more information. + + +Copyright and license +===================== + +Copyright (C) 2010-2014 GRNET S.A. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. diff --git a/snf-branding/conf/15-snf-branding-settings.conf b/snf-branding/conf/15-snf-branding-settings.conf index a482c4d22690adaaf32083695a64cb4d88a35320..e7536b8e5d187437b154570c938208659307f9a5 100644 --- a/snf-branding/conf/15-snf-branding-settings.conf +++ b/snf-branding/conf/15-snf-branding-settings.conf @@ -45,3 +45,19 @@ # Footer message appears above Copyright message at the Compute templates # and the Dashboard UI. Accepts html tags #BRANDING_FOOTER_EXTRA_MESSAGE = '' + + +# A list of webfont css urls. By default fonts are loaded from google fonts +# CDN. The css files served by the urls in the list should contain the +# appropriate css statements for `Open Sans` and `Ubuntu` font families +# to be loaded in order to be used from the Synnefo applications styles. +# Example statements can be found in the source of google css files content. +# We DO NOT recommend to use custom styling in those files as they get loaded +# before the Synnefo theme and most probably your rules will be overriden by +# the default application styles. Use `//` prefix as defined +# in default urls if you serve Synnefo application over both http and https +# protocols (NOT RECOMMENDED). +#BRANDING_FONTS_CSS_URLS = [ +# '//fonts.googleapis.com/css?family=Open+Sans&subset=latin,greek-ext,greek', +# '//fonts.googleapis.com/css?family=Ubuntu&subset=latin,greek' +#] diff --git a/snf-branding/setup.py b/snf-branding/setup.py index ca1e8319a89291d7cef0296cccce98b2a489454f..f9977468e455a6845bcc60f7eaf21ccb002c61c5 100644 --- a/snf-branding/setup.py +++ b/snf-branding/setup.py @@ -1,35 +1,17 @@ -# Copyright 2011-2014 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # import distribute_setup @@ -152,7 +134,7 @@ def find_package_data( setup( name='snf-branding', version=VERSION, - license='BSD', + license='GNU GPLv3', url='http://www.synnefo.org/', description=SHORT_DESCRIPTION, classifiers=CLASSIFIERS, diff --git a/snf-branding/synnefo_branding/context_processors.py b/snf-branding/synnefo_branding/context_processors.py index cf691a225ee657649adf65fa30cead78978ff03c..6649f5fa4cdca55065a1d20e87e9a77c3044e902 100644 --- a/snf-branding/synnefo_branding/context_processors.py +++ b/snf-branding/synnefo_branding/context_processors.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from synnefo_branding import utils diff --git a/snf-branding/synnefo_branding/settings.py b/snf-branding/synnefo_branding/settings.py index 583e875a29d139e29301842a08dd703754f84c5b..ddc1b698f2399c511e067b5c9843559f26993b90 100644 --- a/snf-branding/synnefo_branding/settings.py +++ b/snf-branding/synnefo_branding/settings.py @@ -53,3 +53,9 @@ SYNNEFO_VERSION = get_component_version('common') # Footer message appears above Copyright message at the Compute templates # and the Dashboard UI. Accepts html tags FOOTER_EXTRA_MESSAGE = getattr(settings, 'BRANDING_FOOTER_EXTRA_MESSAGE', '') + +# The location of the css files that contain the font loading css code +FONTS_CSS_URLS = getattr(settings, 'BRANDING_FONTS_CSS_URLS', [ + '//fonts.googleapis.com/css?family=Open+Sans&subset=latin,greek-ext,greek', + '//fonts.googleapis.com/css?family=Ubuntu&subset=latin,greek' +]) diff --git a/snf-branding/synnefo_branding/static/branding/images/console_logo.png b/snf-branding/synnefo_branding/static/branding/images/console_logo.png index 3adcae90de4f8caaef48d91aca69a54982414deb..2cebeef5c593b706d00937b3ee1bf578c450f520 100644 Binary files a/snf-branding/synnefo_branding/static/branding/images/console_logo.png and b/snf-branding/synnefo_branding/static/branding/images/console_logo.png differ diff --git a/snf-branding/synnefo_branding/static/branding/images/console_logo@2x.png b/snf-branding/synnefo_branding/static/branding/images/console_logo@2x.png index 2087092672b7e34517cc1a1dc019a5be55e20e0c..63d1319116903a05f10b31f48390f808d4a668e7 100644 Binary files a/snf-branding/synnefo_branding/static/branding/images/console_logo@2x.png and b/snf-branding/synnefo_branding/static/branding/images/console_logo@2x.png differ diff --git a/snf-branding/synnefo_branding/synnefo_settings.py b/snf-branding/synnefo_branding/synnefo_settings.py index ff75d9ce9ec2366375113fe423bfd92bc79a10b3..660ab592ceee5722232da2dd53716765d4d81579 100644 --- a/snf-branding/synnefo_branding/synnefo_settings.py +++ b/snf-branding/synnefo_branding/synnefo_settings.py @@ -1,35 +1,17 @@ -# Copyright 2011-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # diff --git a/snf-branding/synnefo_branding/utils.py b/snf-branding/synnefo_branding/utils.py index 0418fd0e8dc227da29db79294f5e23145a50f52c..7789192ed0fdfa1a91156ee713256093ab900b59 100644 --- a/snf-branding/synnefo_branding/utils.py +++ b/snf-branding/synnefo_branding/utils.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.conf import settings as django_settings from synnefo_branding import settings diff --git a/snf-common/MANIFEST.in b/snf-common/MANIFEST.in index 6106c5d89680afa76e234fbcf1feb50c52b9a5f6..ce7f614828c2d727bb93e8692c231749d45fdd5a 100644 --- a/snf-common/MANIFEST.in +++ b/snf-common/MANIFEST.in @@ -1 +1 @@ -include distribute_setup.py +include distribute_setup.py README.md diff --git a/snf-common/README.md b/snf-common/README.md new file mode 100644 index 0000000000000000000000000000000000000000..e1b240f0a655df090f6e6e6b681c79617b5e2901 --- /dev/null +++ b/snf-common/README.md @@ -0,0 +1,27 @@ +snf-common +========== + +Overview +-------- + +This is Synnefo's snf-common component. Please see the [official Synnefo +site](http://www.synnefo.org) for more information. + + +Copyright and license +===================== + +Copyright (C) 2010-2014 GRNET S.A. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. diff --git a/snf-common/conf/00-snf-common-admins.conf b/snf-common/conf/00-snf-common-admins.conf index 561a2a4f089421bb9cd5242eb9b5b58167c06aa4..6cf482ca768900e64903f0db6e370b353bb99a9a 100644 --- a/snf-common/conf/00-snf-common-admins.conf +++ b/snf-common/conf/00-snf-common-admins.conf @@ -9,17 +9,33 @@ # ('Your Name', 'your_email@domain.com'), #) # -# List of people to receive user feedback notifications. +# List of people to be used by `ACCOUNT_NOTIFICATIONS_RECIPIENTS`, +# `FEEDBACK_NOTIFICATIONS_RECIPIENTS` and `PROJECT_NOTIFICATIONS_RECIPIENTS`. +# NOTE: This setting has been kept for backward compatibility and it will be +# ignored if the above lists have been set explicitly. #HELPDESK = ( # ('Your Name', 'your_email@domain.com'), #) # -# A list of people to receive email notifications on some application events -# (e.g. account creation/activation). +# List of people to be used by `ACCOUNT_NOTIFICATIONS_RECIPIENTS` and +# `PROJECT_NOTIFICATIONS_RECIPIENTS`. +# NOTE: This setting has been kept for backward compatibility and it will be +# ignored if the above lists have been set explicitly. #MANAGERS = ( # ('Your Name', 'your_email@domain.com'), #) # +## Email notifications recipients +# +# A list of people to receive 'Account moderation/activation' notifications +#ACCOUNT_NOTIFICATIONS_RECIPIENTS = HELPDESK + MANAGERS + ADMINS +# +# A list of people to receive 'Feedback' notifications +#FEEDBACK_NOTIFICATIONS_RECIPIENTS = HELPDESK +# +# A list of people to receive 'Project creation/modification' notifications +#PROJECT_NOTIFICATIONS_RECIPIENTS = HELPDESK + MANAGERS +# ## Email configuration #EMAIL_HOST = "127.0.0.1" #EMAIL_HOST_USER = "" @@ -36,3 +52,12 @@ # ## Email address the emails sent by the service will come from #SERVER_EMAIL = "Synnefo cloud <cloud@example.com>" +# +# +## Synnefo logging directory +############################# +# +# Directory where log files are saved +# Currently only snf-manage uses this to save +# the output of the commands being executed. +#LOG_DIR = "/var/log/synnefo/" diff --git a/snf-common/setup.py b/snf-common/setup.py index 6b5985d0ad14e88be13fa2552bebed598e7ae094..a16c445e2b36ba05243300736c32fc2961906efa 100644 --- a/snf-common/setup.py +++ b/snf-common/setup.py @@ -1,35 +1,17 @@ -# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # import distribute_setup @@ -68,7 +50,7 @@ TESTS_REQUIRES = [ setup( name='snf-common', version=VERSION, - license='BSD', + license='GNU GPLv3', url='http://www.synnefo.org/', description=SHORT_DESCRIPTION, classifiers=CLASSIFIERS, diff --git a/snf-common/synnefo/lib/__init__.py b/snf-common/synnefo/lib/__init__.py index 11a09f8bcad30e2a3f98fa98a5eb4db9d9d4da23..93f0172f910e2e13967eff64db2d21b4d9d9dfce 100644 --- a/snf-common/synnefo/lib/__init__.py +++ b/snf-common/synnefo/lib/__init__.py @@ -1,35 +1,17 @@ -# Copyright (C) 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A. OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from urlparse import urlparse diff --git a/snf-common/synnefo/lib/amqp/__init__.py b/snf-common/synnefo/lib/amqp/__init__.py index 609bb88416016fe523db7554d358746b885ac446..6b34406cfd393c2f3b5df3a2833fae83e057e178 100644 --- a/snf-common/synnefo/lib/amqp/__init__.py +++ b/snf-common/synnefo/lib/amqp/__init__.py @@ -1,35 +1,17 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. """ Module implementing connection and communication with an AMQP broker. diff --git a/snf-common/synnefo/lib/amqp/amqp_haigha.py b/snf-common/synnefo/lib/amqp/amqp_haigha.py index c530a40fd03cb25f889841417c793bf998516489..fc1b1002abeb8a6de82bf38bf0fbf4056b1e8927 100644 --- a/snf-common/synnefo/lib/amqp/amqp_haigha.py +++ b/snf-common/synnefo/lib/amqp/amqp_haigha.py @@ -1,35 +1,17 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from haigha.connections import RabbitConnection from haigha.message import Message @@ -142,7 +124,7 @@ class AMQPHaighaClient(): auto_delete=False, durable=True) def queue_declare(self, queue, exclusive=False, mirrored=True, - mirrored_nodes='all'): + mirrored_nodes='all', ttl=None): """Declare a queue @type queue: string @@ -157,6 +139,8 @@ class AMQPHaighaClient(): the specified nodes, and the master will be the first node in the list. Node names must be provided and not host IP. example: [node1@rabbit,node2@rabbit] + @type ttl: int + @param tll: Queue TTL in seconds """ @@ -172,6 +156,9 @@ class AMQPHaighaClient(): else: arguments = {} + if ttl is not None: + arguments['x-expires'] = ttl * 1000 + self.channel.queue.declare(queue, durable=True, exclusive=exclusive, auto_delete=False, arguments=arguments) @@ -222,7 +209,7 @@ class AMQPHaighaClient(): (exchange, routing_key, body) = self.unacked[mid] self.basic_publish(exchange, routing_key, body) - def basic_consume(self, queue, callback): + def basic_consume(self, queue, callback, no_ack=False, exclusive=False): """Consume from a queue. @type queue: string or list of strings @@ -233,7 +220,8 @@ class AMQPHaighaClient(): """ self.consumers[queue] = callback - self.channel.basic.consume(queue, consumer=callback, no_ack=False) + self.channel.basic.consume(queue, consumer=callback, no_ack=no_ack, + exclusive=exclusive) @reconnect_decorator def basic_wait(self): @@ -249,8 +237,8 @@ class AMQPHaighaClient(): gevent.sleep(0) @reconnect_decorator - def basic_get(self, queue): - self.channel.basic.get(queue, no_ack=False) + def basic_get(self, queue, no_ack=False): + self.channel.basic.get(queue, no_ack=no_ack) @reconnect_decorator def basic_ack(self, message): diff --git a/snf-common/synnefo/lib/amqp/amqp_puka.py b/snf-common/synnefo/lib/amqp/amqp_puka.py index 23c09794f5a7a986e600e5706bdd8e2754134949..c96d8a1e1b8ae538ff6fe257030ce86b13408aba 100644 --- a/snf-common/synnefo/lib/amqp/amqp_puka.py +++ b/snf-common/synnefo/lib/amqp/amqp_puka.py @@ -1,35 +1,17 @@ -# Copyright 2012-2014 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. """ Module implementing connection and communication with an AMQP broker. @@ -198,7 +180,7 @@ class AMQPPukaClient(object): @reconnect_decorator def queue_declare(self, queue, exclusive=False, mirrored=True, mirrored_nodes='all', - dead_letter_exchange=None): + dead_letter_exchange=None, ttl=None): """Declare a queue @type queue: string @@ -213,6 +195,8 @@ class AMQPPukaClient(object): the specified nodes, and the master will be the first node in the list. Node names must be provided and not host IP. example: [node1@rabbit,node2@rabbit] + @type ttl: int + @param ttl: Queue TTL in seconds """ self.log.info('Declaring queue: %s', queue) @@ -228,6 +212,9 @@ class AMQPPukaClient(object): else: arguments = {} + if ttl is not None: + arguments['x-expires'] = ttl * 1000 + if dead_letter_exchange: arguments['x-dead-letter-exchange'] = dead_letter_exchange @@ -328,7 +315,7 @@ class AMQPPukaClient(object): self.unsend.pop(body) @reconnect_decorator - def basic_consume(self, queue, callback, prefetch_count=0): + def basic_consume(self, queue, callback, no_ack=False, prefetch_count=0): """Consume from a queue. @type queue: string or list of strings @@ -352,7 +339,8 @@ class AMQPPukaClient(object): consume_promise = \ self.client.basic_consume(queue=queue, prefetch_count=prefetch_count, - callback=handle_delivery) + callback=handle_delivery, + no_ack=no_ack) self.consume_promises.append(consume_promise) return consume_promise @@ -372,14 +360,14 @@ class AMQPPukaClient(object): return self.client.wait(self.consume_promises, timeout) @reconnect_decorator - def basic_get(self, queue): + def basic_get(self, queue, no_ack=False): """Get a single message from a queue. This is a non-blocking method for getting messages from a queue. It will return None if the queue is empty. """ - get_promise = self.client.basic_get(queue=queue) + get_promise = self.client.basic_get(queue=queue, no_ack=no_ack) result = self.client.wait(get_promise) if 'empty' in result: # The queue is empty diff --git a/snf-common/synnefo/lib/db/pooled_psycopg2/__init__.py b/snf-common/synnefo/lib/db/pooled_psycopg2/__init__.py index 4e4749903361849d96a3f6a2ea4c9a51d0deba7a..47bbc0be4a83e126318015bceda523caf25bda88 100644 --- a/snf-common/synnefo/lib/db/pooled_psycopg2/__init__.py +++ b/snf-common/synnefo/lib/db/pooled_psycopg2/__init__.py @@ -1,35 +1,17 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # import psycopg2 @@ -99,20 +81,27 @@ class Psycopg2ConnectionPool(ObjectPool): log.info("CREATED: got connection %s from psycopg2", conn) return PooledConnection(self, conn) - def _pool_verify_execute(pooledconn): + def _pool_verify(self, conn): try: - cursor = pooledconn.cursor() - cursor.execute("SELECT 1") + # Make sure that the connection is alive before using the fd + res = conn.poll() + if res == psycopg2.extensions.POLL_ERROR: + raise psycopg2.Error + + # There shouldn't be any data available to read. If there is, + # remove the connection from the pool + if select((conn.fileno(),), (), (), 0)[0]: + raise psycopg2.Error + return True except psycopg2.Error: - # The connection has died. - pooledconn.close() - return False - return True - - def _pool_verify(self, conn): - if select((conn.fileno(),), (), (), 0)[0]: + # Since we're not going to be putting the psycopg2 connection + # back into the pool, close it uncoditionally. + log.info("VERIFY: Detected dead connection") + try: + conn.close() + except: + pass return False - return True def _pool_cleanup(self, pooledconn): log.debug("CLEANING, conn = %d", id(pooledconn)) diff --git a/snf-common/synnefo/lib/dict.py b/snf-common/synnefo/lib/dict.py new file mode 100644 index 0000000000000000000000000000000000000000..0cfb452174952de46a9790218ba538094568a948 --- /dev/null +++ b/snf-common/synnefo/lib/dict.py @@ -0,0 +1,74 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from collections import OrderedDict + + +class SnfOrderedDict(OrderedDict): + + """Class that combines a data structure and a list into an OrderedDict. + + Given a data structure (class/dict) and list, create a list of tuples. + Then, use the Python's 2.7 OrderedDict constructor, which expects a list of + key-value pairs, in order to create the OrderedDict. + """ + + def __init__(self, data=None, lst=None, strict=True): + """Combine a data structure and a list into a list of tuples. + + This __init__ function will default to the __init__ function of + OrderedDict, if only one argument is passed. + + By default, the items in the list must correspond to the keys in the + provided data structure. To relax this requirement, set the `strict` + argument to False. + """ + if lst is None: + return super(SnfOrderedDict, self).__init__(data) + + self.__strict = strict + + # Use the appropriate constructor, depending on the data structure + # type, to create the required list of tuples. + if isinstance(data, dict): + tpl = self.fromdict_constructor(data, lst) + else: + tpl = self.fromclass_constructor(data, lst) + + # Call the __init__ function of the OrderedDict class with the tuple + # as argument + return super(SnfOrderedDict, self).__init__(tpl) + + def fromdict_constructor(self, dct, lst): + """Create a list of tuples from a dict and a list.""" + new_lst = [] + for item in lst: + try: + new_lst.append((item, dct[item])) + except KeyError: + if self.__strict: + raise + return new_lst + + def fromclass_constructor(self, cls, lst): + """Create a list of tuples from any class and a list.""" + new_lst = [] + for item in lst: + try: + new_lst.append((item, getattr(cls, item))) + except AttributeError: + if self.__strict: + raise + return new_lst diff --git a/snf-common/synnefo/lib/ordereddict.py b/snf-common/synnefo/lib/ordereddict.py index 5b0303f5a3be2bed7cb3e07ecbc10ea5d02fa996..7065485136b1c2e00901e055cd2855027749ca1a 100644 --- a/snf-common/synnefo/lib/ordereddict.py +++ b/snf-common/synnefo/lib/ordereddict.py @@ -1,127 +1,129 @@ -# Copyright (c) 2009 Raymond Hettinger -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation files -# (the "Software"), to deal in the Software without restriction, -# including without limitation the rights to use, copy, modify, merge, -# publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. - -from UserDict import DictMixin - -class OrderedDict(dict, DictMixin): - - def __init__(self, *args, **kwds): - if len(args) > 1: - raise TypeError('expected at most 1 arguments, got %d' % len(args)) - try: - self.__end - except AttributeError: - self.clear() - self.update(*args, **kwds) - - def clear(self): - self.__end = end = [] - end += [None, end, end] # sentinel node for doubly linked list - self.__map = {} # key --> [key, prev, next] - dict.clear(self) - - def __setitem__(self, key, value): - if key not in self: - end = self.__end - curr = end[1] - curr[2] = end[1] = self.__map[key] = [key, curr, end] - dict.__setitem__(self, key, value) - - def __delitem__(self, key): - dict.__delitem__(self, key) - key, prev, next = self.__map.pop(key) - prev[2] = next - next[1] = prev - - def __iter__(self): - end = self.__end - curr = end[2] - while curr is not end: - yield curr[0] - curr = curr[2] - - def __reversed__(self): - end = self.__end - curr = end[1] - while curr is not end: - yield curr[0] - curr = curr[1] - - def popitem(self, last=True): - if not self: - raise KeyError('dictionary is empty') - if last: - key = reversed(self).next() - else: - key = iter(self).next() - value = self.pop(key) - return key, value - - def __reduce__(self): - items = [[k, self[k]] for k in self] - tmp = self.__map, self.__end - del self.__map, self.__end - inst_dict = vars(self).copy() - self.__map, self.__end = tmp - if inst_dict: - return (self.__class__, (items,), inst_dict) - return self.__class__, (items,) - - def keys(self): - return list(self) - - setdefault = DictMixin.setdefault - update = DictMixin.update - pop = DictMixin.pop - values = DictMixin.values - items = DictMixin.items - iterkeys = DictMixin.iterkeys - itervalues = DictMixin.itervalues - iteritems = DictMixin.iteritems - - def __repr__(self): - if not self: - return '%s()' % (self.__class__.__name__,) - return '%s(%r)' % (self.__class__.__name__, self.items()) - - def copy(self): - return self.__class__(self) - - @classmethod - def fromkeys(cls, iterable, value=None): - d = cls() - for key in iterable: - d[key] = value - return d - - def __eq__(self, other): - if isinstance(other, OrderedDict): - if len(self) != len(other): - return False - for p, q in zip(self.items(), other.items()): - if p != q: - return False - return True - return dict.__eq__(self, other) - - def __ne__(self, other): - return not self == other +# Copyright (c) 2009 Raymond Hettinger +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + +from UserDict import DictMixin + +# FIXME: Deprecated since Python 2.7. This class must be removed in the next +# Synnefo version. +class OrderedDict(dict, DictMixin): + + def __init__(self, *args, **kwds): + if len(args) > 1: + raise TypeError('expected at most 1 arguments, got %d' % len(args)) + try: + self.__end + except AttributeError: + self.clear() + self.update(*args, **kwds) + + def clear(self): + self.__end = end = [] + end += [None, end, end] # sentinel node for doubly linked list + self.__map = {} # key --> [key, prev, next] + dict.clear(self) + + def __setitem__(self, key, value): + if key not in self: + end = self.__end + curr = end[1] + curr[2] = end[1] = self.__map[key] = [key, curr, end] + dict.__setitem__(self, key, value) + + def __delitem__(self, key): + dict.__delitem__(self, key) + key, prev, next = self.__map.pop(key) + prev[2] = next + next[1] = prev + + def __iter__(self): + end = self.__end + curr = end[2] + while curr is not end: + yield curr[0] + curr = curr[2] + + def __reversed__(self): + end = self.__end + curr = end[1] + while curr is not end: + yield curr[0] + curr = curr[1] + + def popitem(self, last=True): + if not self: + raise KeyError('dictionary is empty') + if last: + key = reversed(self).next() + else: + key = iter(self).next() + value = self.pop(key) + return key, value + + def __reduce__(self): + items = [[k, self[k]] for k in self] + tmp = self.__map, self.__end + del self.__map, self.__end + inst_dict = vars(self).copy() + self.__map, self.__end = tmp + if inst_dict: + return (self.__class__, (items,), inst_dict) + return self.__class__, (items,) + + def keys(self): + return list(self) + + setdefault = DictMixin.setdefault + update = DictMixin.update + pop = DictMixin.pop + values = DictMixin.values + items = DictMixin.items + iterkeys = DictMixin.iterkeys + itervalues = DictMixin.itervalues + iteritems = DictMixin.iteritems + + def __repr__(self): + if not self: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, self.items()) + + def copy(self): + return self.__class__(self) + + @classmethod + def fromkeys(cls, iterable, value=None): + d = cls() + for key in iterable: + d[key] = value + return d + + def __eq__(self, other): + if isinstance(other, OrderedDict): + if len(self) != len(other): + return False + for p, q in zip(self.items(), other.items()): + if p != q: + return False + return True + return dict.__eq__(self, other) + + def __ne__(self, other): + return not self == other diff --git a/snf-common/synnefo/lib/services.py b/snf-common/synnefo/lib/services.py index 9729f824d0fe24ab38c41645d95bc584dd2d0dbf..ffbd4ebb532f5fb63579b1a86cd4f3805908b09f 100644 --- a/snf-common/synnefo/lib/services.py +++ b/snf-common/synnefo/lib/services.py @@ -1,54 +1,35 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from copy import deepcopy from synnefo.lib import join_urls -from synnefo.util.keypath import get_path, set_path from urlparse import urlparse def fill_endpoints(services, base_url): for name, service in services.iteritems(): - prefix = get_path(service, 'prefix') - endpoints = get_path(service, 'endpoints') + prefix = service['prefix'] + endpoints = service['endpoints'] for endpoint in endpoints: - version = get_path(endpoint, 'versionId') - publicURL = get_path(endpoint, 'publicURL') + version = endpoint['versionId'] + publicURL = endpoint['publicURL'] if publicURL is not None: continue publicURL = join_urls(base_url, prefix, version).rstrip('/') - set_path(endpoint, 'publicURL', publicURL) + endpoint['publicURL'] = publicURL def filter_public(services): diff --git a/snf-common/synnefo/lib/singleton/__init__.py b/snf-common/synnefo/lib/singleton/__init__.py deleted file mode 100644 index a5ae9edbc7492fa1d2667d426aac6e17bc775f61..0000000000000000000000000000000000000000 --- a/snf-common/synnefo/lib/singleton/__init__.py +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. -# -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: -# -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. -# -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. - - -class ArgBasedSingletonMeta(type): - """Implement the Singleton pattern with a twist. - - Implement the Singleton pattern with a twist: - The uniqueness on the object is based on the class name, - plus the argument list (args and kwargs). - - Unique objects are store in the '_singles' class attribute. - A distinct _singles object is used per subclass. - - """ - def __call__(cls, *args, **kwargs): - kwlist = str([(k, kwargs[k]) for k in sorted(kwargs.keys())]) - distinct = str((cls, args, kwlist)) - - # Allocate a new _singles attribute per subclass - if not hasattr(cls, "_singles_cls") or cls != cls._singles_cls: - cls._singles = {} - cls._singles_cls = cls - - if distinct not in cls._singles: - obj = super(ArgBasedSingletonMeta, cls).__call__(*args, **kwargs) - cls._singles[distinct] = obj - - ret = cls._singles[distinct] - - return ret - - -class ArgBasedSingleton(object): - __metaclass__ = ArgBasedSingletonMeta diff --git a/snf-common/synnefo/lib/singleton/tests.py b/snf-common/synnefo/lib/singleton/tests.py deleted file mode 100755 index 9ebaf77b23b59f0a61d68239cda4b47d7ab6f00b..0000000000000000000000000000000000000000 --- a/snf-common/synnefo/lib/singleton/tests.py +++ /dev/null @@ -1,214 +0,0 @@ -#!/usr/bin/env python -# -# -*- coding: utf-8 -*- -# -# Copyright 2011 GRNET S.A. All rights reserved. -# -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: -# -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. -# -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. -# -# - -"""Unit Tests for the Singleton classes in synnefo.lib.singleton - -Provides unit tests for the code implementing Singleton -classes in the synnefo.lib.singleton module. - -""" - -import unittest - -from synnefo.lib.singleton import ArgBasedSingleton, ArgBasedSingletonMeta - - -class SubClassOne(ArgBasedSingleton): - name = None - - def __init__(self, name): - self.name = name - - -class SubClassTwo(ArgBasedSingleton): - name = None - - def __init__(self, name): - self.name = name - - -class SubClassThree(SubClassTwo): - name2 = None - - def __init__(self, name): - self.name2 = name - - -class SubClassKwArgs(ArgBasedSingleton): - name = None - - def __init__(self, onearg, **kwargs): - self.name = onearg - for x in kwargs: - setattr(self, x, kwargs[x]) - - -class SubClassNoReinit(ArgBasedSingleton): - initialized = None - - def __init__(self, *args, **kwargs): - if self.initialized: - raise Exception("__init__ called twice!") - self.initialized = True - - -class ArgBasedSingletonTestCase(unittest.TestCase): - def test_same_object(self): - o1 = ArgBasedSingleton() - o2 = ArgBasedSingleton() - self.assertTrue(o1 is o2) - - -class MyMeta(ArgBasedSingletonMeta): - def __call__(cls, *args, **kw): - return super(MyMeta, cls).__call__(*args, **kw) - - -class BaseClass(object): - __metaclass__ = MyMeta - - def ret5(self): - return 5 - - -class SubClassMultiple(BaseClass, ArgBasedSingleton): - name = None - - def __init__(self, name): - name = name - - -class SubClassTestCase(unittest.TestCase): - def test_same_object(self): - o1 = SubClassOne('one') - o2 = SubClassOne('two') - o1a = SubClassOne('one') - - self.assertEqual(o1.name, 'one') - self.assertEqual(o2.name, 'two') - self.assertEqual(o1a.name, 'one') - self.assertFalse(o1 is o2) - self.assertTrue(o1 is o1a) - - def test_different_classes(self): - o1 = SubClassOne('one') - o2 = SubClassTwo('one') - - self.assertEqual(o1.name, 'one') - self.assertEqual(o2.name, 'one') - self.assertFalse(o1 is o2) - - -class SubClassKwArgsTestCase(unittest.TestCase): - def test_init_signature(self): - self.assertRaises(TypeError, SubClassKwArgs, 'one', 'two') - - def test_distinct_kwargs(self): - o1 = SubClassKwArgs('one', a=1) - o2 = SubClassKwArgs('two') - o1a = SubClassKwArgs('one', a=2) - o1b = SubClassKwArgs('one', a=1) - o1c = SubClassKwArgs('one', a=1, b=2) - o1d = SubClassKwArgs('one', b=2, a=1) - - self.assertEqual(o1.a, 1) - self.assertEqual(o1a.a, 2) - self.assertEqual(o1b.a, 1) - self.assertRaises(AttributeError, getattr, o2, 'a') - self.assertFalse(o1 is o2) - self.assertFalse(o1 is o1a) - self.assertTrue(o1 is o1b) - self.assertTrue(o1c is o1d) - - -class SubClassDistinctDicts(unittest.TestCase): - def test_distinct_storage_per_subclass(self): - o1 = SubClassOne('one') - o2 = SubClassTwo('one') - o1a = SubClassOne('two') - o2a = SubClassTwo('two') - - self.assertEqual(o1.name, 'one') - self.assertEqual(o2.name, 'one') - self.assertEqual(o1a.name, 'two') - self.assertEqual(o2a.name, 'two') - self.assertTrue(o1._singles is o1a._singles) - self.assertTrue(o2._singles is o2a._singles) - self.assertFalse(o1._singles is o2._singles) - self.assertFalse(o1a._singles is o2a._singles) - - -class SubClassThreeTestCase(unittest.TestCase): - def test_singleton_inheritance(self): - o1 = SubClassThree('one') - o2 = SubClassThree('two') - o1a = SubClassThree('one') - - self.assertEquals(o1.name2, 'one') - self.assertEquals(o2.name2, 'two') - self.assertEquals(o1a.name2, 'one') - - self.assertTrue(o1 is o1a) - self.assertFalse(o1 is o2) - - -class SubClassMultipleTestCase(unittest.TestCase): - def test_multiple_inheritance(self): - o1 = SubClassMultiple('one') - o2 = SubClassMultiple('two') - o1a = SubClassMultiple('one') - - self.assertEquals(o1.ret5(), 5) - self.assertEquals(o2.ret5(), 5) - self.assertEquals(o1a.ret5(), 5) - - self.assertTrue(o1 is o1a) - self.assertFalse(o1 is o2) - - -class SubClassNoReinitTestCase(unittest.TestCase): - def test_no_reinit(self): - o1 = SubClassNoReinit('one') - o2 = SubClassNoReinit('one') - - self.assertTrue(o1 is o2) - - -if __name__ == '__main__': - unittest.main() diff --git a/snf-common/synnefo/lib/trace.py b/snf-common/synnefo/lib/trace.py index 769f0f42e8b2239238de6085e5d2809a611c46d9..717de912dce2bfa6b23b14e3ecc5fd20479a07ac 100644 --- a/snf-common/synnefo/lib/trace.py +++ b/snf-common/synnefo/lib/trace.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. def set_signal_trap(): diff --git a/snf-common/synnefo/lib/utils.py b/snf-common/synnefo/lib/utils.py index 5e6db22ad1dc75d70ce0fb0ce06f3f3c6b782e32..8f204329ed949ed11cd7a7659ae0fc35d5ef901c 100644 --- a/snf-common/synnefo/lib/utils.py +++ b/snf-common/synnefo/lib/utils.py @@ -1,35 +1,17 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import datetime import copy diff --git a/snf-common/synnefo/settings/__init__.py b/snf-common/synnefo/settings/__init__.py index 48d79f136784bf43b0c8ed95f5f19f43a44d3d41..cd512f88061038b6329fad18e80464dc41642de1 100644 --- a/snf-common/synnefo/settings/__init__.py +++ b/snf-common/synnefo/settings/__init__.py @@ -1,35 +1,17 @@ -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import os import sys diff --git a/snf-common/synnefo/settings/default/__init__.py b/snf-common/synnefo/settings/default/__init__.py index b47fa29b2829f2ed6297f8d72a88206885568478..66688b243e6d960aa085d06c82012a2e1607b0a6 100644 --- a/snf-common/synnefo/settings/default/__init__.py +++ b/snf-common/synnefo/settings/default/__init__.py @@ -1,34 +1,16 @@ -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from synnefo.settings.default.admins import * diff --git a/snf-common/synnefo/settings/default/admins.py b/snf-common/synnefo/settings/default/admins.py index ba08da17d7e7edcd9176e3656796ee3d07003704..880a7058a0993b712a13783dbb8970eb597e2e58 100644 --- a/snf-common/synnefo/settings/default/admins.py +++ b/snf-common/synnefo/settings/default/admins.py @@ -35,3 +35,12 @@ CONTACT_EMAIL = "support@synnefo.org" # Email address the emails sent by the service will come from SERVER_EMAIL = "Synnefo cloud <cloud@synnefo.org>" + + +# Synnefo logging directory +############################ + +# Directory where log files are saved +# Currently only snf-manage uses this to save +# the output of the commands being executed. +LOG_DIR = "/var/log/synnefo/" diff --git a/snf-common/synnefo/settings/test.py b/snf-common/synnefo/settings/test.py index 731cf392d3dbf33e78b32abd72d36ebccd2d8662..dd3b512248d91ced0cb9a9783415e29cbedc452e 100644 --- a/snf-common/synnefo/settings/test.py +++ b/snf-common/synnefo/settings/test.py @@ -4,6 +4,7 @@ os.environ['SYNNEFO_SETTINGS_DIR'] = '/etc/synnefo-test-settings' from synnefo.settings import * DEBUG = False +TEMPLATE_DEBUG = False TEST = True CACHE_BACKEND = os.environ.get('SNF_TEST_CACHE_BACKEND', 'locmem://') @@ -29,7 +30,10 @@ SNF_TEST_PITHOS_UPDATE_MD5 = bool(int(os.environ.get( 'SNF_TEST_PITHOS_UPDATE_MD5', False))) SNF_TEST_PITHOS_SQLITE_MODULE = bool(int(os.environ.get( 'SNF_TEST_PITHOS_SQLITE_MODULE', False))) - +PASSWORD_HASHERS = ( + os.environ.get('SNF_TEST_PASSWORD_HASHERS', + 'django.contrib.auth.hashers.MD5PasswordHasher'), +) # override default database if SNF_TEST_USE_POSTGRES: @@ -69,3 +73,5 @@ CLOUDBAR_SERVICES_URL = '/ui/get_services' CLOUDBAR_MENU_URL = '/ui/get_menu' TEST_RUNNER = 'pithos.api.test.PithosTestSuiteRunner' + +CYCLADES_VOLUME_MAX_SIZE = 100000 diff --git a/snf-common/synnefo/util/date.py b/snf-common/synnefo/util/date.py index 8d87435ec4fe9bb812570c45eac4c88d6a4b3b47..a206a897cdbaaf24df76a90205361b29698bf26d 100644 --- a/snf-common/synnefo/util/date.py +++ b/snf-common/synnefo/util/date.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from datetime import timedelta, tzinfo diff --git a/snf-common/synnefo/util/dictconfig.py b/snf-common/synnefo/util/dictconfig.py deleted file mode 100644 index 167831bc150fe423864fe93c1404bc38557e04a8..0000000000000000000000000000000000000000 --- a/snf-common/synnefo/util/dictconfig.py +++ /dev/null @@ -1,553 +0,0 @@ -# This is a copy of the Python logging.config.dictconfig module. -# It is provided here for backwards compatibility for Python versions -# prior to 2.7. -# -# Copyright 2009-2010 by Vinay Sajip. All Rights Reserved. -# -# Permission to use, copy, modify, and distribute this software and its -# documentation for any purpose and without fee is hereby granted, -# provided that the above copyright notice appear in all copies and that -# both that copyright notice and this permission notice appear in -# supporting documentation, and that the name of Vinay Sajip -# not be used in advertising or publicity pertaining to distribution -# of the software without specific, written prior permission. -# VINAY SAJIP DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING -# ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL -# VINAY SAJIP BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR -# ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER -# IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT -# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -import logging.handlers -import re -import sys -import types - -IDENTIFIER = re.compile('^[a-z_][a-z0-9_]*$', re.I) - -def valid_ident(s): - m = IDENTIFIER.match(s) - if not m: - raise ValueError('Not a valid Python identifier: %r' % s) - return True - -# -# This function is defined in logging only in recent versions of Python -# -try: - from logging import _checkLevel -except ImportError: - def _checkLevel(level): - if isinstance(level, int): - rv = level - elif str(level) == level: - if level not in logging._levelNames: - raise ValueError('Unknown level: %r' % level) - rv = logging._levelNames[level] - else: - raise TypeError('Level not an integer or a ' - 'valid string: %r' % level) - return rv - -# The ConvertingXXX classes are wrappers around standard Python containers, -# and they serve to convert any suitable values in the container. The -# conversion converts base dicts, lists and tuples to their wrapped -# equivalents, whereas strings which match a conversion format are converted -# appropriately. -# -# Each wrapper should have a configurator attribute holding the actual -# configurator to use for conversion. - -class ConvertingDict(dict): - """A converting dictionary wrapper.""" - - def __getitem__(self, key): - value = dict.__getitem__(self, key) - result = self.configurator.convert(value) - #If the converted value is different, save for next time - if value is not result: - self[key] = result - if type(result) in (ConvertingDict, ConvertingList, - ConvertingTuple): - result.parent = self - result.key = key - return result - - def get(self, key, default=None): - value = dict.get(self, key, default) - result = self.configurator.convert(value) - #If the converted value is different, save for next time - if value is not result: - self[key] = result - if type(result) in (ConvertingDict, ConvertingList, - ConvertingTuple): - result.parent = self - result.key = key - return result - - def pop(self, key, default=None): - value = dict.pop(self, key, default) - result = self.configurator.convert(value) - if value is not result: - if type(result) in (ConvertingDict, ConvertingList, - ConvertingTuple): - result.parent = self - result.key = key - return result - -class ConvertingList(list): - """A converting list wrapper.""" - def __getitem__(self, key): - value = list.__getitem__(self, key) - result = self.configurator.convert(value) - #If the converted value is different, save for next time - if value is not result: - self[key] = result - if type(result) in (ConvertingDict, ConvertingList, - ConvertingTuple): - result.parent = self - result.key = key - return result - - def pop(self, idx=-1): - value = list.pop(self, idx) - result = self.configurator.convert(value) - if value is not result: - if type(result) in (ConvertingDict, ConvertingList, - ConvertingTuple): - result.parent = self - return result - -class ConvertingTuple(tuple): - """A converting tuple wrapper.""" - def __getitem__(self, key): - value = tuple.__getitem__(self, key) - result = self.configurator.convert(value) - if value is not result: - if type(result) in (ConvertingDict, ConvertingList, - ConvertingTuple): - result.parent = self - result.key = key - return result - -class BaseConfigurator(object): - """ - The configurator base class which defines some useful defaults. - """ - - CONVERT_PATTERN = re.compile(r'^(?P<prefix>[a-z]+)://(?P<suffix>.*)$') - - WORD_PATTERN = re.compile(r'^\s*(\w+)\s*') - DOT_PATTERN = re.compile(r'^\.\s*(\w+)\s*') - INDEX_PATTERN = re.compile(r'^\[\s*(\w+)\s*\]\s*') - DIGIT_PATTERN = re.compile(r'^\d+$') - - value_converters = { - 'ext' : 'ext_convert', - 'cfg' : 'cfg_convert', - } - - # We might want to use a different one, e.g. importlib - importer = __import__ - - def __init__(self, config): - self.config = ConvertingDict(config) - self.config.configurator = self - - def resolve(self, s): - """ - Resolve strings to objects using standard import and attribute - syntax. - """ - name = s.split('.') - used = name.pop(0) - try: - found = self.importer(used) - for frag in name: - used += '.' + frag - try: - found = getattr(found, frag) - except AttributeError: - self.importer(used) - found = getattr(found, frag) - return found - except ImportError: - e, tb = sys.exc_info()[1:] - v = ValueError('Cannot resolve %r: %s' % (s, e)) - v.__cause__, v.__traceback__ = e, tb - raise v - - def ext_convert(self, value): - """Default converter for the ext:// protocol.""" - return self.resolve(value) - - def cfg_convert(self, value): - """Default converter for the cfg:// protocol.""" - rest = value - m = self.WORD_PATTERN.match(rest) - if m is None: - raise ValueError("Unable to convert %r" % value) - else: - rest = rest[m.end():] - d = self.config[m.groups()[0]] - #print d, rest - while rest: - m = self.DOT_PATTERN.match(rest) - if m: - d = d[m.groups()[0]] - else: - m = self.INDEX_PATTERN.match(rest) - if m: - idx = m.groups()[0] - if not self.DIGIT_PATTERN.match(idx): - d = d[idx] - else: - try: - n = int(idx) # try as number first (most likely) - d = d[n] - except TypeError: - d = d[idx] - if m: - rest = rest[m.end():] - else: - raise ValueError('Unable to convert ' - '%r at %r' % (value, rest)) - #rest should be empty - return d - - def convert(self, value): - """ - Convert values to an appropriate type. dicts, lists and tuples are - replaced by their converting alternatives. Strings are checked to - see if they have a conversion format and are converted if they do. - """ - if not isinstance(value, ConvertingDict) and isinstance(value, dict): - value = ConvertingDict(value) - value.configurator = self - elif not isinstance(value, ConvertingList) and isinstance(value, list): - value = ConvertingList(value) - value.configurator = self - elif not isinstance(value, ConvertingTuple) and\ - isinstance(value, tuple): - value = ConvertingTuple(value) - value.configurator = self - elif isinstance(value, basestring): # str for py3k - m = self.CONVERT_PATTERN.match(value) - if m: - d = m.groupdict() - prefix = d['prefix'] - converter = self.value_converters.get(prefix, None) - if converter: - suffix = d['suffix'] - converter = getattr(self, converter) - value = converter(suffix) - return value - - def configure_custom(self, config): - """Configure an object with a user-supplied factory.""" - c = config.pop('()') - if not hasattr(c, '__call__') and hasattr(types, 'ClassType') and type(c) != types.ClassType: - c = self.resolve(c) - props = config.pop('.', None) - # Check for valid identifiers - kwargs = dict([(k, config[k]) for k in config if valid_ident(k)]) - result = c(**kwargs) - if props: - for name, value in props.items(): - setattr(result, name, value) - return result - - def as_tuple(self, value): - """Utility function which converts lists to tuples.""" - if isinstance(value, list): - value = tuple(value) - return value - -class DictConfigurator(BaseConfigurator): - """ - Configure logging using a dictionary-like object to describe the - configuration. - """ - - def configure(self): - """Do the configuration.""" - - config = self.config - if 'version' not in config: - raise ValueError("dictionary doesn't specify a version") - if config['version'] != 1: - raise ValueError("Unsupported version: %s" % config['version']) - incremental = config.pop('incremental', False) - EMPTY_DICT = {} - logging._acquireLock() - try: - if incremental: - handlers = config.get('handlers', EMPTY_DICT) - # incremental handler config only if handler name - # ties in to logging._handlers (Python 2.7) - if sys.version_info[:2] == (2, 7): - for name in handlers: - if name not in logging._handlers: - raise ValueError('No handler found with ' - 'name %r' % name) - else: - try: - handler = logging._handlers[name] - handler_config = handlers[name] - level = handler_config.get('level', None) - if level: - handler.setLevel(_checkLevel(level)) - except StandardError, e: - raise ValueError('Unable to configure handler ' - '%r: %s' % (name, e)) - loggers = config.get('loggers', EMPTY_DICT) - for name in loggers: - try: - self.configure_logger(name, loggers[name], True) - except StandardError, e: - raise ValueError('Unable to configure logger ' - '%r: %s' % (name, e)) - root = config.get('root', None) - if root: - try: - self.configure_root(root, True) - except StandardError, e: - raise ValueError('Unable to configure root ' - 'logger: %s' % e) - else: - disable_existing = config.pop('disable_existing_loggers', True) - - logging._handlers.clear() - del logging._handlerList[:] - - # Do formatters first - they don't refer to anything else - formatters = config.get('formatters', EMPTY_DICT) - for name in formatters: - try: - formatters[name] = self.configure_formatter( - formatters[name]) - except StandardError, e: - raise ValueError('Unable to configure ' - 'formatter %r: %s' % (name, e)) - # Next, do filters - they don't refer to anything else, either - filters = config.get('filters', EMPTY_DICT) - for name in filters: - try: - filters[name] = self.configure_filter(filters[name]) - except StandardError, e: - raise ValueError('Unable to configure ' - 'filter %r: %s' % (name, e)) - - # Next, do handlers - they refer to formatters and filters - # As handlers can refer to other handlers, sort the keys - # to allow a deterministic order of configuration - handlers = config.get('handlers', EMPTY_DICT) - for name in sorted(handlers): - try: - handler = self.configure_handler(handlers[name]) - handler.name = name - handlers[name] = handler - except StandardError, e: - raise ValueError('Unable to configure handler ' - '%r: %s' % (name, e)) - # Next, do loggers - they refer to handlers and filters - - #we don't want to lose the existing loggers, - #since other threads may have pointers to them. - #existing is set to contain all existing loggers, - #and as we go through the new configuration we - #remove any which are configured. At the end, - #what's left in existing is the set of loggers - #which were in the previous configuration but - #which are not in the new configuration. - root = logging.root - existing = root.manager.loggerDict.keys() - #The list needs to be sorted so that we can - #avoid disabling child loggers of explicitly - #named loggers. With a sorted list it is easier - #to find the child loggers. - existing.sort() - #We'll keep the list of existing loggers - #which are children of named loggers here... - child_loggers = [] - #now set up the new ones... - loggers = config.get('loggers', EMPTY_DICT) - for name in loggers: - if name in existing: - i = existing.index(name) - prefixed = name + "." - pflen = len(prefixed) - num_existing = len(existing) - i = i + 1 # look at the entry after name - while (i < num_existing) and\ - (existing[i][:pflen] == prefixed): - child_loggers.append(existing[i]) - i = i + 1 - existing.remove(name) - try: - self.configure_logger(name, loggers[name]) - except StandardError, e: - raise ValueError('Unable to configure logger ' - '%r: %s' % (name, e)) - - #Disable any old loggers. There's no point deleting - #them as other threads may continue to hold references - #and by disabling them, you stop them doing any logging. - #However, don't disable children of named loggers, as that's - #probably not what was intended by the user. - for log in existing: - logger = root.manager.loggerDict[log] - if log in child_loggers: - logger.level = logging.NOTSET - logger.handlers = [] - logger.propagate = True - elif disable_existing: - logger.disabled = True - - # And finally, do the root logger - root = config.get('root', None) - if root: - try: - self.configure_root(root) - except StandardError, e: - raise ValueError('Unable to configure root ' - 'logger: %s' % e) - finally: - logging._releaseLock() - - def configure_formatter(self, config): - """Configure a formatter from a dictionary.""" - if '()' in config: - factory = config['()'] # for use in exception handler - try: - result = self.configure_custom(config) - except TypeError, te: - if "'format'" not in str(te): - raise - #Name of parameter changed from fmt to format. - #Retry with old name. - #This is so that code can be used with older Python versions - #(e.g. by Django) - config['fmt'] = config.pop('format') - config['()'] = factory - result = self.configure_custom(config) - else: - fmt = config.get('format', None) - dfmt = config.get('datefmt', None) - result = logging.Formatter(fmt, dfmt) - return result - - def configure_filter(self, config): - """Configure a filter from a dictionary.""" - if '()' in config: - result = self.configure_custom(config) - else: - name = config.get('name', '') - result = logging.Filter(name) - return result - - def add_filters(self, filterer, filters): - """Add filters to a filterer from a list of names.""" - for f in filters: - try: - filterer.addFilter(self.config['filters'][f]) - except StandardError, e: - raise ValueError('Unable to add filter %r: %s' % (f, e)) - - def configure_handler(self, config): - """Configure a handler from a dictionary.""" - formatter = config.pop('formatter', None) - if formatter: - try: - formatter = self.config['formatters'][formatter] - except StandardError, e: - raise ValueError('Unable to set formatter ' - '%r: %s' % (formatter, e)) - level = config.pop('level', None) - filters = config.pop('filters', None) - if '()' in config: - c = config.pop('()') - if not hasattr(c, '__call__') and hasattr(types, 'ClassType') and type(c) != types.ClassType: - c = self.resolve(c) - factory = c - else: - klass = self.resolve(config.pop('class')) - #Special case for handler which refers to another handler - if issubclass(klass, logging.handlers.MemoryHandler) and\ - 'target' in config: - try: - config['target'] = self.config['handlers'][config['target']] - except StandardError, e: - raise ValueError('Unable to set target handler ' - '%r: %s' % (config['target'], e)) - elif issubclass(klass, logging.handlers.SMTPHandler) and\ - 'mailhost' in config: - config['mailhost'] = self.as_tuple(config['mailhost']) - elif issubclass(klass, logging.handlers.SysLogHandler) and\ - 'address' in config: - config['address'] = self.as_tuple(config['address']) - factory = klass - kwargs = dict([(k, config[k]) for k in config if valid_ident(k)]) - try: - result = factory(**kwargs) - except TypeError, te: - if "'stream'" not in str(te): - raise - #The argument name changed from strm to stream - #Retry with old name. - #This is so that code can be used with older Python versions - #(e.g. by Django) - kwargs['strm'] = kwargs.pop('stream') - result = factory(**kwargs) - if formatter: - result.setFormatter(formatter) - if level is not None: - result.setLevel(_checkLevel(level)) - if filters: - self.add_filters(result, filters) - return result - - def add_handlers(self, logger, handlers): - """Add handlers to a logger from a list of names.""" - for h in handlers: - try: - logger.addHandler(self.config['handlers'][h]) - except StandardError, e: - raise ValueError('Unable to add handler %r: %s' % (h, e)) - - def common_logger_config(self, logger, config, incremental=False): - """ - Perform configuration which is common to root and non-root loggers. - """ - level = config.get('level', None) - if level is not None: - logger.setLevel(_checkLevel(level)) - if not incremental: - #Remove any existing handlers - for h in logger.handlers[:]: - logger.removeHandler(h) - handlers = config.get('handlers', None) - if handlers: - self.add_handlers(logger, handlers) - filters = config.get('filters', None) - if filters: - self.add_filters(logger, filters) - - def configure_logger(self, name, config, incremental=False): - """Configure a non-root logger from a dictionary.""" - logger = logging.getLogger(name) - self.common_logger_config(logger, config, incremental) - propagate = config.get('propagate', None) - if propagate is not None: - logger.propagate = propagate - - def configure_root(self, config, incremental=False): - """Configure a root logger from a dictionary.""" - root = logging.getLogger() - self.common_logger_config(root, config, incremental) - -dictConfigClass = DictConfigurator - -def dictConfig(config): - """Configure logging using a dictionary.""" - dictConfigClass(config).configure() diff --git a/snf-common/synnefo/util/entry_points.py b/snf-common/synnefo/util/entry_points.py index aaeb409c2c897b8564734e509167707f76116a7a..d610b1dafa4d2f39854a3c9c363f4568ccc1df36 100644 --- a/snf-common/synnefo/util/entry_points.py +++ b/snf-common/synnefo/util/entry_points.py @@ -1,35 +1,17 @@ -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import sys import pkg_resources diff --git a/snf-common/synnefo/util/keypath.py b/snf-common/synnefo/util/keypath.py deleted file mode 100644 index 02b893990bf4c49cbc1cf597b623ab351153098f..0000000000000000000000000000000000000000 --- a/snf-common/synnefo/util/keypath.py +++ /dev/null @@ -1,251 +0,0 @@ -# Copyright 2013 GRNET S.A. All rights reserved. -# -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: -# -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. -# -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. - - -import re -integer_re = re.compile('-?[0-9]+') - - -def join_path(sep, path): - iterable = ((str(n) if isinstance(n, (int, long)) else n) for n in path) - return sep.join(iterable) - - -def lookup_path(container, path, sep='.', createpath=False): - """ - return (['a','b'], - [container['a'], container['a']['b']], - 'c') where path=sep.join(['a','b','c']) - - """ - names = path.split(sep) - dirnames = names[:-1] - basename = names[-1] - if integer_re.match(basename): - basename = int(basename) - - node = container - name_path = [] - node_path = [node] - for name in dirnames: - name_path.append(name) - - if integer_re.match(name): - name = int(name) - - try: - node = node[name] - except KeyError as e: - if not createpath: - m = "'{0}': path not found".format(join_path(sep, name_path)) - raise KeyError(m) - node[name] = {} - node = node[name] - except IndexError as e: - if not createpath: - m = "'{0}': path not found: {1}".format( - join_path(sep, name_path), e) - raise KeyError(m) - size = name if name > 0 else -name - node += (dict() for _ in xrange(len(node), size)) - node = node[name] - except TypeError as e: - m = "'{0}': cannot traverse path beyond this node: {1}" - m = m.format(join_path(sep, name_path), str(e)) - raise ValueError(m) - node_path.append(node) - - return name_path, node_path, basename - - -def walk_paths(container): - for name, node in container.iteritems(): - if not hasattr(node, 'items'): - yield [name], [node] - else: - for names, nodes in walk_paths(node): - yield [name] + names, [node] + nodes - - -def list_paths(container, sep='.'): - """ - >>> sorted(list_paths({'a': {'b': {'c': 'd'}}})) - [('a.b.c', 'd')] - >>> sorted(list_paths({'a': {'b': {'c': 'd'}, 'e': 3}})) - [('a.b.c', 'd'), ('a.e', 3)] - >>> sorted(list_paths({'a': {'b': {'c': 'd'}, 'e': {'f': 3}}})) - [('a.b.c', 'd'), ('a.e.f', 3)] - >>> sorted(list_paths({'a': [{'b': 3}, 2]})) - [('a', [{'b': 3}, 2])] - >>> list_paths({}) - [] - - """ - return [(join_path(sep, name_path), node_path[-1]) - for name_path, node_path in walk_paths(container)] - - -def del_path(container, path, sep='.', collect=True): - """ - del container['a']['b']['c'] where path=sep.join(['a','b','c']) - - >>> d = {'a': {'b': {'c': 'd'}}}; del_path(d, 'a.b.c'); d - {} - >>> d = {'a': {'b': {'c': 'd'}}}; del_path(d, 'a.b.c', collect=False); d - {'a': {'b': {}}} - >>> d = {'a': {'b': {'c': 'd'}}}; del_path(d, 'a.b.c.d') - Traceback (most recent call last): - ValueError: 'a.b.c': cannot traverse path beyond this node:\ - 'str' object does not support item deletion - """ - - name_path, node_path, basename = \ - lookup_path(container, path, sep=sep, createpath=False) - - lastnode = node_path.pop() - lastname = basename - try: - if basename in lastnode: - del lastnode[basename] - except (TypeError, KeyError) as e: - m = "'{0}': cannot traverse path beyond this node: {1}" - m = m.format(join_path(sep, name_path), str(e)) - raise ValueError(m) - - if collect: - while node_path and not lastnode: - basename = name_path.pop() - lastnode = node_path.pop() - del lastnode[basename] - - -def get_path(container, path, sep='.'): - """ - return container['a']['b']['c'] where path=sep.join(['a','b','c']) - - >>> get_path({'a': {'b': {'c': 'd'}}}, 'a.b.c.d') - Traceback (most recent call last): - ValueError: 'a.b.c.d': cannot traverse path beyond this node:\ - string indices must be integers, not str - >>> get_path({'a': {'b': {'c': 1}}}, 'a.b.c.d') - Traceback (most recent call last): - ValueError: 'a.b.c.d': cannot traverse path beyond this node:\ - 'int' object is unsubscriptable - >>> get_path({'a': {'b': {'c': 1}}}, 'a.b.c') - 1 - >>> get_path({'a': {'b': {'c': 1}}}, 'a.b') - {'c': 1} - >>> get_path({'a': [{'z': 1}]}, 'a.0') - {'z': 1} - >>> get_path({'a': [{'z': 1}]}, 'a.0.z') - 1 - >>> get_path({'a': [{'z': 1}]}, 'a.-1.z') - 1 - - """ - name_path, node_path, basename = \ - lookup_path(container, path, sep=sep, createpath=False) - name_path.append(basename) - node = node_path[-1] - - try: - return node[basename] - except TypeError as e: - m = "'{0}': cannot traverse path beyond this node: {1}" - m = m.format(join_path(sep, name_path), str(e)) - raise ValueError(m) - except KeyError as e: - m = "'{0}': path not found: {1}" - m = m.format(join_path(sep, name_path), str(e)) - raise KeyError(m) - - -def set_path(container, path, value, sep='.', - createpath=False, overwrite=True): - """ - container['a']['b']['c'] = value where path=sep.join(['a','b','c']) - - >>> set_path({'a': {'b': {'c': 'd'}}}, 'a.b.c.d', 1) - Traceback (most recent call last): - ValueError: 'a.b.c.d': cannot index non-object node with string - >>> set_path({'a': {'b': {'c': 'd'}}}, 'a.b.x.d', 1) - Traceback (most recent call last): - KeyError: "'a.b.x': path not found" - >>> set_path({'a': {'b': {'c': 'd'}}}, 'a.b.x.d', 1, createpath=True) - - >>> set_path({'a': {'b': {'c': 'd'}}}, 'a.b.c', 1) - - >>> set_path({'a': {'b': {'c': 'd'}}}, 'a.b.c', 1, overwrite=False) - Traceback (most recent call last): - ValueError: will not overwrite path 'a.b.c' - >>> d = {'a': [{'z': 1}]}; set_path(d, 'a.-2.1', 2, createpath=False) - Traceback (most recent call last): - KeyError: "'a.-2': path not found: list index out of range" - >>> d = {'a': [{'z': 1}]}; set_path(d, 'a.-2.1', 2, createpath=True) - Traceback (most recent call last): - ValueError: 'a.-2.1': will not index object node with integer - >>> d = {'a': [{'z': 1}]}; set_path(d, 'a.-2.z', 2, createpath=True); \ - d['a'][-2]['z'] - 2 - - """ - name_path, node_path, basename = \ - lookup_path(container, path, sep=sep, createpath=createpath) - name_path.append(basename) - node = node_path[-1] - - if basename in node and not overwrite: - m = "will not overwrite path '{0}'".format(path) - raise ValueError(m) - - is_object_node = hasattr(node, 'keys') - is_string_name = isinstance(basename, basestring) - if not is_string_name and is_object_node: - m = "'{0}': will not index object node with integer" - m = m.format(join_path(sep, name_path)) - raise ValueError(m) - if is_string_name and not is_object_node: - m = "'{0}': cannot index non-object node with string" - m = m.format(join_path(sep, name_path)) - raise ValueError(m) - try: - node[basename] = value - except TypeError as e: - m = "'{0}': cannot traverse path beyond this node: {1}" - m = m.format(join_path(sep, name_path), str(e)) - raise ValueError(m) - - -if __name__ == '__main__': - import doctest - doctest.testmod() diff --git a/snf-common/synnefo/util/mac2eui64.py b/snf-common/synnefo/util/mac2eui64.py index 58d9b2c42dc2dfc1c1744ade1a1ed29117c61b1a..51096e2c7cebebec5128d4cf697250d41840b7fe 100644 --- a/snf-common/synnefo/util/mac2eui64.py +++ b/snf-common/synnefo/util/mac2eui64.py @@ -1,35 +1,17 @@ -# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # from IPy import IP diff --git a/snf-common/synnefo/util/number.py b/snf-common/synnefo/util/number.py index ce017a030a31f3e3383ffd622f0cffda4cbb2fc2..e9bfaf5c7de8c3d0fd687534a2776f71be6de542 100644 --- a/snf-common/synnefo/util/number.py +++ b/snf-common/synnefo/util/number.py @@ -1,37 +1,19 @@ # -*- coding: utf-8 -*- -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. def strbigdec(bignum, nr_lsd=12): diff --git a/snf-common/synnefo/util/text.py b/snf-common/synnefo/util/text.py index b53313d28f124656995055e3725861cac3ed099e..ab74edcc12f6811b8eb161ef76f3b844afe6104f 100644 --- a/snf-common/synnefo/util/text.py +++ b/snf-common/synnefo/util/text.py @@ -1,37 +1,19 @@ # -*- coding: utf-8 -*- -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. uenc_encoding = 'UTF-8' diff --git a/snf-common/synnefo/util/units.py b/snf-common/synnefo/util/units.py index 7e39679411a9727f50b660bfacd46fadd792dc80..74e7ad32701999cef4a7056d53508b9d72a8d8c9 100644 --- a/snf-common/synnefo/util/units.py +++ b/snf-common/synnefo/util/units.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from synnefo.lib.ordereddict import OrderedDict import re @@ -77,6 +59,9 @@ def _parse_number_with_unit(s): def parse_with_style(s): + if isinstance(s, (int, long)): + return s, 0 + if s in ['inf', 'infinite']: return PRACTICALLY_INFINITE, 0 @@ -139,12 +124,12 @@ def get_exponent(style): raise StyleError() -def show(n, unit, style=None): +def show(n, unit, style=None, inf='inf'): if style == 'none': return str(n) if n == PRACTICALLY_INFINITE: - return 'inf' + return inf try: unit_dict = UNITS[unit] diff --git a/snf-common/synnefo/util/version.py b/snf-common/synnefo/util/version.py index b32f40d86f52f026f5739e1dce5f719fdfe171de..1acb4c329157a87b4c1e35e229a0de9411f2c1e8 100644 --- a/snf-common/synnefo/util/version.py +++ b/snf-common/synnefo/util/version.py @@ -1,35 +1,17 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import pkg_resources import os diff --git a/snf-common/synnefo/versions/__init__.py b/snf-common/synnefo/versions/__init__.py index c78fbe672324992f6c941a7828e4827928b270c9..5cda842903d0bfec117db1c3c5a43281d46eb551 100644 --- a/snf-common/synnefo/versions/__init__.py +++ b/snf-common/synnefo/versions/__init__.py @@ -1,35 +1,17 @@ -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # this is a namespace package try: diff --git a/snf-cyclades-app/MANIFEST.in b/snf-cyclades-app/MANIFEST.in index 228020b9bc92e1ce040bc8e74a8dd8af98eb40cd..e63594453d5793c23af1961e82eba906e59af145 100644 --- a/snf-cyclades-app/MANIFEST.in +++ b/snf-cyclades-app/MANIFEST.in @@ -1,6 +1,6 @@ recursive-include synnefo *.json *.html *.json *.xml *.txt recursive-include synnefo/admin/static * recursive-include synnefo/ui/static * -recursive-include docs/ *.rst +recursive-include docs *.rst -include distribute_setup.py +include distribute_setup.py README.md diff --git a/snf-cyclades-app/README.md b/snf-cyclades-app/README.md new file mode 100644 index 0000000000000000000000000000000000000000..db5fa09fefb8ddd69975a4c56820cf6bba7d1b98 --- /dev/null +++ b/snf-cyclades-app/README.md @@ -0,0 +1,27 @@ +snf-cyclades-app +================ + +Overview +-------- + +This is Synnefo's snf-cyclades-app component. Please see the [official Synnefo +site](http://www.synnefo.org) for more information. + + +Copyright and license +===================== + +Copyright (C) 2010-2014 GRNET S.A. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. diff --git a/snf-cyclades-app/conf/20-snf-cyclades-app-api.conf b/snf-cyclades-app/conf/20-snf-cyclades-app-api.conf index f0f18b00bc200c95fd052c255bb504c449842188..fbbe3c6ee4927bec472dd942ebc10c95566f39c2 100644 --- a/snf-cyclades-app/conf/20-snf-cyclades-app-api.conf +++ b/snf-cyclades-app/conf/20-snf-cyclades-app-api.conf @@ -16,6 +16,11 @@ ## Astakos groups that have access to '/admin' views. #ADMIN_STATS_PERMITTED_GROUPS = ["admin-stats"] # +## Enable/Disable the snapshots feature altogether at the API level. +## If set to False, Cyclades will not expose the '/snapshots' API URL +## of the 'volume' app. +#CYCLADES_SNAPSHOTS_ENABLED = True +# ## ## Network Configuration ## @@ -149,22 +154,34 @@ ##} #CYCLADES_PORT_FORWARDING = {} -## Extra configuration options required for snf-vncauthproxy (>=1.5) -#CYCLADES_VNCAUTHPROXY_OPTS = { -# # These values are required for VNC console support. They should match a -# # user / password configured in the snf-vncauthproxy authentication / users -# # file (/var/lib/vncauthproxy/users). -# 'auth_user': 'synnefo', -# 'auth_password': 'secret_password', -# # server_address and server_port should reflect the --listen-address and -# # --listen-port options passed to the vncauthproxy daemon -# 'server_address': '127.0.0.1', -# 'server_port': 24999, -# # Set to True to enable SSL support on the control socket. -# 'enable_ssl': False, -# # If you enabled SSL support for snf-vncauthproxy you can optionally -# # provide a path to a CA file and enable strict checkfing for the server -# # certficiate. -# 'ca_cert': None, -# 'strict': False, -#} +## Extra configuration options required for snf-vncauthproxy (>=1.5). Each dict +## of the list, describes one vncauthproxy instance. +#CYCLADES_VNCAUTHPROXY_OPTS = [ +# { +# # These values are required for VNC console support. They should match +# # a user / password configured in the snf-vncauthproxy authentication / +# # users file (/var/lib/vncauthproxy/users). +# 'auth_user': 'synnefo', +# 'auth_password': 'secret_password', +# # server_address and server_port should reflect the --listen-address and +# # --listen-port options passed to the vncauthproxy daemon +# 'server_address': '127.0.0.1', +# 'server_port': 24999, +# # Set to True to enable SSL support on the control socket. +# 'enable_ssl': False, +# # If you enabled SSL support for snf-vncauthproxy you can optionally +# # provide a path to a CA file and enable strict checkfing for the server +# # certficiate. +# 'ca_cert': None, +# 'strict': False, +# }, +#] +# +## The maximum allowed size(GB) for a Cyclades Volume +#CYCLADES_VOLUME_MAX_SIZE = 200 +# +## The maximum allowed metadata items for a Cyclades Volume +#CYCLADES_VOLUME_MAX_METADATA = 10 +# +## The maximum allowed metadata items for a Cyclades Virtual Machine +#CYCLADES_VM_MAX_METADATA = 10 diff --git a/snf-cyclades-app/conf/20-snf-cyclades-app-backend.conf b/snf-cyclades-app/conf/20-snf-cyclades-app-backend.conf index aa5a926774e28a6c8c90fb0355c4a9d722194f34..a660effcc04db7f10c684657971335d217ff11ca 100644 --- a/snf-cyclades-app/conf/20-snf-cyclades-app-backend.conf +++ b/snf-cyclades-app/conf/20-snf-cyclades-app-backend.conf @@ -27,16 +27,20 @@ # 'hvparams': {'kvm': {'serial_console': False}, # 'xen-pvm': {}, # 'xen-hvm': {}}, -# 'wait_for_sync': False} +#} # ## If True, qemu-kvm will hotplug a NIC when connecting a vm to ## a network. This requires qemu-kvm=1.0. -#GANETI_USE_HOTPLUG = False +#GANETI_USE_HOTPLUG = True # ## If True, Ganeti will try to allocate new instances only on nodes that are ## not already locked. This might result in slightly unbalanced clusters. #GANETI_USE_OPPORTUNISTIC_LOCKING = True # +## If False, Ganeti will not wait for the disk mirror to sync +## (--no-wait-for-sync option in Ganeti). Useful only for DRBD template. +#GANETI_DISKS_WAIT_FOR_SYNC = False +# ## This module implements the strategy for allocating a vm to a backend #BACKEND_ALLOCATOR_MODULE = "synnefo.logic.allocators.default_allocator" ## Refresh backend statistics timeout, in minutes, used in backend allocation @@ -46,6 +50,10 @@ ## than 'max:nic-count' option of Ganeti's ipolicy. #GANETI_MAX_NICS_PER_INSTANCE = 8 # +## Maximum number of disks per Ganeti instance. This value must be less or +## equal than 'max:disk-count' option of Ganeti's ipolicy. +#GANETI_MAX_DISKS_PER_INSTANCE = 8 +# ## The following setting defines a dictionary with key-value parameters to be ## passed to each Ganeti ExtStorage provider. The setting defines a mapping ## from the provider name, e.g. 'archipelago' to a dictionary with the actual diff --git a/snf-cyclades-app/conf/20-snf-cyclades-app-plankton.conf b/snf-cyclades-app/conf/20-snf-cyclades-app-plankton.conf index 50a8839b5e7ffe3b43cbe9002f2d8c6404ec1bf3..19a262c1210d6edff7f5296d23c20616aa1698fa 100644 --- a/snf-cyclades-app/conf/20-snf-cyclades-app-plankton.conf +++ b/snf-cyclades-app/conf/20-snf-cyclades-app-plankton.conf @@ -5,7 +5,6 @@ # ## Backend settings #BACKEND_DB_CONNECTION = 'sqlite:////usr/share/synnefo/pithos/backend.db' -#BACKEND_BLOCK_PATH = '/usr/share/synnefo/pithos/data/' #PITHOS_BACKEND_POOL_SIZE = 8 # ## The Pithos container where images will be stored by default @@ -19,3 +18,14 @@ # ## The owner of the images that will be marked as "system images" by the UI #SYSTEM_IMAGES_OWNER = 'okeanos' +#Archipelago Configuration File +#PITHOS_BACKEND_ARCHIPELAGO_CONF = '/etc/archipelago/archipelago.conf' +# +#Archipelagp xseg pool size +#PITHOS_BACKEND_XSEG_POOL_SIZE = 8 +# +#The maximum interval (in seconds) for consequent backend object map checks +#PITHOS_BACKEND_MAP_CHECK_INTERVAL = 1 +# +#The maximum allowed number of image metadata +#PITHOS_RESOURCE_MAX_METADATA = 32 diff --git a/snf-cyclades-app/conf/20-snf-cyclades-app-ui.conf b/snf-cyclades-app/conf/20-snf-cyclades-app-ui.conf index 3ca7f13e277ebeb21cccb9972cb9a34692898ba5..9203b92c08d40a7e9712400a15690202e5cf0c44 100644 --- a/snf-cyclades-app/conf/20-snf-cyclades-app-ui.conf +++ b/snf-cyclades-app/conf/20-snf-cyclades-app-ui.conf @@ -179,9 +179,5 @@ ## Message to display for vms with empty fqdn value #UI_NO_FQDN_MESSAGE = 'No available FQDN' # -##Base url for external css fonts. If set to ``None``, no external css fonts -##will be loaded. -#UI_FONTS_BASE_URL = "//fonts.googleapis.com/" -# ## A list of os family names which don't support ssh public key injection #UI_SSH_SUPPORT_OSFAMILY_EXCLUDE_LIST = ['windows'] diff --git a/snf-cyclades-app/setup.py b/snf-cyclades-app/setup.py index 36d4edeb01765309b9c89e6079e520267eae1ea5..c0abff28c1c3212b8b0cbddffca3d5267ee705c6 100644 --- a/snf-cyclades-app/setup.py +++ b/snf-cyclades-app/setup.py @@ -1,35 +1,17 @@ -# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # import distribute_setup @@ -60,7 +42,6 @@ CLASSIFIERS = [] INSTALL_REQUIRES = [ 'Django>=1.4, <1.5', 'simplejson>=2.1.1', - 'pycurl>=7.19.0', 'python-dateutil>=1.4.1', 'IPy>=0.70', 'South>=0.7.3', @@ -68,7 +49,7 @@ INSTALL_REQUIRES = [ 'puka', 'python-daemon>=1.5.5, <1.6', 'snf-common', - 'vncauthproxy>1.4', + 'vncauthproxy>1.5', 'snf-pithos-backend', 'lockfile>=0.8, <0.9', 'ipaddr', @@ -187,7 +168,7 @@ def find_package_data( setup( name = 'snf-cyclades-app', version = VERSION, - license = 'BSD', + license = 'GNU GPLv3', url = 'http://www.synnefo.org/', description = SHORT_DESCRIPTION, classifiers = CLASSIFIERS, diff --git a/snf-cyclades-app/synnefo/__init__.py b/snf-cyclades-app/synnefo/__init__.py index c78fbe672324992f6c941a7828e4827928b270c9..5cda842903d0bfec117db1c3c5a43281d46eb551 100644 --- a/snf-cyclades-app/synnefo/__init__.py +++ b/snf-cyclades-app/synnefo/__init__.py @@ -1,35 +1,17 @@ -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # this is a namespace package try: diff --git a/snf-cyclades-app/synnefo/admin/stats.py b/snf-cyclades-app/synnefo/admin/stats.py index f80217bbcba6081be14720bd9667c69a55e0df41..e62d6865ac3bf8781b13589c3e6b96f6d4938c4e 100644 --- a/snf-cyclades-app/synnefo/admin/stats.py +++ b/snf-cyclades-app/synnefo/admin/stats.py @@ -1,35 +1,17 @@ -# Copyright 2013-2014 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import datetime @@ -37,12 +19,13 @@ import datetime from collections import defaultdict # , OrderedDict from copy import copy from django.conf import settings +from django.db import connection from django.db.models import Count, Sum from snf_django.lib.astakos import UserCache -from synnefo.db.models import (VirtualMachine, Network, Backend, +from synnefo.plankton.backend import PlanktonBackend +from synnefo.db.models import (VirtualMachine, Network, Backend, VolumeType, pooled_rapi_client, Flavor) -from synnefo.plankton.utils import image_backend def get_cyclades_stats(backend=None, clusters=True, servers=True, @@ -119,12 +102,12 @@ def _get_total_servers(backend=None): def get_server_stats(backend=None): - servers = VirtualMachine.objects.select_related("flavor")\ + servers = VirtualMachine.objects.select_related("flavor__volume_type")\ .filter(deleted=False) if backend is not None: servers = servers.filter(backend=backend) - disk_templates = Flavor.objects.values_list("disk_template", flat=True)\ - .distinct() + disk_templates = \ + VolumeType.objects.values_list("disk_template", flat=True).distinct() # Initialize stats server_stats = defaultdict(dict) @@ -144,7 +127,7 @@ def get_server_stats(backend=None): state = "stopped" flavor = s.flavor - disk_template = flavor.disk_template + disk_template = flavor.volume_type.disk_template server_stats[state]["count"] += 1 server_stats[state]["cpu"][flavor.cpu] += 1 server_stats[state]["ram"][flavor.ram << 20] += 1 @@ -188,19 +171,25 @@ def get_ip_pool_stats(): return ip_stats -def get_image_stats(backend=None): - total_servers = _get_total_servers(backend=backend) - active_servers = total_servers.filter(deleted=False) +IMAGES_QUERY = """ +SELECT is_system, osfamily, os, count(vm.id) +FROM db_virtualmachine as vm LEFT OUTER JOIN db_image as img +ON img.uuid = vm.imageid AND img.version = vm.image_version +WHERE vm.deleted=false +GROUP BY is_system, osfamily, os +""" - active_servers_images = active_servers.values("imageid", "userid")\ - .annotate(number=Count("imageid")) - - image_cache = ImageCache() - image_stats = defaultdict(int) - for result in active_servers_images: - imageid = image_cache.get_image(result["imageid"], result["userid"]) - image_stats[imageid] += result["number"] - return dict(image_stats) +def get_image_stats(backend=None): + cursor = connection.cursor() + cursor.execute(IMAGES_QUERY) + images = cursor.fetchall() + images_stats = {} + for image in images: + owner = "system" if image[0] else "user" + osfamily = image[1] or "unknown" + os = image[2] or "unknown" + images_stats["%s:%s:%s" % (owner, osfamily, os)] = image[3] + return images_stats class ImageCache(object): @@ -214,7 +203,7 @@ class ImageCache(object): def get_image(self, imageid, userid): if imageid not in self.images: try: - with image_backend(userid) as ib: + with PlanktonBackend(userid) as ib: image = ib.get_image(imageid) properties = image.get("properties") os = properties.get("os", diff --git a/snf-cyclades-app/synnefo/admin/urls.py b/snf-cyclades-app/synnefo/admin/urls.py index 2dee81402bef93db2288dfdfacb59cd269e7b3d6..33170f1ea56d9e3f6082aa0ec01bbf21975d0ecf 100644 --- a/snf-cyclades-app/synnefo/admin/urls.py +++ b/snf-cyclades-app/synnefo/admin/urls.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.conf.urls import url, patterns diff --git a/snf-cyclades-app/synnefo/admin/views.py b/snf-cyclades-app/synnefo/admin/views.py index 892c6021385deaff37ab1e8b07f81d34bbd8f97b..3692e758b8240b43f0c741fc76e56580de1ccb94 100644 --- a/snf-cyclades-app/synnefo/admin/views.py +++ b/snf-cyclades-app/synnefo/admin/views.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import logging from django import http diff --git a/snf-cyclades-app/synnefo/api/compute_urls.py b/snf-cyclades-app/synnefo/api/compute_urls.py index 503106834b5c9a7d60ff9315f127c6a9d0d1e6fb..d374aaf37d3808208620c06202b1ee42de738d2d 100644 --- a/snf-cyclades-app/synnefo/api/compute_urls.py +++ b/snf-cyclades-app/synnefo/api/compute_urls.py @@ -1,35 +1,17 @@ -# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.conf.urls import include, patterns diff --git a/snf-cyclades-app/synnefo/api/extensions.py b/snf-cyclades-app/synnefo/api/extensions.py index 5959bf5ceea3bc5c7c92b917fbe460a513a46d19..c224a0d97bd74c22653abf0343a3a20ec67aa024 100644 --- a/snf-cyclades-app/synnefo/api/extensions.py +++ b/snf-cyclades-app/synnefo/api/extensions.py @@ -1,35 +1,17 @@ -# Copyright 2011-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.conf.urls import patterns diff --git a/snf-cyclades-app/synnefo/api/faults.py b/snf-cyclades-app/synnefo/api/faults.py index 30b953da49183c7e3fbf9bd65409bf9fdae59166..c026c882f0adefb33ea8f26ac71c6e393d90a2a3 100644 --- a/snf-cyclades-app/synnefo/api/faults.py +++ b/snf-cyclades-app/synnefo/api/faults.py @@ -1,35 +1,17 @@ -# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. def camelCase(s): diff --git a/snf-cyclades-app/synnefo/api/flavors.py b/snf-cyclades-app/synnefo/api/flavors.py index 74192de009c88994e3cd327ace975ea3c99d4425..fe3f86e35dfb83dbf87e35cae01de3f61cae87bc 100644 --- a/snf-cyclades-app/synnefo/api/flavors.py +++ b/snf-cyclades-app/synnefo/api/flavors.py @@ -1,35 +1,17 @@ -# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from logging import getLogger from django.conf.urls import patterns @@ -61,7 +43,8 @@ def flavor_to_dict(flavor, detail=True): d['ram'] = flavor.ram d['disk'] = flavor.disk d['vcpus'] = flavor.cpu - d['SNF:disk_template'] = flavor.disk_template + d['SNF:disk_template'] = flavor.volume_type.disk_template + d['SNF:volume_type'] = flavor.volume_type_id d['SNF:allow_create'] = flavor.allow_create return d @@ -76,7 +59,8 @@ def list_flavors(request, detail=False): # overLimit (413) log.debug('list_flavors detail=%s', detail) - active_flavors = Flavor.objects.exclude(deleted=True) + active_flavors = Flavor.objects.select_related("volume_type")\ + .exclude(deleted=True) flavors = [flavor_to_dict(flavor, detail) for flavor in active_flavors.order_by('id')] diff --git a/snf-cyclades-app/synnefo/api/floating_ips.py b/snf-cyclades-app/synnefo/api/floating_ips.py index b758a29d4a91f16b57675dcec6098619e44fa8e9..b80b15f825144206cd8cee3efe686bd680d2c61f 100644 --- a/snf-cyclades-app/synnefo/api/floating_ips.py +++ b/snf-cyclades-app/synnefo/api/floating_ips.py @@ -1,38 +1,20 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.conf.urls.defaults import patterns -from django.db import transaction +from synnefo.db import transaction from django.http import HttpResponse from django.utils import simplejson as json @@ -62,7 +44,9 @@ ips_urlpatterns = patterns( 'synnefo.api.floating_ips', (r'^(?:/|.json|.xml)?$', 'demux'), (r'^/detail(?:.json|.xml)?$', 'list_floating_ips', {'detail': True}), - (r'^/(\w+)(?:/|.json|.xml)?$', 'floating_ip_demux')) + (r'^/(\w+)(?:/|.json|.xml)?$', 'floating_ip_demux'), + (r'^/(\w+)/action(?:.json|.xml)?$', 'floating_ip_action_demux'), +) def demux(request): @@ -87,6 +71,29 @@ def floating_ip_demux(request, floating_ip_id): allowed_methods=['GET', 'DELETE']) +@api.api_method(http_method='POST', user_required=True, logger=log, + serializations=["json"]) +def floating_ip_action_demux(request, floating_ip_id): + userid = request.user_uniq + req = utils.get_json_body(request) + log.debug('floating_ip_action %s %s', floating_ip_id, req) + if len(req) != 1: + raise faults.BadRequest('Malformed request.') + floating_ip = util.get_floating_ip_by_id(userid, + floating_ip_id, + for_update=True) + action = req.keys()[0] + try: + f = FLOATING_IP_ACTIONS[action] + except KeyError: + raise faults.BadRequest("Action %s not supported." % action) + action_args = req[action] + if not isinstance(action_args, dict): + raise faults.BadRequest("Invalid argument.") + + return f(request, floating_ip, action_args) + + def ip_to_dict(floating_ip): machine_id = None port_id = None @@ -99,9 +106,9 @@ def ip_to_dict(floating_ip): "floating_ip_address": floating_ip.address, "port_id": str(port_id) if port_id else None, "floating_network_id": str(floating_ip.network_id), - "deleted": floating_ip.deleted, - "tenant_id": floating_ip.userid, - "user_id": floating_ip.userid} + "user_id": floating_ip.userid, + "tenant_id": floating_ip.project, + "deleted": floating_ip.deleted} @api.api_method(http_method="GET", user_required=True, logger=log, @@ -140,10 +147,11 @@ def get_floating_ip(request, floating_ip_id): @transaction.commit_on_success def allocate_floating_ip(request): """Allocate a floating IP.""" - req = utils.get_request_dict(request) + req = utils.get_json_body(request) floating_ip_dict = api.utils.get_attribute(req, "floatingip", required=True, attr_type=dict) userid = request.user_uniq + project = floating_ip_dict.get("project", None) log.info('allocate_floating_ip user: %s request: %s', userid, req) # the network_pool is a mandatory field @@ -152,7 +160,7 @@ def allocate_floating_ip(request): required=False, attr_type=(basestring, int)) if network_id is None: - floating_ip = ips.create_floating_ip(userid) + floating_ip = ips.create_floating_ip(userid, project=project) else: try: network_id = int(network_id) @@ -165,7 +173,8 @@ def allocate_floating_ip(request): "floating_ip_address", required=False, attr_type=basestring) - floating_ip = ips.create_floating_ip(userid, network, address) + floating_ip = ips.create_floating_ip(userid, network, address, + project=project) log.info("User '%s' allocated floating IP '%s'", userid, floating_ip) request.serialization = "json" @@ -198,7 +207,7 @@ def update_floating_ip(request, floating_ip_id): #userid = request.user_uniq #log.info("update_floating_ip '%s'. User '%s'.", floating_ip_id, userid) - #req = utils.get_request_dict(request) + #req = utils.get_json_body(request) #info = api.utils.get_attribute(req, "floatingip", required=True) #device_id = api.utils.get_attribute(info, "device_id", required=False) @@ -235,6 +244,20 @@ def list_floating_ip_pools(request): return HttpResponse(data, status=200) +@transaction.commit_on_success +def reassign(request, floating_ip, args): + project = args.get("project") + if project is None: + raise faults.BadRequest("Missing 'project' attribute.") + ips.reassign_floating_ip(floating_ip, project) + return HttpResponse(status=200) + + +FLOATING_IP_ACTIONS = { + "reassign": reassign, +} + + def network_to_floating_ip_pool(network): """Convert a 'Network' object to a floating IP pool dict.""" total, free = network.ip_count() diff --git a/snf-cyclades-app/synnefo/api/images.py b/snf-cyclades-app/synnefo/api/images.py index a1e4986dc006c7f79f1f5d6fd0d51bce183b1016..108166a699b859912222ef311fdfd82b0be51776 100644 --- a/snf-cyclades-app/synnefo/api/images.py +++ b/snf-cyclades-app/synnefo/api/images.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from logging import getLogger from itertools import ifilter @@ -44,7 +26,7 @@ from django.utils import simplejson as json from snf_django.lib import api from snf_django.lib.api import faults, utils from synnefo.api import util -from synnefo.plankton.utils import image_backend +from synnefo.plankton import backend log = getLogger(__name__) @@ -103,20 +85,31 @@ def metadata_item_demux(request, image_id, key): 'DELETE']) +API_STATUS_FROM_IMAGE_STATUS = { + backend.OBJECT_UNAVAILABLE: "SAVING", + backend.OBJECT_AVAILABLE: "ACTIVE", + backend.OBJECT_ERROR: "ERROR", + "DELETED": "DELETED"} # Unused status + + def image_to_dict(image, detail=True): d = dict(id=image['id'], name=image['name']) if detail: d['updated'] = utils.isoformat(date_parse(image['updated_at'])) d['created'] = utils.isoformat(date_parse(image['created_at'])) - d['status'] = 'DELETED' if image['deleted_at'] else 'ACTIVE' - d['progress'] = 100 if image['status'] == 'available' else 0 + img_status = image.get("status", "").upper() + status = API_STATUS_FROM_IMAGE_STATUS.get(img_status, "UNKNOWN") + d['status'] = status + d['progress'] = 100 if status == 'ACTIVE' else 0 d['user_id'] = image['owner'] d['tenant_id'] = image['owner'] + d['public'] = image["is_public"] d['links'] = util.image_to_links(image["id"]) if image["properties"]: d['metadata'] = image['properties'] else: d['metadata'] = {} + d["is_snapshot"] = image["is_snapshot"] return d @@ -131,8 +124,8 @@ def list_images(request, detail=False): log.debug('list_images detail=%s', detail) since = utils.isoparse(request.GET.get('changes-since')) - with image_backend(request.user_uniq) as backend: - images = backend.list_images() + with backend.PlanktonBackend(request.user_uniq) as b: + images = b.list_images() if since: updated_since = lambda img: date_parse(img["updated_at"]) >= since images = ifilter(updated_since, images) @@ -180,8 +173,8 @@ def get_image_details(request, image_id): # overLimit (413) log.debug('get_image_details %s', image_id) - with image_backend(request.user_uniq) as backend: - image = backend.get_image(image_id) + with backend.PlanktonBackend(request.user_uniq) as b: + image = b.get_image(image_id) reply = image_to_dict(image) if request.serialization == 'xml': @@ -202,8 +195,8 @@ def delete_image(request, image_id): # overLimit (413) log.info('delete_image %s', image_id) - with image_backend(request.user_uniq) as backend: - backend.unregister(image_id) + with backend.PlanktonBackend(request.user_uniq) as b: + b.unregister(image_id) log.info('User %s deleted image %s', request.user_uniq, image_id) return HttpResponse(status=204) @@ -218,8 +211,8 @@ def list_metadata(request, image_id): # overLimit (413) log.debug('list_image_metadata %s', image_id) - with image_backend(request.user_uniq) as backend: - image = backend.get_image(image_id) + with backend.PlanktonBackend(request.user_uniq) as b: + image = b.get_image(image_id) metadata = image['properties'] return util.render_metadata(request, metadata, use_values=False, status=200) @@ -236,10 +229,10 @@ def update_metadata(request, image_id): # badMediaType(415), # overLimit (413) - req = utils.get_request_dict(request) + req = utils.get_json_body(request) log.info('update_image_metadata %s %s', image_id, req) - with image_backend(request.user_uniq) as backend: - image = backend.get_image(image_id) + with backend.PlanktonBackend(request.user_uniq) as b: + image = b.get_image(image_id) try: metadata = req['metadata'] assert isinstance(metadata, dict) @@ -249,7 +242,7 @@ def update_metadata(request, image_id): properties = image['properties'] properties.update(metadata) - backend.update_metadata(image_id, dict(properties=properties)) + b.update_metadata(image_id, dict(properties=properties)) return util.render_metadata(request, properties, status=201) @@ -265,8 +258,8 @@ def get_metadata_item(request, image_id, key): # overLimit (413) log.debug('get_image_metadata_item %s %s', image_id, key) - with image_backend(request.user_uniq) as backend: - image = backend.get_image(image_id) + with backend.PlanktonBackend(request.user_uniq) as b: + image = b.get_image(image_id) val = image['properties'].get(key) if val is None: raise faults.ItemNotFound('Metadata key not found.') @@ -285,7 +278,7 @@ def create_metadata_item(request, image_id, key): # badMediaType(415), # overLimit (413) - req = utils.get_request_dict(request) + req = utils.get_json_body(request) log.info('create_image_metadata_item %s %s %s', image_id, key, req) try: metadict = req['meta'] @@ -296,12 +289,12 @@ def create_metadata_item(request, image_id, key): raise faults.BadRequest('Malformed request.') val = metadict[key] - with image_backend(request.user_uniq) as backend: - image = backend.get_image(image_id) + with backend.PlanktonBackend(request.user_uniq) as b: + image = b.get_image(image_id) properties = image['properties'] properties[key] = val - backend.update_metadata(image_id, dict(properties=properties)) + b.update_metadata(image_id, dict(properties=properties)) return util.render_meta(request, {key: val}, status=201) @@ -319,11 +312,11 @@ def delete_metadata_item(request, image_id, key): # overLimit (413), log.info('delete_image_metadata_item %s %s', image_id, key) - with image_backend(request.user_uniq) as backend: - image = backend.get_image(image_id) + with backend.PlanktonBackend(request.user_uniq) as b: + image = b.get_image(image_id) properties = image['properties'] properties.pop(key, None) - backend.update_metadata(image_id, dict(properties=properties)) + b.update_metadata(image_id, dict(properties=properties)) return HttpResponse(status=204) diff --git a/snf-cyclades-app/synnefo/api/network_urls.py b/snf-cyclades-app/synnefo/api/network_urls.py index 21e9d2e214f6f12465af02a6951c0a15044562a7..9c60e6a70ff9edd511986f2c29d75d287567e1a5 100644 --- a/snf-cyclades-app/synnefo/api/network_urls.py +++ b/snf-cyclades-app/synnefo/api/network_urls.py @@ -1,35 +1,17 @@ -# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.conf.urls import include, patterns diff --git a/snf-cyclades-app/synnefo/api/networks.py b/snf-cyclades-app/synnefo/api/networks.py index 400b1e6850b5311c96ea8ac12c3fc5ce0ea9f5d6..552c317cfb7d550964d70b19095ee4996b81f3ee 100644 --- a/snf-cyclades-app/synnefo/api/networks.py +++ b/snf-cyclades-app/synnefo/api/networks.py @@ -1,45 +1,28 @@ -# Copyright 2011-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.conf import settings from django.conf.urls import patterns from django.http import HttpResponse from django.utils import simplejson as json -from django.db import transaction +from synnefo.db import transaction from django.db.models import Q from django.template.loader import render_to_string from snf_django.lib import api +from snf_django.lib.api import utils from synnefo.api import util from synnefo.db.models import Network @@ -53,7 +36,9 @@ urlpatterns = patterns( 'synnefo.api.networks', (r'^(?:/|.json|.xml)?$', 'demux'), (r'^/detail(?:.json|.xml)?$', 'list_networks', {'detail': True}), - (r'^/(\w+)(?:/|.json|.xml)?$', 'network_demux')) + (r'^/(\w+)(?:/|.json|.xml)?$', 'network_demux'), + (r'^/(\w+)/action(?:/|.json|.xml)?$', 'network_action_demux'), +) def demux(request): @@ -81,6 +66,23 @@ def network_demux(request, network_id): 'DELETE']) +@api.api_method(http_method='POST', user_required=True, logger=log) +def network_action_demux(request, network_id): + req = utils.get_json_body(request) + network = util.get_network(network_id, request.user_uniq, for_update=True, + non_deleted=True) + action = req.keys()[0] + try: + f = NETWORK_ACTIONS[action] + except KeyError: + raise api.faults.BadRequest("Action %s not supported." % action) + action_args = req[action] + if not isinstance(action_args, dict): + raise api.faults.BadRequest("Invalid argument.") + + return f(request, network, action_args) + + @api.api_method(http_method='GET', user_required=True, logger=log) def list_networks(request, detail=True): log.debug('list_networks detail=%s', detail) @@ -88,8 +90,6 @@ def list_networks(request, detail=True): user_networks = Network.objects.filter(Q(userid=request.user_uniq) | Q(public=True))\ .order_by('id') - if detail: - user_networks = user_networks.prefetch_related("subnets") user_networks = api.utils.filter_modified_since(request, objects=user_networks) @@ -109,7 +109,7 @@ def list_networks(request, detail=True): @api.api_method(http_method='POST', user_required=True, logger=log) def create_network(request): userid = request.user_uniq - req = api.utils.get_request_dict(request) + req = api.utils.get_json_body(request) log.info('create_network user: %s request: %s', userid, req) network_dict = api.utils.get_attribute(req, "network", @@ -128,8 +128,9 @@ def create_network(request): if name is None: name = "" + project = network_dict.get('project', None) network = networks.create(userid=userid, name=name, flavor=flavor, - public=False) + public=False, project=project) networkdict = network_to_dict(network, detail=True) response = render_network(request, networkdict, status=201) @@ -145,13 +146,14 @@ def get_network_details(request, network_id): @api.api_method(http_method='PUT', user_required=True, logger=log) def update_network(request, network_id): - info = api.utils.get_request_dict(request) + info = api.utils.get_json_body(request) network = api.utils.get_attribute(info, "network", attr_type=dict, required=True) new_name = api.utils.get_attribute(network, "name", attr_type=basestring) - network = util.get_network(network_id, request.user_uniq, for_update=True) + network = util.get_network(network_id, request.user_uniq, for_update=True, + non_deleted=True) if network.public: raise api.faults.Forbidden("Cannot rename the public network.") network = networks.rename(network, new_name) @@ -162,7 +164,8 @@ def update_network(request, network_id): @transaction.commit_on_success def delete_network(request, network_id): log.info('delete_network %s', network_id) - network = util.get_network(network_id, request.user_uniq, for_update=True) + network = util.get_network(network_id, request.user_uniq, for_update=True, + non_deleted=True) if network.public: raise api.faults.Forbidden("Cannot delete the public network.") networks.delete(network) @@ -173,15 +176,9 @@ def network_to_dict(network, detail=True): d = {'id': str(network.id), 'name': network.name} d['links'] = util.network_to_links(network.id) if detail: - # Loop over subnets. Do not perform any extra query because of prefetch - # related! - subnet_ids = [] - for subnet in network.subnets.all(): - subnet_ids.append(subnet.id) - state = "SNF:DRAINED" if network.drained else network.state d['user_id'] = network.userid - d['tenant_id'] = network.userid + d['tenant_id'] = network.project d['type'] = network.flavor d['updated'] = api.utils.isoformat(network.updated) d['created'] = api.utils.isoformat(network.created) @@ -190,12 +187,26 @@ def network_to_dict(network, detail=True): d['shared'] = network.public d['router:external'] = network.external_router d['admin_state_up'] = True - d['subnets'] = subnet_ids + d['subnets'] = network.subnet_ids d['SNF:floating_ip_pool'] = network.floating_ip_pool d['deleted'] = network.deleted return d +@transaction.commit_on_success +def reassign_network(request, network, args): + project = args.get("project") + if project is None: + raise api.faults.BadRequest("Missing 'project' attribute.") + networks.reassign(network, project) + return HttpResponse(status=200) + + +NETWORK_ACTIONS = { + "reassign": reassign_network, +} + + def render_network(request, networkdict, status=200): if request.serialization == 'xml': data = render_to_string('network.xml', {'network': networkdict}) diff --git a/snf-cyclades-app/synnefo/api/ports.py b/snf-cyclades-app/synnefo/api/ports.py index b99c688ff264d5126fa3a496c28d3acb2905ccc7..7beea1987a7af0cd4269cdf7cb0a594e4d4ffe82 100644 --- a/snf-cyclades-app/synnefo/api/ports.py +++ b/snf-cyclades-app/synnefo/api/ports.py @@ -1,42 +1,24 @@ -# Copyright 2011-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. #from django.conf import settings import ipaddr from django.conf.urls import patterns from django.http import HttpResponse from django.utils import simplejson as json -from django.db import transaction +from synnefo.db import transaction from django.template.loader import render_to_string from snf_django.lib import api @@ -107,7 +89,7 @@ def list_ports(request, detail=True): @transaction.commit_on_success def create_port(request): user_id = request.user_uniq - req = api.utils.get_request_dict(request) + req = api.utils.get_json_body(request) log.info('create_port user: %s request: %s', user_id, req) port_dict = api.utils.get_attribute(req, "port", attr_type=dict) @@ -203,7 +185,7 @@ def update_port(request, port_id): You can update only name, security_groups ''' port = util.get_port(port_id, request.user_uniq, for_update=True) - req = api.utils.get_request_dict(request) + req = api.utils.get_json_body(request) port_info = api.utils.get_attribute(req, "port", required=True, attr_type=dict) @@ -249,6 +231,10 @@ def delete_port(request, port_id): deleted=False).exists(): raise faults.Forbidden("Cannot disconnect from public network.") + vm = port.machine + if vm is not None and vm.suspended: + raise faults.Forbidden("Administratively Suspended VM.") + servers.delete_port(port) return HttpResponse(status=204) diff --git a/snf-cyclades-app/synnefo/api/servers.py b/snf-cyclades-app/synnefo/api/servers.py index 368bf405225fb91bb7b6a0889ff3b55f021e8151..cf4ba9987ccc0d2d4e85e431d68551541b496c20 100644 --- a/snf-cyclades-app/synnefo/api/servers.py +++ b/snf-cyclades-app/synnefo/api/servers.py @@ -1,50 +1,34 @@ -# Copyright 2011-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.conf import settings from django.conf.urls import patterns -from django.db import transaction +from synnefo.db import transaction from django.http import HttpResponse from django.template.loader import render_to_string from django.utils import simplejson as json +from django.core.urlresolvers import reverse from snf_django.lib import api from snf_django.lib.api import faults, utils from synnefo.api import util from synnefo.db.models import (VirtualMachine, VirtualMachineMetadata) -from synnefo.logic import servers, utils as logic_utils +from synnefo.logic import servers, utils as logic_utils, server_attachments +from synnefo.volume.util import get_volume from logging import getLogger log = getLogger(__name__) @@ -61,8 +45,21 @@ urlpatterns = patterns( (r'^/(\d+)/metadata/(.+?)(?:.json|.xml)?$', 'metadata_item_demux'), (r'^/(\d+)/stats(?:.json|.xml)?$', 'server_stats'), (r'^/(\d+)/diagnostics(?:.json)?$', 'get_server_diagnostics'), + (r'^/(\d+)/os-volume_attachments(?:.json)?$', 'demux_volumes'), + (r'^/(\d+)/os-volume_attachments/(\d+)(?:.json)?$', 'demux_volumes_item'), ) +VOLUME_SOURCE_TYPES = [ + "image", + "volume", + "blank" +] + +if settings.CYCLADES_SNAPSHOTS_ENABLED: + # If snapshots are enabled, add 'snapshot' to the list of allowed sources + # for a new block device. + VOLUME_SOURCE_TYPES.append("snapshot") + def demux(request): if request.method == 'GET': @@ -112,6 +109,26 @@ def metadata_item_demux(request, server_id, key): 'DELETE']) +def demux_volumes(request, server_id): + if request.method == 'GET': + return get_volumes(request, server_id) + elif request.method == 'POST': + return attach_volume(request, server_id) + else: + return api.api_method_not_allowed(request, + allowed_methods=['GET', 'POST']) + + +def demux_volumes_item(request, server_id, volume_id): + if request.method == 'GET': + return get_volume_info(request, server_id, volume_id) + elif request.method == 'DELETE': + return detach_volume(request, server_id, volume_id) + else: + return api.api_method_not_allowed(request, + allowed_methods=['GET', 'DELETE']) + + def nic_to_attachments(nic): """Convert a NIC object to 'attachments attribute. @@ -171,7 +188,7 @@ def vm_to_dict(vm, detail=False): d['links'] = util.vm_to_links(vm.id) if detail: d['user_id'] = vm.userid - d['tenant_id'] = vm.userid + d['tenant_id'] = vm.project d['status'] = logic_utils.get_rsapi_state(vm) d['SNF:task_state'] = logic_utils.get_task_state(vm) d['progress'] = 100 if d['status'] == 'ACTIVE' else vm.buildpercentage @@ -194,6 +211,8 @@ def vm_to_dict(vm, detail=False): d['attachments'] = attachments d['addresses'] = attachments_to_addresses(attachments) + d['volumes'] = [v.id for v in vm.volumes.filter(deleted=False).order_by('id')] + # include the latest vm diagnostic, if set diagnostic = vm.get_last_diagnostic() if diagnostic: @@ -221,7 +240,7 @@ def get_server_public_ip(vm_nics, version=4): """ for nic in vm_nics: for ip in nic.ips.all(): - if ip.ipversion == version and ip.public: + if nic.public and ip.ipversion == version: return ip return None @@ -369,7 +388,7 @@ def create_server(request): # badRequest (400), # serverCapacityUnavailable (503), # overLimit (413) - req = utils.get_request_dict(request) + req = utils.get_json_body(request) user_id = request.user_uniq log.info('create_server user: %s request: %s', user_id, req) @@ -385,13 +404,17 @@ def create_server(request): networks = server.get("networks") if networks is not None: assert isinstance(networks, list) + project = server.get("project") except (KeyError, AssertionError): raise faults.BadRequest("Malformed request") + volumes = None + dev_map = server.get("block_device_mapping_v2") + if dev_map is not None: + volumes = parse_block_device_mapping(dev_map) + # Verify that personalities are well-formed util.verify_personality(personality) - # Get image information - image = util.get_image_dict(image_id, user_id) # Get flavor (ensure it is active) flavor = util.get_flavor(flavor_id, include_deleted=False) if not flavor.allow_create: @@ -401,9 +424,9 @@ def create_server(request): # Generate password password = util.random_password() - vm = servers.create(user_id, name, password, flavor, image, + vm = servers.create(user_id, name, password, flavor, image_id, metadata=metadata, personality=personality, - networks=networks) + project=project, networks=networks, volumes=volumes) server = vm_to_dict(vm, detail=True) server['status'] = 'BUILD' @@ -414,6 +437,65 @@ def create_server(request): return response +def parse_block_device_mapping(dev_map): + """Parse 'block_device_mapping_v2' attribute""" + if not isinstance(dev_map, list): + raise faults.BadRequest("Block Device Mapping is Invalid") + return [_parse_block_device(device) for device in dev_map] + + +def _parse_block_device(device): + """Parse and validate a block device mapping""" + if not isinstance(device, dict): + raise faults.BadRequest("Block Device Mapping is Invalid") + + # Validate source type + source_type = device.get("source_type") + if source_type is None: + raise faults.BadRequest("Block Device Mapping is Invalid: Invalid" + " source_type field") + elif source_type not in VOLUME_SOURCE_TYPES: + raise faults.BadRequest("Block Device Mapping is Invalid: source_type" + " must be on of %s" + % ", ".join(VOLUME_SOURCE_TYPES)) + + # Validate source UUID + uuid = device.get("uuid") + if uuid is None and source_type != "blank": + raise faults.BadRequest("Block Device Mapping is Invalid: uuid of" + " %s is missing" % source_type) + + # Validate volume size + size = device.get("volume_size") + if size is not None: + try: + size = int(size) + except (TypeError, ValueError): + raise faults.BadRequest("Block Device Mapping is Invalid: Invalid" + " size field") + + # Validate 'delete_on_termination' + delete_on_termination = device.get("delete_on_termination") + if delete_on_termination is not None: + if not isinstance(delete_on_termination, bool): + raise faults.BadRequest("Block Device Mapping is Invalid: Invalid" + " delete_on_termination field") + else: + if source_type == "volume": + delete_on_termination = False + else: + delete_on_termination = True + + # Unused API Attributes + # boot_index = device.get("boot_index") + # destination_type = device.get("destination_type") + + return {"source_type": source_type, + "source_uuid": uuid, + "size": size, + "delete_on_termination": delete_on_termination} + + @api.api_method(http_method='GET', user_required=True, logger=log) def get_server_details(request, server_id): # Normal Response Codes: 200, 203 @@ -444,7 +526,7 @@ def update_server_name(request, server_id): # buildInProgress (409), # overLimit (413) - req = utils.get_request_dict(request) + req = utils.get_json_body(request) log.info('update_server_name %s %s', server_id, req) req = utils.get_attribute(req, "server", attr_type=dict, required=True) @@ -452,7 +534,7 @@ def update_server_name(request, server_id): required=True) vm = util.get_vm(server_id, request.user_uniq, for_update=True, - non_suspended=True) + non_suspended=True, non_deleted=True) servers.rename(vm, new_name=name) @@ -472,13 +554,15 @@ def delete_server(request, server_id): log.info('delete_server %s', server_id) vm = util.get_vm(server_id, request.user_uniq, for_update=True, - non_suspended=True) + non_suspended=True, non_deleted=True) vm = servers.destroy(vm) return HttpResponse(status=204) # additional server actions -ARBITRARY_ACTIONS = ['console', 'firewallProfile'] +ARBITRARY_ACTIONS = ['console', 'firewallProfile', 'reassign', + 'os-getVNCConsole', 'os-getRDPConsole', + 'os-getSPICEConsole'] def key_to_action(key): @@ -496,7 +580,7 @@ def key_to_action(key): @api.api_method(http_method='POST', user_required=True, logger=log) @transaction.commit_on_success def demux_server_action(request, server_id): - req = utils.get_request_dict(request) + req = utils.get_json_body(request) log.debug('server_action %s %s', server_id, req) if not isinstance(req, dict) and len(req) != 1: @@ -506,7 +590,11 @@ def demux_server_action(request, server_id): vm = util.get_vm(server_id, request.user_uniq, for_update=True, non_deleted=True, non_suspended=True) - action = req.keys()[0] + try: + action = req.keys()[0] + except IndexError: + raise faults.BadRequest("Malformed Request.") + if not isinstance(action, basestring): raise faults.BadRequest("Malformed Request. Invalid action.") @@ -595,13 +683,28 @@ def update_metadata(request, server_id): # badMediaType(415), # overLimit (413) - req = utils.get_request_dict(request) + req = utils.get_json_body(request) log.info('update_server_metadata %s %s', server_id, req) - vm = util.get_vm(server_id, request.user_uniq, non_suspended=True) + vm = util.get_vm(server_id, request.user_uniq, non_suspended=True, + non_deleted=True) metadata = utils.get_attribute(req, "metadata", required=True, attr_type=dict) + if len(metadata) + len(vm.metadata.all()) - \ + len(vm.metadata.all().filter(meta_key__in=metadata.keys())) > \ + settings.CYCLADES_VM_MAX_METADATA: + raise faults.BadRequest("Virtual Machines cannot have more than %s " + "metadata items" % + settings.CYCLADES_VM_MAX_METADATA) + for key, val in metadata.items(): + if len(key) > VirtualMachineMetadata.KEY_LENGTH: + raise faults.BadRequest("Malformed Request. Metadata key is too" + " long") + if len(val) > VirtualMachineMetadata.VALUE_LENGTH: + raise faults.BadRequest("Malformed Request. Metadata value is too" + " long") + if not isinstance(key, (basestring, int)) or\ not isinstance(val, (basestring, int)): raise faults.BadRequest("Malformed Request. Invalid metadata.") @@ -644,9 +747,10 @@ def create_metadata_item(request, server_id, key): # badMediaType(415), # overLimit (413) - req = utils.get_request_dict(request) + req = utils.get_json_body(request) log.info('create_server_metadata_item %s %s %s', server_id, key, req) - vm = util.get_vm(server_id, request.user_uniq, non_suspended=True) + vm = util.get_vm(server_id, request.user_uniq, non_suspended=True, + non_deleted=True) try: metadict = req['meta'] assert isinstance(metadict, dict) @@ -655,11 +759,27 @@ def create_metadata_item(request, server_id, key): except (KeyError, AssertionError): raise faults.BadRequest("Malformed request") + value = metadict[key] + + # Check key, value length + if len(key) > VirtualMachineMetadata.KEY_LENGTH: + raise faults.BadRequest("Malformed Request. Metadata key is too long") + if len(value) > VirtualMachineMetadata.VALUE_LENGTH: + raise faults.BadRequest("Malformed Request. Metadata value is too" + " long") + + # Check number of metadata items + if vm.metadata.exclude(meta_key=key).count() == \ + settings.CYCLADES_VM_MAX_METADATA: + raise faults.BadRequest("Virtual Machines cannot have more than %s" + " metadata items" % + settings.CYCLADES_VM_MAX_METADATA) + meta, created = VirtualMachineMetadata.objects.get_or_create( meta_key=key, vm=vm) - meta.meta_value = metadict[key] + meta.meta_value = value meta.save() vm.save() d = {meta.meta_key: meta.meta_value} @@ -680,7 +800,8 @@ def delete_metadata_item(request, server_id, key): # overLimit (413), log.info('delete_server_metadata_item %s %s', server_id, key) - vm = util.get_vm(server_id, request.user_uniq, non_suspended=True) + vm = util.get_vm(server_id, request.user_uniq, non_suspended=True, + non_deleted=True) meta = util.get_vm_meta(vm, key) meta.delete() vm.save() @@ -818,14 +939,103 @@ def resize(request, vm, args): # serverCapacityUnavailable (503), # overLimit (413), # resizeNotAllowed (403) - flavorRef = args.get("flavorRef") - if flavorRef is None: + flavor_id = args.get("flavorRef") + if flavor_id is None: raise faults.BadRequest("Missing 'flavorRef' attribute.") - flavor = util.get_flavor(flavor_id=flavorRef, include_deleted=False) + flavor = util.get_flavor(flavor_id=flavor_id, include_deleted=False) servers.resize(vm, flavor=flavor) return HttpResponse(status=202) +@server_action('os-getSPICEConsole') +def os_get_spice_console(request, vm, args): + # Normal Response Code: 200 + # Error Response Codes: computeFault (400, 500), + # serviceUnavailable (503), + # unauthorized (401), + # badRequest (400), + # badMediaType(415), + # itemNotFound (404), + # buildInProgress (409), + # overLimit (413) + + log.info('Get Spice console for VM %s: %s', vm, args) + + raise faults.NotImplemented('Spice console not implemented') + + +@server_action('os-getRDPConsole') +def os_get_rdp_console(request, vm, args): + # Normal Response Code: 200 + # Error Response Codes: computeFault (400, 500), + # serviceUnavailable (503), + # unauthorized (401), + # badRequest (400), + # badMediaType(415), + # itemNotFound (404), + # buildInProgress (409), + # overLimit (413) + + log.info('Get RDP console for VM %s: %s', vm, args) + + raise faults.NotImplemented('RDP console not implemented') + + +machines_console_url = None + + +@server_action('os-getVNCConsole') +def os_get_vnc_console(request, vm, args): + # Normal Response Code: 200 + # Error Response Codes: computeFault (400, 500), + # serviceUnavailable (503), + # unauthorized (401), + # badRequest (400), + # badMediaType(415), + # itemNotFound (404), + # buildInProgress (409), + # overLimit (413) + + log.info('Get osVNC console for VM %s: %s', vm, args) + + console_type = args.get('type') + if console_type is None: + raise faults.BadRequest("No console 'type' specified.") + + supported_types = {'novnc': 'vnc-wss', 'xvpvnc': 'vnc'} + if console_type not in supported_types: + raise faults.BadRequest('Supported types: %s' % + ', '.join(supported_types.keys())) + + console_info = servers.console(vm, supported_types[console_type]) + + global machines_console_url + if machines_console_url is None: + machines_console_url = reverse('synnefo.ui.views.machines_console') + + if console_type == 'novnc': + # Return the URL of the WebSocket noVNC client + url = settings.CYCLADES_BASE_URL + machines_console_url + url += '?host=%(host)s&port=%(port)s&password=%(password)s' + else: + # Return a URL to paste into a Java VNC client + # FIXME: VNC clients (and the TigerVNC Java applet) can't handle the + # password. + url = '%(host)s:%(port)s?password=%(password)s' + + resp = {'type': console_type, + 'url': url % console_info} + + if request.serialization == 'xml': + mimetype = 'application/xml' + data = render_to_string('os-console.xml', {'console': resp}) + else: + mimetype = 'application/json' + data = json.dumps({'console': resp}) + + return HttpResponse(data, mimetype=mimetype, status=200) + + @server_action('console') def get_console(request, vm, args): # Normal Response Code: 200 @@ -843,8 +1053,12 @@ def get_console(request, vm, args): console_type = args.get("type") if console_type is None: raise faults.BadRequest("No console 'type' specified.") - elif console_type != "vnc": - raise faults.BadRequest("Console 'type' can only be 'vnc'.") + + supported_types = ['vnc', 'vnc-ws', 'vnc-wss'] + if console_type not in supported_types: + raise faults.BadRequest('Supported types: %s' % + ', '.join(supported_types)) + console_info = servers.console(vm, console_type) if request.serialization == 'xml': @@ -877,6 +1091,15 @@ def revert_resize(request, vm, args): raise faults.NotImplemented('Resize not supported.') +@server_action('reassign') +def reassign(request, vm, args): + project = args.get("project") + if project is None: + raise faults.BadRequest("Missing 'project' attribute.") + servers.reassign(vm, project) + return HttpResponse(status=200) + + @network_action('add') @transaction.commit_on_success def add(request, net, args): @@ -893,7 +1116,8 @@ def add(request, net, args): if not server_id: raise faults.BadRequest('Malformed Request.') - vm = util.get_vm(server_id, request.user_uniq, non_suspended=True) + vm = util.get_vm(server_id, request.user_uniq, non_suspended=True, + for_update=True, non_deleted=True) servers.connect(vm, network=net) return HttpResponse(status=202) @@ -920,7 +1144,8 @@ def remove(request, net, args): nic = util.get_nic(nic_id=nic_id) server_id = nic.machine_id - vm = util.get_vm(server_id, request.user_uniq, non_suspended=True) + vm = util.get_vm(server_id, request.user_uniq, non_suspended=True, + for_update=True, non_deleted=True) servers.disconnect(vm, nic) @@ -953,3 +1178,69 @@ def remove_floating_ip(request, vm, args): % address) servers.delete_port(floating_ip.nic) return HttpResponse(status=202) + + +def volume_to_attachment(volume): + return {"id": volume.id, + "volumeId": volume.id, + "serverId": volume.machine_id, + "device": ""} # TODO: What device to return? + + +@api.api_method(http_method='GET', user_required=True, logger=log) +def get_volumes(request, server_id): + log.debug("get_volumes server_id %s", server_id) + vm = util.get_vm(server_id, request.user_uniq, for_update=False) + + # TODO: Filter attachments!! + volumes = vm.volumes.filter(deleted=False).order_by("id") + attachments = [volume_to_attachment(v) for v in volumes] + + data = json.dumps({'volumeAttachments': attachments}) + return HttpResponse(data, status=200) + pass + + +@api.api_method(http_method='GET', user_required=True, logger=log) +def get_volume_info(request, server_id, volume_id): + log.debug("get_volume_info server_id %s volume_id", server_id, volume_id) + user_id = request.user_uniq + vm = util.get_vm(server_id, user_id, for_update=False) + volume = get_volume(user_id, volume_id, for_update=False, non_deleted=True, + exception=faults.BadRequest) + servers._check_attachment(vm, volume) + attachment = volume_to_attachment(volume) + data = json.dumps({'volumeAttachment': attachment}) + return HttpResponse(data, status=200) + + +@api.api_method(http_method='POST', user_required=True, logger=log) +def attach_volume(request, server_id): + req = utils.get_json_body(request) + log.debug("attach_volume server_id %s request", server_id, req) + user_id = request.user_uniq + vm = util.get_vm(server_id, user_id, for_update=True, non_deleted=True) + + attachment_dict = api.utils.get_attribute(req, "volumeAttachment", + required=True) + # Get volume + volume_id = api.utils.get_attribute(attachment_dict, "volumeId") + volume = get_volume(user_id, volume_id, for_update=True, non_deleted=True, + exception=faults.BadRequest) + vm = server_attachments.attach_volume(vm, volume) + attachment = volume_to_attachment(volume) + data = json.dumps({'volumeAttachment': attachment}) + + return HttpResponse(data, status=202) + + +@api.api_method(http_method='DELETE', user_required=True, logger=log) +def detach_volume(request, server_id, volume_id): + log.debug("detach_volume server_id %s volume_id", server_id, volume_id) + user_id = request.user_uniq + vm = util.get_vm(server_id, user_id, for_update=True, non_deleted=True) + volume = get_volume(user_id, volume_id, for_update=True, non_deleted=True, + exception=faults.BadRequest) + vm = server_attachments.detach_volume(vm, volume) + # TODO: Check volume state, send job to detach volume + return HttpResponse(status=202) diff --git a/snf-cyclades-app/synnefo/api/services.py b/snf-cyclades-app/synnefo/api/services.py index c8fe4dc51e3e5fb90f6268e09b88a552bbe45781..524e2205caf37ce8b4e0a393cffe9e4e47eba5f0 100644 --- a/snf-cyclades-app/synnefo/api/services.py +++ b/snf-cyclades-app/synnefo/api/services.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # Required but undefined fields are given a value of None @@ -187,4 +169,16 @@ cyclades_services = { ], 'resources': {}, }, + + 'cyclades_volume': { + 'type': 'volume', + 'component': 'cyclades', + 'prefix': 'volume', + 'public': True, + 'endpoints': [ + {'versionId': 'v2.0', + 'publicURL': None}, + ], + 'resources': {}, + }, } diff --git a/snf-cyclades-app/synnefo/api/subnets.py b/snf-cyclades-app/synnefo/api/subnets.py index 0f2af7da107f06e655d5471d8b5bd18eacc8ee95..4a8de2a9c344ea2da672f7b58dc6ad7b0fff40e3 100644 --- a/snf-cyclades-app/synnefo/api/subnets.py +++ b/snf-cyclades-app/synnefo/api/subnets.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from logging import getLogger from snf_django.lib import api @@ -104,7 +86,7 @@ def create_subnet(request): network_id and the desired cidr are mandatory, everything else is optional """ - dictionary = utils.get_request_dict(request) + dictionary = utils.get_json_body(request) user_id = request.user_uniq log.info('create subnet user: %s request: %s', user_id, dictionary) @@ -191,7 +173,7 @@ def update_subnet(request, sub_id): """ - dictionary = utils.get_request_dict(request) + dictionary = utils.get_json_body(request) user_id = request.user_uniq try: diff --git a/snf-cyclades-app/synnefo/api/templates/os-console.xml b/snf-cyclades-app/synnefo/api/templates/os-console.xml new file mode 100644 index 0000000000000000000000000000000000000000..99eb83a44ff76f250777049a890755c3edff481f --- /dev/null +++ b/snf-cyclades-app/synnefo/api/templates/os-console.xml @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="UTF-8"?> +<console xmlns="http://docs.openstack.org/compute/api/v1.1" xmlns:atom="http://www.w3.org/2005/Atom" type="{{ console.type }}" url="{{ console.url}}"> +</console> diff --git a/snf-cyclades-app/synnefo/api/tests/__init__.py b/snf-cyclades-app/synnefo/api/tests/__init__.py index 3f00e5b993c6ce5959f0bbcdc74bf5b4b1355a51..a9f9e7cd35547a0e93c0ceec58ee09b0ea863cc5 100644 --- a/snf-cyclades-app/synnefo/api/tests/__init__.py +++ b/snf-cyclades-app/synnefo/api/tests/__init__.py @@ -1,35 +1,17 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # Import TestCases from .servers import * diff --git a/snf-cyclades-app/synnefo/api/tests/extensions.py b/snf-cyclades-app/synnefo/api/tests/extensions.py index 180ea57a79b6b34f2d1b981dfd3131c1227bd7a2..8d521450b2c08735a831db34ed88b5f83d8b1634 100644 --- a/snf-cyclades-app/synnefo/api/tests/extensions.py +++ b/snf-cyclades-app/synnefo/api/tests/extensions.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import json diff --git a/snf-cyclades-app/synnefo/api/tests/flavors.py b/snf-cyclades-app/synnefo/api/tests/flavors.py index 428144314f8d838868c276ce9ce82d347d73104a..3df33511af408660eff01236c78aad2cf9fa047f 100644 --- a/snf-cyclades-app/synnefo/api/tests/flavors.py +++ b/snf-cyclades-app/synnefo/api/tests/flavors.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import json @@ -86,7 +68,7 @@ class FlavorAPITest(BaseAPITest): self.assertEqual(api_flavor['name'], db_flavor.name) self.assertEqual(api_flavor['ram'], db_flavor.ram) self.assertEqual(api_flavor['SNF:disk_template'], - db_flavor.disk_template) + db_flavor.volume_type.disk_template) def test_flavor_details(self): """Test if the expected flavor is returned.""" @@ -103,7 +85,7 @@ class FlavorAPITest(BaseAPITest): self.assertEqual(api_flavor['name'], db_flavor.name) self.assertEqual(api_flavor['ram'], db_flavor.ram) self.assertEqual(api_flavor['SNF:disk_template'], - db_flavor.disk_template) + db_flavor.volume_type.disk_template) def test_deleted_flavor_details(self): """Test that API returns details for deleted flavors""" diff --git a/snf-cyclades-app/synnefo/api/tests/floating_ips.py b/snf-cyclades-app/synnefo/api/tests/floating_ips.py index 9d71b8684c13c7a4255eb03088faa73a568e1e12..a6186331ec43c3fc273d76cc6153d3547b56a0b7 100644 --- a/snf-cyclades-app/synnefo/api/tests/floating_ips.py +++ b/snf-cyclades-app/synnefo/api/tests/floating_ips.py @@ -1,35 +1,17 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.utils import simplejson as json from snf_django.utils.testing import BaseAPITest, mocked_quotaholder @@ -65,7 +47,8 @@ class FloatingIPAPITest(BaseAPITest): self.assertEqual(json.loads(response.content)["floatingips"], []) def test_list_ips(self): - ip = mf.IPv4AddressFactory(userid="user1", floating_ip=True) + ip = mf.IPv4AddressFactory(userid="user1", project="user1", + floating_ip=True) with mocked_quotaholder(): response = self.get(URL, "user1") self.assertSuccess(response) @@ -77,12 +60,13 @@ class FloatingIPAPITest(BaseAPITest): "id": str(ip.id), "port_id": str(ip.nic.id), "deleted": False, - "floating_network_id": str(ip.network_id), - "tenant_id": ip.userid, - "user_id": ip.userid}) + "user_id": "user1", + "tenant_id": "user1", + "floating_network_id": str(ip.network_id)}) def test_get_ip(self): - ip = mf.IPv4AddressFactory(userid="user1", floating_ip=True) + ip = mf.IPv4AddressFactory(userid="user1", project="user1", + floating_ip=True) with mocked_quotaholder(): response = self.get(URL + "/%s" % ip.id, "user1") self.assertSuccess(response) @@ -94,9 +78,9 @@ class FloatingIPAPITest(BaseAPITest): "id": str(ip.id), "port_id": str(ip.nic.id), "deleted": False, - "floating_network_id": str(ip.network_id), - "tenant_id": ip.userid, - "user_id": ip.userid}) + "user_id": "user1", + "tenant_id": "user1", + "floating_network_id": str(ip.network_id)}) def test_wrong_user(self): ip = mf.IPv4AddressFactory(userid="user1", floating_ip=True) @@ -128,9 +112,9 @@ class FloatingIPAPITest(BaseAPITest): "id": str(ip.id), "port_id": None, "deleted": False, - "floating_network_id": str(self.pool.id), - "tenant_id": ip.userid, - "user_id": ip.userid}) + "user_id": "test_user", + "tenant_id": "test_user", + "floating_network_id": str(self.pool.id)}) def test_reserve_empty_body(self): """Test reserve FIP without specifying network.""" @@ -204,9 +188,9 @@ class FloatingIPAPITest(BaseAPITest): "id": str(ip.id), "port_id": None, "deleted": False, - "floating_network_id": str(self.pool.id), - "tenant_id": ip.userid, - "user_id": ip.userid}) + "user_id": "test_user", + "tenant_id": "test_user", + "floating_network_id": str(self.pool.id)}) # Already reserved with mocked_quotaholder(): diff --git a/snf-cyclades-app/synnefo/api/tests/images.py b/snf-cyclades-app/synnefo/api/tests/images.py index 0276a50d0b1489bc28e8c2b44f094513748dfccd..f7ed231ef8bd7851aa0ca3e849ae08c489498003 100644 --- a/snf-cyclades-app/synnefo/api/tests/images.py +++ b/snf-cyclades-app/synnefo/api/tests/images.py @@ -1,35 +1,17 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import json @@ -42,9 +24,13 @@ from synnefo.lib import join_urls from mock import patch from functools import wraps +compute_path = get_service_path(cyclades_services, 'compute', + version='v2.0') +IMAGES_URL = join_urls(compute_path, "images/") + def assert_backend_closed(func): - """Decorator for ensuring that ImageBackend is returned to pool.""" + """Decorator for ensuring that PlanktonBackend is returned to pool.""" @wraps(func) def wrapper(self, backend): result = func(self, backend) @@ -54,34 +40,12 @@ def assert_backend_closed(func): return wrapper -class ComputeAPITest(BaseAPITest): - def setUp(self, *args, **kwargs): - super(ComputeAPITest, self).setUp(*args, **kwargs) - self.compute_path = get_service_path(cyclades_services, 'compute', - version='v2.0') - def myget(self, path, *args, **kwargs): - path = join_urls(self.compute_path, path) - return self.get(path, *args, **kwargs) - - def myput(self, path, *args, **kwargs): - path = join_urls(self.compute_path, path) - return self.put(path, *args, **kwargs) - - def mypost(self, path, *args, **kwargs): - path = join_urls(self.compute_path, path) - return self.post(path, *args, **kwargs) - - def mydelete(self, path, *args, **kwargs): - path = join_urls(self.compute_path, path) - return self.delete(path, *args, **kwargs) - - -@patch('synnefo.plankton.backend.ImageBackend') -class ImageAPITest(ComputeAPITest): +@patch('synnefo.plankton.backend.PlanktonBackend') +class ImageAPITest(BaseAPITest): @assert_backend_closed def test_create_image(self, mimage): """Test that create image is not implemented""" - response = self.mypost('images/', 'user', json.dumps(''), 'json') + response = self.post(IMAGES_URL, 'user', json.dumps(''), 'json') self.assertEqual(response.status_code, 501) @assert_backend_closed @@ -90,14 +54,15 @@ class ImageAPITest(ComputeAPITest): images = [{'id': 1, 'name': u'image-1 \u2601'}, {'id': 2, 'name': u'image-2 \u2602'}, {'id': 3, 'name': u'image-3 \u2603'}] - mimage().list_images.return_value = images - response = self.myget('images', 'user') + mimage().__enter__().list_images.return_value = images + response = self.get(IMAGES_URL, 'user') self.assertSuccess(response) api_images = json.loads(response.content)['images'] self.assertEqual(images, api_images) @assert_backend_closed def test_list_images_detail(self, mimage): + self.maxDiff = None images = [{'id': 1, 'name': u'image-1 \u2601', 'status': 'available', @@ -105,6 +70,8 @@ class ImageAPITest(ComputeAPITest): 'updated_at': '2012-12-26 11:52:54', 'owner': 'user1', 'deleted_at': '', + 'is_snapshot': False, + 'is_public': True, 'properties': {u'foo\u2610': u'bar\u2611'}}, {'id': 2, 'name': 'image-2', @@ -113,6 +80,8 @@ class ImageAPITest(ComputeAPITest): 'updated_at': '2012-12-26 11:52:54', 'owner': 'user1', 'deleted_at': '2012-12-27 11:52:54', + 'is_snapshot': False, + 'is_public': True, 'properties': ''}, {'id': 3, 'name': 'image-3', @@ -121,6 +90,8 @@ class ImageAPITest(ComputeAPITest): 'deleted_at': '', 'updated_at': '2012-12-26 11:52:54', 'owner': 'user1', + 'is_snapshot': False, + 'is_public': False, 'properties': ''}] result_images = [ {'id': 1, @@ -131,6 +102,8 @@ class ImageAPITest(ComputeAPITest): 'updated': '2012-12-26T11:52:54+00:00', 'user_id': 'user1', 'tenant_id': 'user1', + 'is_snapshot': False, + 'public': True, 'metadata': {u'foo\u2610': u'bar\u2611'}}, {'id': 2, 'name': 'image-2', @@ -140,6 +113,8 @@ class ImageAPITest(ComputeAPITest): 'tenant_id': 'user1', 'created': '2012-11-26T11:52:54+00:00', 'updated': '2012-12-26T11:52:54+00:00', + 'is_snapshot': False, + 'public': True, 'metadata': {}}, {'id': 3, 'name': 'image-3', @@ -149,9 +124,11 @@ class ImageAPITest(ComputeAPITest): 'tenant_id': 'user1', 'created': '2012-11-26T11:52:54+00:00', 'updated': '2012-12-26T11:52:54+00:00', + 'is_snapshot': False, + 'public': False, 'metadata': {}}] - mimage().list_images.return_value = images - response = self.myget('images/detail', 'user') + mimage().__enter__().list_images.return_value = images + response = self.get(join_urls(IMAGES_URL, "detail"), 'user') self.assertSuccess(response) api_images = json.loads(response.content)['images'] self.assertEqual(len(result_images), len(api_images)) @@ -168,12 +145,14 @@ class ImageAPITest(ComputeAPITest): images = [ {'id': 1, 'name': 'image-1', - 'status':'available', + 'status': 'available', 'progress': 100, 'created_at': old_time.isoformat(), 'deleted_at': '', 'updated_at': old_time.isoformat(), 'owner': 'user1', + 'is_snapshot': False, + 'is_public': True, 'properties': ''}, {'id': 2, 'name': 'image-2', @@ -183,16 +162,20 @@ class ImageAPITest(ComputeAPITest): 'created_at': new_time.isoformat(), 'updated_at': new_time.isoformat(), 'deleted_at': new_time.isoformat(), + 'is_snapshot': False, + 'is_public': False, 'properties': ''}] - mimage().list_images.return_value = images + mimage().__enter__().list_images.return_value = images response =\ - self.myget('images/detail?changes-since=%sUTC' % new_time) + self.get(join_urls(IMAGES_URL, 'detail?changes-since=%sUTC' % + new_time)) self.assertSuccess(response) api_images = json.loads(response.content)['images'] self.assertEqual(1, len(api_images)) @assert_backend_closed def test_get_image_details(self, mimage): + self.maxDiff = None image = {'id': 42, 'name': 'image-1', 'status': 'available', @@ -200,6 +183,8 @@ class ImageAPITest(ComputeAPITest): 'updated_at': '2012-12-26 11:52:54', 'deleted_at': '', 'owner': 'user1', + 'is_snapshot': False, + 'is_public': True, 'properties': {'foo': 'bar'}} result_image = \ {'id': 42, @@ -210,175 +195,173 @@ class ImageAPITest(ComputeAPITest): 'updated': '2012-12-26T11:52:54+00:00', 'user_id': 'user1', 'tenant_id': 'user1', + 'is_snapshot': False, + 'public': True, 'metadata': {'foo': 'bar'}} - mimage.return_value.get_image.return_value = image - response = self.myget('images/42', 'user') + mimage().__enter__().get_image.return_value = image + response = self.get(join_urls(IMAGES_URL, "42"), 'user') self.assertSuccess(response) api_image = json.loads(response.content)['image'] api_image.pop("links") self.assertEqual(api_image, result_image) - @assert_backend_closed def test_invalid_image(self, mimage): - mimage.return_value.get_image.side_effect = faults.ItemNotFound('Image not found') - response = self.myget('images/42', 'user') + mimage().__enter__().get_image.side_effect = \ + faults.ItemNotFound('Image not found') + response = self.get(join_urls(IMAGES_URL, "42"), 'user') self.assertItemNotFound(response) @assert_backend_closed def test_delete_image(self, mimage): - response = self.mydelete("images/42", "user") + response = self.delete(join_urls(IMAGES_URL, "42"), 'user') self.assertEqual(response.status_code, 204) - mimage.return_value.unregister.assert_called_once_with('42') - mimage.return_value._delete.assert_not_called('42') + mimage().__enter__().unregister.assert_called_once_with('42') @assert_backend_closed def test_catch_wrong_api_paths(self, *args): - response = self.myget('nonexistent') + response = self.get(join_urls(IMAGES_URL, 'nonexistent/lala/foo')) self.assertEqual(response.status_code, 400) try: - error = json.loads(response.content) + json.loads(response.content) except ValueError: self.assertTrue(False) @assert_backend_closed def test_method_not_allowed(self, *args): # /images/ allows only POST, GET - response = self.myput('images', '', '') + response = self.put(IMAGES_URL, '', '') self.assertMethodNotAllowed(response) - response = self.mydelete('images') + response = self.delete(IMAGES_URL, '') self.assertMethodNotAllowed(response) # /images/<imgid>/ allows only GET, DELETE - response = self.mypost("images/42") - self.assertMethodNotAllowed(response) - response = self.myput('images/42', '', '') - self.assertMethodNotAllowed(response) - - # /images/<imgid>/metadata/ allows only POST, GET - response = self.myput('images/42/metadata', '', '') + response = self.post(join_urls(IMAGES_URL, "42"), 'user') self.assertMethodNotAllowed(response) - response = self.mydelete('images/42/metadata') + response = self.put(join_urls(IMAGES_URL, "42"), 'user') self.assertMethodNotAllowed(response) # /images/<imgid>/metadata/ allows only POST, GET - response = self.myput('images/42/metadata', '', '') + response = self.put(join_urls(IMAGES_URL, "42", "metadata"), 'user') self.assertMethodNotAllowed(response) - response = self.mydelete('images/42/metadata') + response = self.delete(join_urls(IMAGES_URL, "42", "metadata"), 'user') self.assertMethodNotAllowed(response) # /images/<imgid>/metadata/<key> allows only PUT, GET, DELETE - response = self.mypost('images/42/metadata/foo') + response = self.post(join_urls(IMAGES_URL, "42", "metadata", "foo"), + 'user') self.assertMethodNotAllowed(response) -@patch('synnefo.plankton.backend.ImageBackend') -class ImageMetadataAPITest(ComputeAPITest): +@patch('synnefo.plankton.backend.PlanktonBackend') +class ImageMetadataAPITest(BaseAPITest): def setUp(self): self.image = {'id': 42, - 'name': 'image-1', - 'status': 'available', - 'created_at': '2012-11-26 11:52:54', - 'updated_at': '2012-12-26 11:52:54', - 'deleted_at': '', - 'properties': {'foo': 'bar', 'foo2': 'bar2'}} + 'name': 'image-1', + 'status': 'available', + 'created_at': '2012-11-26 11:52:54', + 'updated_at': '2012-12-26 11:52:54', + 'deleted_at': '', + 'properties': {'foo': 'bar', 'foo2': 'bar2'}} self.result_image = \ - {'id': 42, - 'name': 'image-1', - 'status': 'ACTIVE', - 'progress': 100, - 'created': '2012-11-26T11:52:54+00:00', - 'updated': '2012-12-26T11:52:54+00:00', - 'metadata': {'foo': 'bar'}} + {'id': 42, + 'name': 'image-1', + 'status': 'ACTIVE', + 'progress': 100, + 'created': '2012-11-26T11:52:54+00:00', + 'updated': '2012-12-26T11:52:54+00:00', + 'metadata': {'foo': 'bar'}} super(ImageMetadataAPITest, self).setUp() @assert_backend_closed def test_list_metadata(self, backend): - backend.return_value.get_image.return_value = self.image - response = self.myget('images/42/metadata', 'user') + backend().__enter__().get_image.return_value = self.image + response = self.get(join_urls(IMAGES_URL, '42/metadata'), 'user') self.assertSuccess(response) meta = json.loads(response.content)['metadata'] self.assertEqual(meta, self.image['properties']) @assert_backend_closed def test_get_metadata(self, backend): - backend.return_value.get_image.return_value = self.image - response = self.myget('images/42/metadata/foo', 'user') + backend().__enter__().get_image.return_value = self.image + response = self.get(join_urls(IMAGES_URL, '42/metadata/foo'), 'user') self.assertSuccess(response) meta = json.loads(response.content)['meta'] self.assertEqual(meta['foo'], 'bar') @assert_backend_closed def test_get_invalid_metadata(self, backend): - backend.return_value.get_image.return_value = self.image - response = self.myget('images/42/metadata/not_found', 'user') + backend().__enter__().get_image.return_value = self.image + response = self.get(join_urls(IMAGES_URL, '42/metadata/not_found'), + 'user') self.assertItemNotFound(response) def test_delete_metadata_item(self, backend): - backend.return_value.get_image.return_value = self.image - response = self.mydelete('images/42/metadata/foo', 'user') + backend().__enter__().get_image.return_value = self.image + response = self.delete(join_urls(IMAGES_URL, '42/metadata/foo'), + 'user') self.assertEqual(response.status_code, 204) - backend.return_value.update_metadata.assert_called_once_with('42', {'properties': {'foo2': - 'bar2'}}) + backend().__enter__().update_metadata\ + .assert_called_once_with('42', {'properties': {'foo2': 'bar2'}}) @assert_backend_closed def test_create_metadata_item(self, backend): - backend.return_value.get_image.return_value = self.image + backend().__enter__().get_image.return_value = self.image request = {'meta': {'foo3': 'bar3'}} - response = self.myput('images/42/metadata/foo3', 'user', - json.dumps(request), 'json') + response = self.put(join_urls(IMAGES_URL, '42/metadata/foo3'), 'user', + json.dumps(request), 'json') self.assertEqual(response.status_code, 201) - backend.return_value.update_metadata.assert_called_once_with('42', + backend().__enter__().update_metadata.assert_called_once_with('42', {'properties': {'foo': 'bar', 'foo2': 'bar2', 'foo3': 'bar3'}}) @assert_backend_closed def test_create_metadata_malformed_1(self, backend): - backend.return_value.get_image.return_value = self.image + backend().__enter__().get_image.return_value = self.image request = {'met': {'foo3': 'bar3'}} - response = self.myput('images/42/metadata/foo3', 'user', - json.dumps(request), 'json') + response = self.put(join_urls(IMAGES_URL, '42/metadata/foo3'), 'user', + json.dumps(request), 'json') self.assertBadRequest(response) @assert_backend_closed def test_create_metadata_malformed_2(self, backend): - backend.return_value.get_image.return_value = self.image + backend().__enter__().get_image.return_value = self.image request = {'metadata': [('foo3', 'bar3')]} - response = self.myput('images/42/metadata/foo3', 'user', - json.dumps(request), 'json') + response = self.put(join_urls(IMAGES_URL, '42/metadata/foo3'), 'user', + json.dumps(request), 'json') self.assertBadRequest(response) @assert_backend_closed def test_create_metadata_malformed_3(self, backend): - backend.return_value.get_image.return_value = self.image + backend().__enter__().get_image.return_value = self.image request = {'met': {'foo3': 'bar3', 'foo4': 'bar4'}} - response = self.myput('images/42/metadata/foo3', 'user', - json.dumps(request), 'json') + response = self.put(join_urls(IMAGES_URL, '42/metadata/foo3'), 'user', + json.dumps(request), 'json') self.assertBadRequest(response) @assert_backend_closed def test_create_metadata_malformed_4(self, backend): - backend.return_value.get_image.return_value = self.image + backend().__enter__().get_image.return_value = self.image request = {'met': {'foo3': 'bar3'}} - response = self.myput('images/42/metadata/foo4', 'user', - json.dumps(request), 'json') + response = self.put(join_urls(IMAGES_URL, '42/metadata/foo4'), 'user', + json.dumps(request), 'json') self.assertBadRequest(response) @assert_backend_closed def test_update_metadata_item(self, backend): - backend.return_value.get_image.return_value = self.image + backend().__enter__().get_image.return_value = self.image request = {'metadata': {'foo': 'bar_new', 'foo4': 'bar4'}} - response = self.mypost('images/42/metadata', 'user', - json.dumps(request), 'json') + response = self.post(join_urls(IMAGES_URL, '42/metadata'), 'user', + json.dumps(request), 'json') self.assertEqual(response.status_code, 201) - backend.return_value.update_metadata.assert_called_once_with('42', + backend().__enter__().update_metadata.assert_called_once_with('42', {'properties': {'foo': 'bar_new', 'foo2': 'bar2', 'foo4': 'bar4'} }) @assert_backend_closed def test_update_metadata_malformed(self, backend): - backend.return_value.get_image.return_value = self.image + backend().__enter__().get_image.return_value = self.image request = {'meta': {'foo': 'bar_new', 'foo4': 'bar4'}} - response = self.mypost('images/42/metadata', 'user', - json.dumps(request), 'json') + response = self.post(join_urls(IMAGES_URL, '42/metadata'), 'user', + json.dumps(request), 'json') self.assertBadRequest(response) diff --git a/snf-cyclades-app/synnefo/api/tests/networks.py b/snf-cyclades-app/synnefo/api/tests/networks.py index 3dac6655c8ba3a39e2602820098c0c2291f9ac14..8bf89e6764daf76365772c4b5eee0dc26ef14474 100644 --- a/snf-cyclades-app/synnefo/api/tests/networks.py +++ b/snf-cyclades-app/synnefo/api/tests/networks.py @@ -1,35 +1,17 @@ -# Copyright 2012-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from snf_django.utils.testing import (BaseAPITest, override_settings) from django.utils import simplejson as json @@ -99,8 +81,9 @@ class NetworkTest(BaseAPITest): # TEST QUOTAS!!! name, args, kwargs =\ self.mocked_quotaholder.issue_one_commission.mock_calls[0] - commission_resources = args[2] - self.assertEqual(commission_resources, {"cyclades.network.private": 1}) + commission_resources = args[1] + self.assertEqual(commission_resources, + {("user", "cyclades.network.private"): 1}) name, args, kwargs =\ self.mocked_quotaholder.resolve_commissions.mock_calls[0] serial = QuotaHolderSerial.objects.order_by("-serial")[0] diff --git a/snf-cyclades-app/synnefo/api/tests/ports.py b/snf-cyclades-app/synnefo/api/tests/ports.py index 028c184f6188626273e61814a26d2135a3dcbb8f..7d52cd3b6e1a5a509c07d47dcf900b4af2675d53 100644 --- a/snf-cyclades-app/synnefo/api/tests/ports.py +++ b/snf-cyclades-app/synnefo/api/tests/ports.py @@ -1,35 +1,17 @@ -# Copyright 2011-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A.i +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.conf import settings from snf_django.utils.testing import BaseAPITest, override_settings diff --git a/snf-cyclades-app/synnefo/api/tests/servers.py b/snf-cyclades-app/synnefo/api/tests/servers.py index 7e111a532bff2b631a4661d2c87a010f1b388217..f93f4437802e78a18b70118be3c19f589168cf38 100644 --- a/snf-cyclades-app/synnefo/api/tests/servers.py +++ b/snf-cyclades-app/synnefo/api/tests/servers.py @@ -1,36 +1,18 @@ # encoding: utf-8 -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import json from copy import deepcopy @@ -38,7 +20,7 @@ from copy import deepcopy from snf_django.utils.testing import (BaseAPITest, mocked_quotaholder, override_settings) from synnefo.db.models import (VirtualMachine, VirtualMachineMetadata, - IPAddress, NetworkInterface) + IPAddress, NetworkInterface, Volume) from synnefo.db import models_factory as mfactory from synnefo.logic.utils import get_rsapi_state from synnefo.cyclades_settings import cyclades_services @@ -326,14 +308,20 @@ class ServerAPITest(ComputeAPITest): fixed_image = Mock() fixed_image.return_value = {'location': 'pithos://foo', - 'checksum': '1234', + 'mapfile': '1234', "id": 1, "name": "test_image", - "size": "41242", + "version": 42, + "is_public": True, + "owner": "user1", + "size": 1024, + "is_snapshot": False, + "status": "AVAILABLE", 'disk_format': 'diskdump'} @patch('synnefo.api.util.get_image', fixed_image) +@patch('synnefo.volume.util.get_snapshot', fixed_image) @patch('synnefo.logic.rapi_pool.GanetiRapiClient') class ServerCreateAPITest(ComputeAPITest): def setUp(self): @@ -594,6 +582,89 @@ class ServerCreateAPITest(ComputeAPITest): json.dumps(request), 'json') self.assertEqual(response.status_code, 404) + def test_create_server_with_volumes(self, mrapi): + user = "test_user" + mrapi().CreateInstance.return_value = 42 + # Test creation without any volumes. Server will use flavor+image + request = deepcopy(self.request) + request["server"]["block_device_mapping_v2"] = [] + with mocked_quotaholder(): + response = self.mypost("servers", user, + json.dumps(request), 'json') + self.assertEqual(response.status_code, 202, msg=response.content) + vm_id = json.loads(response.content)["server"]["id"] + volume = Volume.objects.get(machine_id=vm_id) + self.assertEqual(volume.volume_type, self.flavor.volume_type) + self.assertEqual(volume.size, self.flavor.disk) + self.assertEqual(volume.source, "image:%s" % fixed_image()["id"]) + self.assertEqual(volume.delete_on_termination, True) + self.assertEqual(volume.userid, user) + + # Test using an image + request["server"]["block_device_mapping_v2"] = [ + {"source_type": "image", + "uuid": fixed_image()["id"], + "volume_size": 10, + "delete_on_termination": True} + ] + with mocked_quotaholder(): + response = self.mypost("servers", user, + json.dumps(request), 'json') + self.assertEqual(response.status_code, 202, msg=response.content) + vm_id = json.loads(response.content)["server"]["id"] + volume = Volume.objects.get(machine_id=vm_id) + self.assertEqual(volume.volume_type, self.flavor.volume_type) + self.assertEqual(volume.size, 10) + self.assertEqual(volume.source, "image:%s" % fixed_image()["id"]) + self.assertEqual(volume.delete_on_termination, True) + self.assertEqual(volume.userid, user) + self.assertEqual(volume.origin, fixed_image()["mapfile"]) + + # Test using a snapshot + request["server"]["block_device_mapping_v2"] = [ + {"source_type": "snapshot", + "uuid": fixed_image()["id"], + "volume_size": 10, + "delete_on_termination": True} + ] + with mocked_quotaholder(): + response = self.mypost("servers", user, + json.dumps(request), 'json') + self.assertEqual(response.status_code, 202, msg=response.content) + vm_id = json.loads(response.content)["server"]["id"] + volume = Volume.objects.get(machine_id=vm_id) + self.assertEqual(volume.volume_type, self.flavor.volume_type) + self.assertEqual(volume.size, 10) + self.assertEqual(volume.source, "snapshot:%s" % fixed_image()["id"]) + self.assertEqual(volume.origin, fixed_image()["mapfile"]) + self.assertEqual(volume.delete_on_termination, True) + self.assertEqual(volume.userid, user) + + source_volume = volume + # Test using source volume + request["server"]["block_device_mapping_v2"] = [ + {"source_type": "volume", + "uuid": source_volume.id, + "volume_size": source_volume.size, + "delete_on_termination": True} + ] + with mocked_quotaholder(): + response = self.mypost("servers", user, + json.dumps(request), 'json') + # This will fail because the volume is not AVAILABLE. + self.assertBadRequest(response) + + # Test using a blank volume + request["server"]["block_device_mapping_v2"] = [ + {"source_type": "blank", + "volume_size": 10, + "delete_on_termination": True} + ] + with mocked_quotaholder(): + response = self.mypost("servers", user, + json.dumps(request), 'json') + self.assertBadRequest(response) + @patch('synnefo.logic.rapi_pool.GanetiRapiClient') class ServerDestroyAPITest(ComputeAPITest): @@ -787,8 +858,10 @@ class ServerActionAPITest(ComputeAPITest): response = self.mypost('servers/%d/action' % vm.id, vm.userid, json.dumps(request), 'json') self.assertBadRequest(response) - flavor2 = mfactory.FlavorFactory(disk_template="foo") - flavor3 = mfactory.FlavorFactory(disk_template="baz") + + # Check flavor with different volume type + flavor2 = mfactory.FlavorFactory(volume_type__disk_template="foo") + flavor3 = mfactory.FlavorFactory(volume_type__disk_template="baz") vm = self.get_vm(flavor=flavor2, operstate="STOPPED") request = {'resize': {'flavorRef': flavor3.id}} response = self.mypost('servers/%d/action' % vm.id, @@ -796,7 +869,7 @@ class ServerActionAPITest(ComputeAPITest): self.assertBadRequest(response) # Check success vm = self.get_vm(flavor=flavor, operstate="STOPPED") - flavor4 = mfactory.FlavorFactory(disk_template=flavor.disk_template, + flavor4 = mfactory.FlavorFactory(volume_type=vm.flavor.volume_type, disk=flavor.disk, cpu=4, ram=2048) request = {'resize': {'flavorRef': flavor4.id}} @@ -850,9 +923,19 @@ class ServerVNCConsole(ComputeAPITest): vm.save() data = json.dumps({'console': {'type': 'vnc'}}) - with override_settings(settings, TEST=True): - response = self.mypost('servers/%d/action' % vm.id, - vm.userid, data, 'json') + with patch('synnefo.logic.rapi_pool.GanetiRapiClient') as rapi: + rapi().GetInstance.return_value = {"pnode": "node1", + "network_port": 5055, + "oper_state": True, + "hvparams": { + "serial_console": False + }} + with patch("synnefo.logic.servers.request_vnc_forwarding") as vnc: + vnc.return_value = {"status": "OK", + "source_port": 42} + response = self.mypost('servers/%d/action' % vm.id, + vm.userid, data, 'json') + self.assertEqual(response.status_code, 200) reply = json.loads(response.content) self.assertEqual(reply.keys(), ['console']) @@ -871,3 +954,83 @@ class ServerVNCConsole(ComputeAPITest): response = self.mypost('servers/%d/action' % vm.id, vm.userid, data, 'json') self.assertBadRequest(response) + + +@patch('synnefo.logic.rapi_pool.GanetiRapiClient') +class ServerAttachments(ComputeAPITest): + def test_list_attachments(self, mrapi): + # Test default volume + vol = mfactory.VolumeFactory() + vm = vol.machine + + response = self.myget("servers/%d/os-volume_attachments" % vm.id, + vm.userid) + self.assertSuccess(response) + attachments = json.loads(response.content) + self.assertEqual(len(attachments), 1) + self.assertEqual(attachments["volumeAttachments"][0], + {"volumeId": vol.id, + "serverId": vm.id, + "id": vol.id, + "device": ""}) + + # Test deleted Volume + dvol = mfactory.VolumeFactory(machine=vm, deleted=True) + response = self.myget("servers/%d/os-volume_attachments" % vm.id, + vm.userid) + self.assertSuccess(response) + attachments = json.loads(response.content)["volumeAttachments"] + self.assertEqual(len([d for d in attachments if d["id"] == dvol.id]), + 0) + + def test_attach_detach_volume(self, mrapi): + vol = mfactory.VolumeFactory(status="AVAILABLE") + vm = vol.machine + volume_type = vm.flavor.volume_type + # Test that we cannot detach the root volume + response = self.mydelete("servers/%d/os-volume_attachments/%d" % + (vm.id, vol.id), vm.userid) + self.assertBadRequest(response) + + # Test that we cannot attach a used volume + vol1 = mfactory.VolumeFactory(status="IN_USE", + volume_type=volume_type, + userid=vm.userid) + request = json.dumps({"volumeAttachment": {"volumeId": vol1.id}}) + response = self.mypost("servers/%d/os-volume_attachments" % + vm.id, vm.userid, + request, "json") + self.assertBadRequest(response) + + vol1.status = "AVAILABLE" + # We cannot attach a volume of different disk template + volume_type_2 = mfactory.VolumeTypeFactory(disk_template="lalalal") + vol1.volume_type = volume_type_2 + vol1.save() + response = self.mypost("servers/%d/os-volume_attachments/" % + vm.id, vm.userid, + request, "json") + self.assertBadRequest(response) + + vol1.volume_type = volume_type + vol1.save() + mrapi().ModifyInstance.return_value = 43 + response = self.mypost("servers/%d/os-volume_attachments" % + vm.id, vm.userid, + request, "json") + self.assertEqual(response.status_code, 202, response.content) + attachment = json.loads(response.content)["volumeAttachment"] + self.assertEqual(attachment, {"volumeId": vol1.id, + "serverId": vm.id, + "id": vol1.id, + "device": ""}) + # And we delete it...will fail because of status + response = self.mydelete("servers/%d/os-volume_attachments/%d" % + (vm.id, vol1.id), vm.userid) + self.assertBadRequest(response) + vm.task = None + vm.save() + vm.volumes.all().update(status="IN_USE") + response = self.mydelete("servers/%d/os-volume_attachments/%d" % + (vm.id, vol1.id), vm.userid) + self.assertEqual(response.status_code, 202, response.content) diff --git a/snf-cyclades-app/synnefo/api/tests/subnets.py b/snf-cyclades-app/synnefo/api/tests/subnets.py index e0d98be48ffb28253922bc4f2813aa994ece7a0f..cc5e184fc7f16310f7fe155bb704b4e979f73ed7 100644 --- a/snf-cyclades-app/synnefo/api/tests/subnets.py +++ b/snf-cyclades-app/synnefo/api/tests/subnets.py @@ -1,31 +1,17 @@ -# Copyright 2011-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from snf_django.utils.testing import BaseAPITest from django.utils import simplejson as json diff --git a/snf-cyclades-app/synnefo/api/tests/versions.py b/snf-cyclades-app/synnefo/api/tests/versions.py index 6a385d17de36569e6585afe4872132709b5e2603..b87065ba97acba9ac183669fec11866e0d0dc57b 100644 --- a/snf-cyclades-app/synnefo/api/tests/versions.py +++ b/snf-cyclades-app/synnefo/api/tests/versions.py @@ -1,35 +1,17 @@ -# Copyright 2012-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.utils import simplejson as json from django.test import TestCase diff --git a/snf-cyclades-app/synnefo/api/util.py b/snf-cyclades-app/synnefo/api/util.py index b7ee6730034723ccc2145a5669a4fe50f4f54928..c62b0cb3b199f0c6ce4fb244f190e64fdb65c65f 100644 --- a/snf-cyclades-app/synnefo/api/util.py +++ b/snf-cyclades-app/synnefo/api/util.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from base64 import urlsafe_b64encode, b64decode from urllib import quote @@ -51,7 +33,7 @@ from synnefo.db.models import (Flavor, VirtualMachine, VirtualMachineMetadata, Network, NetworkInterface, SecurityGroup, BridgePoolTable, MacPrefixPoolTable, IPAddress, IPPoolTable) -from synnefo.plankton.utils import image_backend +from synnefo.plankton.backend import PlanktonBackend from synnefo.cyclades_settings import cyclades_services, BASE_HOST from synnefo.lib.services import get_service_path @@ -85,7 +67,7 @@ def random_password(): """Generates a random password We generate a windows compliant password: it must contain at least - one charachter from each of the groups: upper case, lower case, digits. + one character from each of the groups: upper case, lower case, digits. """ pool = lowercase + uppercase + digits @@ -166,8 +148,11 @@ def get_vm_meta(vm, key): def get_image(image_id, user_id): """Return an Image instance or raise ItemNotFound.""" - with image_backend(user_id) as backend: - return backend.get_image(image_id) + with PlanktonBackend(user_id) as backend: + try: + return backend.get_image(image_id) + except faults.ItemNotFound: + raise faults.ItemNotFound("Image '%s' not found" % image_id) def get_image_dict(image_id, user_id): @@ -175,13 +160,17 @@ def get_image_dict(image_id, user_id): img = get_image(image_id, user_id) image["id"] = img["id"] image["name"] = img["name"] - image["format"] = img["disk_format"] - image["checksum"] = img["checksum"] image["location"] = img["location"] + image["is_snapshot"] = img["is_snapshot"] + image["is_public"] = img["is_public"] + image["status"] = img["status"] + image["owner"] = img["owner"] + image["format"] = img["disk_format"] + image["version"] = img["version"] - checksum = image["checksum"] = img["checksum"] size = image["size"] = img["size"] - image["backend_id"] = PITHOSMAP_PREFIX + "/".join([checksum, str(size)]) + mapfile = image["mapfile"] = img["mapfile"] + image["pithosmap"] = PITHOSMAP_PREFIX + "/".join([mapfile, str(size)]) properties = img.get("properties", {}) image["metadata"] = dict((key.upper(), val) @@ -195,31 +184,16 @@ def get_flavor(flavor_id, include_deleted=False): try: flavor_id = int(flavor_id) - if include_deleted: - return Flavor.objects.get(id=flavor_id) - else: - return Flavor.objects.get(id=flavor_id, deleted=include_deleted) + flavors = Flavor.objects.select_related("volume_type") + if not include_deleted: + flavors = flavors.filter(deleted=False) + return flavors.get(id=flavor_id) except (ValueError, TypeError): raise faults.BadRequest("Invalid flavor ID '%s'" % flavor_id) except Flavor.DoesNotExist: raise faults.ItemNotFound('Flavor not found.') -def get_flavor_provider(flavor): - """Extract provider from disk template. - - Provider for `ext` disk_template is encoded in the disk template - name, which is formed `ext_<provider_name>`. Provider is None - for all other disk templates. - - """ - disk_template = flavor.disk_template - provider = None - if disk_template.startswith("ext"): - disk_template, provider = disk_template.split("_", 1) - return disk_template, provider - - def get_network(network_id, user_id, for_update=False, non_deleted=False): """Return a Network instance or raise ItemNotFound.""" diff --git a/snf-cyclades-app/synnefo/api/versions.py b/snf-cyclades-app/synnefo/api/versions.py index a0e8a48eb1c5d1c9b3d9b276e2b3aa3b75dcaadb..77ad912a1e3f70b86799b2070e1f32148d12103c 100644 --- a/snf-cyclades-app/synnefo/api/versions.py +++ b/snf-cyclades-app/synnefo/api/versions.py @@ -1,35 +1,17 @@ -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from logging import getLogger diff --git a/snf-cyclades-app/synnefo/app_settings/__init__.py b/snf-cyclades-app/synnefo/app_settings/__init__.py index 8d26ca043a830ab69ec842c93aa31ba31313b023..bb205283d10a83aa8c82298526a46b660d77f62b 100644 --- a/snf-cyclades-app/synnefo/app_settings/__init__.py +++ b/snf-cyclades-app/synnefo/app_settings/__init__.py @@ -8,6 +8,7 @@ synnefo_web_apps = [ 'synnefo.helpdesk', 'synnefo.userdata', 'synnefo.quotas', + 'synnefo.volume', ] synnefo_web_middleware = [] diff --git a/snf-cyclades-app/synnefo/app_settings/default/__init__.py b/snf-cyclades-app/synnefo/app_settings/default/__init__.py index cb23c8a60c2b860a029b728c756fd85c380eeeec..b1f8634f8e17fa595c7fc12ad658c69127096760 100644 --- a/snf-cyclades-app/synnefo/app_settings/default/__init__.py +++ b/snf-cyclades-app/synnefo/app_settings/default/__init__.py @@ -1,35 +1,17 @@ -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from synnefo.app_settings.default.backend import * from synnefo.app_settings.default.queues import * diff --git a/snf-cyclades-app/synnefo/app_settings/default/api.py b/snf-cyclades-app/synnefo/app_settings/default/api.py index 55daec1d9e242e56597832f0ac2b5fbe8c0cc3f0..2d8bdc746ac7a61856043dbac0857bf4b0c5f1b0 100644 --- a/snf-cyclades-app/synnefo/app_settings/default/api.py +++ b/snf-cyclades-app/synnefo/app_settings/default/api.py @@ -16,6 +16,11 @@ POLL_LIMIT = 3600 # Astakos groups that have access to '/admin' views. ADMIN_STATS_PERMITTED_GROUPS = ["admin-stats"] +# Enable/Disable the snapshots feature altogether at the API level. +# If set to False, Cyclades will not expose the '/snapshots' API URL +# of the 'volume' app. +CYCLADES_SNAPSHOTS_ENABLED = True + # # Network Configuration # @@ -147,22 +152,34 @@ CYCLADES_SERVERS_FQDN = 'snf-%(id)s.vm.example.synnefo.org' #} CYCLADES_PORT_FORWARDING = {} -# Extra configuration options required for snf-vncauthproxy (>=1.5) -CYCLADES_VNCAUTHPROXY_OPTS = { - # These values are required for VNC console support. They should match a - # user / password configured in the snf-vncauthproxy authentication / users - # file (/var/lib/vncauthproxy/users). - 'auth_user': 'synnefo', - 'auth_password': 'secret_password', - # server_address and server_port should reflect the --listen-address and - # --listen-port options passed to the vncauthproxy daemon - 'server_address': '127.0.0.1', - 'server_port': 24999, - # Set to True to enable SSL support on the control socket. - 'enable_ssl': False, - # If you enabled SSL support for snf-vncauthproxy you can optionally - # provide a path to a CA file and enable strict checkfing for the server - # certficiate. - 'ca_cert': None, - 'strict': False, -} +# Extra configuration options required for snf-vncauthproxy (>=1.5). Each dict +# of the list, describes one vncauthproxy instance. +CYCLADES_VNCAUTHPROXY_OPTS = [ + { + # These values are required for VNC console support. They should match + # a user / password configured in the snf-vncauthproxy authentication / + # users file (/var/lib/vncauthproxy/users). + 'auth_user': 'synnefo', + 'auth_password': 'secret_password', + # server_address and server_port should reflect the --listen-address and + # --listen-port options passed to the vncauthproxy daemon + 'server_address': '127.0.0.1', + 'server_port': 24999, + # Set to True to enable SSL support on the control socket. + 'enable_ssl': False, + # If you enabled SSL support for snf-vncauthproxy you can optionally + # provide a path to a CA file and enable strict checkfing for the server + # certficiate. + 'ca_cert': None, + 'strict': False, + }, +] + +# The maximum allowed size(GB) for a Cyclades Volume +CYCLADES_VOLUME_MAX_SIZE = 200 + +# The maximum allowed metadata items for a Cyclades Volume +CYCLADES_VOLUME_MAX_METADATA = 10 + +# The maximmum allowed metadata items for a Cyclades Virtual Machine +CYCLADES_VM_MAX_METADATA = 10 diff --git a/snf-cyclades-app/synnefo/app_settings/default/backend.py b/snf-cyclades-app/synnefo/app_settings/default/backend.py index f4ea5f4a9d7f9fea6e6bad2eade938af8d2456aa..16b2adf551f9ce9f8278a54b9312f64514be7c0c 100644 --- a/snf-cyclades-app/synnefo/app_settings/default/backend.py +++ b/snf-cyclades-app/synnefo/app_settings/default/backend.py @@ -27,7 +27,7 @@ GANETI_CREATEINSTANCE_KWARGS = { 'hvparams': {"kvm": {'serial_console': False}, "xen-pvm": {}, "xen-hvm": {}}, - 'wait_for_sync': False} +} # If True, qemu-kvm will hotplug a NIC when connecting a vm to # a network. This requires qemu-kvm=1.0. @@ -37,6 +37,10 @@ GANETI_USE_HOTPLUG = True # not already locked. This might result in slightly unbalanced clusters. GANETI_USE_OPPORTUNISTIC_LOCKING = True +# If False, Ganeti will not wait for the disk mirror to sync +# (--no-wait-for-sync option in Ganeti). Useful only for DRBD template. +GANETI_DISKS_WAIT_FOR_SYNC = False + # This module implements the strategy for allocating a vm to a backend BACKEND_ALLOCATOR_MODULE = "synnefo.logic.allocators.default_allocator" # Refresh backend statistics timeout, in minutes, used in backend allocation @@ -46,6 +50,10 @@ BACKEND_REFRESH_MIN = 15 # than 'max:nic-count' option of Ganeti's ipolicy. GANETI_MAX_NICS_PER_INSTANCE = 8 +# Maximum number of disks per Ganeti instance. This value must be less or equal +# than 'max:disk-count' option of Ganeti's ipolicy. +GANETI_MAX_DISKS_PER_INSTANCE = 8 + # The following setting defines a dictionary with key-value parameters to be # passed to each Ganeti ExtStorage provider. The setting defines a mapping from # the provider name, e.g. 'archipelago' to a dictionary with the actual diff --git a/snf-cyclades-app/synnefo/app_settings/default/plankton.py b/snf-cyclades-app/synnefo/app_settings/default/plankton.py index 18a12cc1700d3b21f19f4c103e7d45a54d981a6f..bb62b596c91a9f70a10d852acd0712ad58870e0c 100644 --- a/snf-cyclades-app/synnefo/app_settings/default/plankton.py +++ b/snf-cyclades-app/synnefo/app_settings/default/plankton.py @@ -5,7 +5,6 @@ # Backend settings BACKEND_DB_CONNECTION = 'sqlite:////usr/share/synnefo/pithos/backend.db' -BACKEND_BLOCK_PATH = '/usr/share/synnefo/pithos/data/' PITHOS_BACKEND_POOL_SIZE = 8 # The Pithos container where images will be stored by default @@ -19,3 +18,15 @@ DEFAULT_CONTAINER_FORMAT = 'bare' # The owner of the images that will be marked as "system images" by the UI SYSTEM_IMAGES_OWNER = 'okeanos' + +# Archipelago Configuration File +PITHOS_BACKEND_ARCHIPELAGO_CONF = '/etc/archipelago/archipelago.conf' + +# Archipelagp xseg pool size +PITHOS_BACKEND_XSEG_POOL_SIZE = 8 + +# The maximum interval (in seconds) for consequent backend object map checks +PITHOS_BACKEND_MAP_CHECK_INTERVAL = 1 + +#The maximum allowed number of image metadata +PITHOS_RESOURCE_MAX_METADATA = 32 diff --git a/snf-cyclades-app/synnefo/app_settings/urls.py b/snf-cyclades-app/synnefo/app_settings/urls.py index 0cc628c35f450d3fb7185f82692e4a00ea771ec4..991c0bb9745e0de12907fdf3ee336e61f609b463 100644 --- a/snf-cyclades-app/synnefo/app_settings/urls.py +++ b/snf-cyclades-app/synnefo/app_settings/urls.py @@ -1,35 +1,17 @@ -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.conf.urls import patterns, include @@ -40,7 +22,7 @@ from snf_django.utils.urls import \ from snf_django.lib.api.urls import api_patterns from synnefo.cyclades_settings import ( BASE_PATH, COMPUTE_PREFIX, NETWORK_PREFIX, VMAPI_PREFIX, - PLANKTON_PREFIX, HELPDESK_PREFIX, UI_PREFIX, + PLANKTON_PREFIX, HELPDESK_PREFIX, UI_PREFIX, VOLUME_PREFIX, USERDATA_PREFIX, ADMIN_PREFIX, ASTAKOS_AUTH_PROXY_PATH, ASTAKOS_AUTH_URL, ASTAKOS_ACCOUNT_PROXY_PATH, ASTAKOS_ACCOUNT_URL, @@ -66,6 +48,7 @@ cyclades_patterns = api_patterns( (prefix_pattern(NETWORK_PREFIX), include('synnefo.api.network_urls')), (prefix_pattern(USERDATA_PREFIX), include('synnefo.userdata.urls')), (prefix_pattern(ADMIN_PREFIX), include('synnefo.admin.urls')), + (prefix_pattern(VOLUME_PREFIX), include('synnefo.volume.urls')), ) cyclades_patterns += patterns( diff --git a/snf-cyclades-app/synnefo/cyclades_settings.py b/snf-cyclades-app/synnefo/cyclades_settings.py index 906ab4118c07bbc01f09eb7cb0c8ff4d6be37a3f..d05c4a403524f6f9c98411716a0ccb849573edd7 100644 --- a/snf-cyclades-app/synnefo/cyclades_settings.py +++ b/snf-cyclades-app/synnefo/cyclades_settings.py @@ -1,41 +1,22 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import logging from django.conf import settings from synnefo.lib import join_urls, parse_base_url -from synnefo.util.keypath import get_path, set_path from synnefo.api.services import cyclades_services as vanilla_cyclades_services from synnefo.lib.services import fill_endpoints from astakosclient import AstakosClient @@ -53,20 +34,18 @@ BASE_URL = getattr(settings, 'CYCLADES_BASE_URL', BASE_HOST, BASE_PATH = parse_base_url(BASE_URL) SERVICE_TOKEN = getattr(settings, 'CYCLADES_SERVICE_TOKEN', "") -CUSTOMIZE_SERVICES = getattr(settings, 'CYCLADES_CUSTOMIZE_SERVICES', ()) cyclades_services = deepcopy(vanilla_cyclades_services) fill_endpoints(cyclades_services, BASE_URL) -for path, value in CUSTOMIZE_SERVICES: - set_path(cyclades_services, path, value, createpath=True) - -COMPUTE_PREFIX = get_path(cyclades_services, 'cyclades_compute.prefix') -NETWORK_PREFIX = get_path(cyclades_services, 'cyclades_network.prefix') -VMAPI_PREFIX = get_path(cyclades_services, 'cyclades_vmapi.prefix') -PLANKTON_PREFIX = get_path(cyclades_services, 'cyclades_plankton.prefix') -HELPDESK_PREFIX = get_path(cyclades_services, 'cyclades_helpdesk.prefix') -UI_PREFIX = get_path(cyclades_services, 'cyclades_ui.prefix') -USERDATA_PREFIX = get_path(cyclades_services, 'cyclades_userdata.prefix') -ADMIN_PREFIX = get_path(cyclades_services, 'cyclades_admin.prefix') + +COMPUTE_PREFIX = cyclades_services['cyclades_compute']['prefix'] +NETWORK_PREFIX = cyclades_services['cyclades_network']['prefix'] +VMAPI_PREFIX = cyclades_services['cyclades_vmapi']['prefix'] +PLANKTON_PREFIX = cyclades_services['cyclades_plankton']['prefix'] +HELPDESK_PREFIX = cyclades_services['cyclades_helpdesk']['prefix'] +UI_PREFIX = cyclades_services['cyclades_ui']['prefix'] +USERDATA_PREFIX = cyclades_services['cyclades_userdata']['prefix'] +ADMIN_PREFIX = cyclades_services['cyclades_admin']['prefix'] +VOLUME_PREFIX = cyclades_services['cyclades_volume']['prefix'] COMPUTE_ROOT_URL = join_urls(BASE_URL, COMPUTE_PREFIX) diff --git a/snf-cyclades-app/synnefo/db/__init__.py b/snf-cyclades-app/synnefo/db/__init__.py index 9397d35b8d7117256c84bc4dafecb5636865601e..36f261843fda4524f4f4dbce03391cbeba0bfdce 100644 --- a/snf-cyclades-app/synnefo/db/__init__.py +++ b/snf-cyclades-app/synnefo/db/__init__.py @@ -1,35 +1,17 @@ -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.db.backends.signals import connection_created diff --git a/snf-cyclades-app/synnefo/db/aes_encrypt.py b/snf-cyclades-app/synnefo/db/aes_encrypt.py index a3af3eb6421171207163be2ec5c33ff59ad438ad..bc0648bf935dbfbae867757c54362ee1f06d9864 100644 --- a/snf-cyclades-app/synnefo/db/aes_encrypt.py +++ b/snf-cyclades-app/synnefo/db/aes_encrypt.py @@ -1,31 +1,17 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from binascii import b2a_base64, a2b_base64 from Crypto.Cipher import AES diff --git a/snf-cyclades-app/synnefo/db/fields.py b/snf-cyclades-app/synnefo/db/fields.py index 1267559b87647c933940ebb6916bd9b253e5f693..63e0d7d642117d53d4c8d3a9fd9d757877be4321 100644 --- a/snf-cyclades-app/synnefo/db/fields.py +++ b/snf-cyclades-app/synnefo/db/fields.py @@ -1,31 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.db import models from south.modelsinspector import add_introspection_rules diff --git a/snf-cyclades-app/synnefo/db/fixtures/flavors.json b/snf-cyclades-app/synnefo/db/fixtures/flavors.json index 8de6a9ae90df5159e7b0afb61fb95b411e0ceb8b..b07030dd39c903b939d193426daff080b4cf49b3 100644 --- a/snf-cyclades-app/synnefo/db/fixtures/flavors.json +++ b/snf-cyclades-app/synnefo/db/fixtures/flavors.json @@ -1,4 +1,13 @@ [ + { + "model": "db.VolumeType", + "pk": 1, + "fields": { + "name": "drbd", + "disk_template": "drbd" + } + }, + { "model": "db.Flavor", "pk": 1, @@ -6,7 +15,7 @@ "cpu": 1, "ram": 1024, "disk": 20, - "disk_template": "drbd" + "volume_type": 1 } }, @@ -17,7 +26,7 @@ "cpu": 1, "ram": 1024, "disk": 30, - "disk_template": "drbd" + "volume_type": 1 } }, @@ -28,7 +37,7 @@ "cpu": 1, "ram": 1024, "disk": 40, - "disk_template": "drbd" + "volume_type": 1 } }, @@ -39,7 +48,7 @@ "cpu": 1, "ram": 2048, "disk": 20, - "disk_template": "drbd" + "volume_type": 1 } }, @@ -50,7 +59,7 @@ "cpu": 1, "ram": 2048, "disk": 30, - "disk_template": "drbd" + "volume_type": 1 } }, @@ -61,7 +70,7 @@ "cpu": 1, "ram": 2048, "disk": 40, - "disk_template": "drbd" + "volume_type": 1 } }, @@ -72,7 +81,7 @@ "cpu": 1, "ram": 4096, "disk": 20, - "disk_template": "drbd" + "volume_type": 1 } }, @@ -83,7 +92,7 @@ "cpu": 1, "ram": 4096, "disk": 30, - "disk_template": "drbd" + "volume_type": 1 } }, @@ -94,7 +103,7 @@ "cpu": 1, "ram": 4096, "disk": 40, - "disk_template": "drbd" + "volume_type": 1 } }, @@ -105,7 +114,7 @@ "cpu": 2, "ram": 1024, "disk": 20, - "disk_template": "drbd" + "volume_type": 1 } }, @@ -116,7 +125,7 @@ "cpu": 2, "ram": 1024, "disk": 30, - "disk_template": "drbd" + "volume_type": 1 } }, @@ -127,7 +136,7 @@ "cpu": 2, "ram": 1024, "disk": 40, - "disk_template": "drbd" + "volume_type": 1 } }, @@ -138,7 +147,7 @@ "cpu": 2, "ram": 2048, "disk": 20, - "disk_template": "drbd" + "volume_type": 1 } }, @@ -149,7 +158,7 @@ "cpu": 2, "ram": 2048, "disk": 30, - "disk_template": "drbd" + "volume_type": 1 } }, @@ -160,7 +169,7 @@ "cpu": 2, "ram": 2048, "disk": 40, - "disk_template": "drbd" + "volume_type": 1 } }, @@ -171,7 +180,7 @@ "cpu": 2, "ram": 4096, "disk": 20, - "disk_template": "drbd" + "volume_type": 1 } }, @@ -182,7 +191,7 @@ "cpu": 2, "ram": 4096, "disk": 30, - "disk_template": "drbd" + "volume_type": 1 } }, @@ -193,7 +202,7 @@ "cpu": 2, "ram": 4096, "disk": 40, - "disk_template": "drbd" + "volume_type": 1 } }, @@ -204,7 +213,7 @@ "cpu": 4, "ram": 1024, "disk": 20, - "disk_template": "drbd" + "volume_type": 1 } }, @@ -215,7 +224,7 @@ "cpu": 4, "ram": 1024, "disk": 30, - "disk_template": "drbd" + "volume_type": 1 } }, @@ -226,7 +235,7 @@ "cpu": 4, "ram": 1024, "disk": 40, - "disk_template": "drbd" + "volume_type": 1 } }, @@ -237,7 +246,7 @@ "cpu": 4, "ram": 2048, "disk": 20, - "disk_template": "drbd" + "volume_type": 1 } }, @@ -248,7 +257,7 @@ "cpu": 4, "ram": 2048, "disk": 30, - "disk_template": "drbd" + "volume_type": 1 } }, @@ -259,7 +268,7 @@ "cpu": 4, "ram": 2048, "disk": 40, - "disk_template": "drbd" + "volume_type": 1 } }, @@ -270,7 +279,7 @@ "cpu": 4, "ram": 4096, "disk": 20, - "disk_template": "drbd" + "volume_type": 1 } }, @@ -281,7 +290,7 @@ "cpu": 4, "ram": 4096, "disk": 30, - "disk_template": "drbd" + "volume_type": 1 } }, @@ -292,7 +301,7 @@ "cpu": 4, "ram": 4096, "disk": 40, - "disk_template": "drbd" + "volume_type": 1 } } ] diff --git a/snf-cyclades-app/synnefo/db/migrations/0099_auto__add_field_ipaddress_project__add_field_virtualmachine_project__a.py b/snf-cyclades-app/synnefo/db/migrations/0099_auto__add_field_ipaddress_project__add_field_virtualmachine_project__a.py new file mode 100644 index 0000000000000000000000000000000000000000..3fec8d06ce965064cab44db8072761dc94804a32 --- /dev/null +++ b/snf-cyclades-app/synnefo/db/migrations/0099_auto__add_field_ipaddress_project__add_field_virtualmachine_project__a.py @@ -0,0 +1,255 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'IPAddress.project' + db.add_column('db_ipaddress', 'project', + self.gf('django.db.models.fields.CharField')(max_length=255, null=True), + keep_default=False) + + # Adding field 'VirtualMachine.project' + db.add_column('db_virtualmachine', 'project', + self.gf('django.db.models.fields.CharField')(max_length=255, null=True), + keep_default=False) + + # Adding field 'Network.project' + db.add_column('db_network', 'project', + self.gf('django.db.models.fields.CharField')(max_length=255, null=True), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'IPAddress.project' + db.delete_column('db_ipaddress', 'project') + + # Deleting field 'VirtualMachine.project' + db.delete_column('db_virtualmachine', 'project') + + # Deleting field 'Network.project' + db.delete_column('db_network', 'project') + + + models = { + 'db.backend': { + 'Meta': {'ordering': "['clustername']", 'object_name': 'Backend'}, + 'clustername': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}), + 'ctotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'dfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'disk_templates': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'dtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'hash': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'hypervisor': ('django.db.models.fields.CharField', [], {'default': "'kvm'", 'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'unique': 'True'}), + 'mfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'mtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'offline': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'password_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), + 'pinst_cnt': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'port': ('django.db.models.fields.PositiveIntegerField', [], {'default': '5080'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}) + }, + 'db.backendnetwork': { + 'Meta': {'unique_together': "(('network', 'backend'),)", 'object_name': 'BackendNetwork'}, + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'backend_networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '30'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + 'db.bridgepooltable': { + 'Meta': {'object_name': 'BridgePoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.flavor': { + 'Meta': {'unique_together': "(('cpu', 'ram', 'disk', 'disk_template'),)", 'object_name': 'Flavor'}, + 'allow_create': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cpu': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'disk': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'disk_template': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ram': ('django.db.models.fields.IntegerField', [], {'default': '0'}) + }, + 'db.ipaddress': { + 'Meta': {'unique_together': "(('network', 'address', 'deleted'),)", 'object_name': 'IPAddress'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'floating_ip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'nic': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.NetworkInterface']"}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.ipaddresslog': { + 'Meta': {'object_name': 'IPAddressLog'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}), + 'allocated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'network_id': ('django.db.models.fields.IntegerField', [], {}), + 'released_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'server_id': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.ippooltable': { + 'Meta': {'object_name': 'IPPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ip_pools'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}) + }, + 'db.macprefixpooltable': { + 'Meta': {'object_name': 'MacPrefixPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.network': { + 'Meta': {'object_name': 'Network'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '32', 'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'external_router': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'flavor': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'floating_ip_pool': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'link': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'machines': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.VirtualMachine']", 'through': "orm['db.NetworkInterface']", 'symmetrical': 'False'}), + 'mode': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'network'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '32'}), + 'tags': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.networkinterface': { + 'Meta': {'object_name': 'NetworkInterface'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'device_owner': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'firewall_profile': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'mac': ('django.db.models.fields.CharField', [], {'max_length': '32', 'unique': 'True', 'null': 'True'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.VirtualMachine']"}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'security_groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.SecurityGroup']", 'null': 'True', 'symmetrical': 'False'}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'ACTIVE'", 'max_length': '32'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.quotaholderserial': { + 'Meta': {'ordering': "['serial']", 'object_name': 'QuotaHolderSerial'}, + 'accept': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pending': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'resolved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'serial': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_index': 'True'}) + }, + 'db.securitygroup': { + 'Meta': {'object_name': 'SecurityGroup'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'db.subnet': { + 'Meta': {'object_name': 'Subnet'}, + 'cidr': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'dhcp': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'dns_nameservers': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'gateway': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'host_routes': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'subnets'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.virtualmachine': { + 'Meta': {'object_name': 'VirtualMachine'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '30', 'null': 'True'}), + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machines'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backend_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'buildpercentage': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'flavor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.Flavor']", 'on_delete': 'models.PROTECT'}), + 'hostid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'imageid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'BUILD'", 'max_length': '30'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machine'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'suspended': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'task': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'task_job_id': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}) + }, + 'db.virtualmachinediagnostic': { + 'Meta': {'ordering': "['-created']", 'object_name': 'VirtualMachineDiagnostic'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'details': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'diagnostics'", 'to': "orm['db.VirtualMachine']"}), + 'message': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'source_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}) + }, + 'db.virtualmachinemetadata': { + 'Meta': {'unique_together': "(('meta_key', 'vm'),)", 'object_name': 'VirtualMachineMetadata'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'meta_key': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'meta_value': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'vm': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'metadata'", 'to': "orm['db.VirtualMachine']"}) + } + } + + complete_apps = ['db'] \ No newline at end of file diff --git a/snf-cyclades-app/synnefo/db/migrations/0100_project_ids.py b/snf-cyclades-app/synnefo/db/migrations/0100_project_ids.py new file mode 100644 index 0000000000000000000000000000000000000000..7a508b73c6070ef20884cd40ed591f5a8111c9ce --- /dev/null +++ b/snf-cyclades-app/synnefo/db/migrations/0100_project_ids.py @@ -0,0 +1,238 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + + def forwards(self, orm): + def upd(model): + model.objects.all().update(project=models.F('userid')) + + upd(orm.VirtualMachine) + upd(orm.Network) + upd(orm.IPAddress) + + def backwards(self, orm): + "Write your backwards methods here." + + models = { + 'db.backend': { + 'Meta': {'ordering': "['clustername']", 'object_name': 'Backend'}, + 'clustername': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}), + 'ctotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'dfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'disk_templates': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'dtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'hash': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'hypervisor': ('django.db.models.fields.CharField', [], {'default': "'kvm'", 'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'unique': 'True'}), + 'mfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'mtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'offline': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'password_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), + 'pinst_cnt': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'port': ('django.db.models.fields.PositiveIntegerField', [], {'default': '5080'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}) + }, + 'db.backendnetwork': { + 'Meta': {'unique_together': "(('network', 'backend'),)", 'object_name': 'BackendNetwork'}, + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'backend_networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '30'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + 'db.bridgepooltable': { + 'Meta': {'object_name': 'BridgePoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.flavor': { + 'Meta': {'unique_together': "(('cpu', 'ram', 'disk', 'disk_template'),)", 'object_name': 'Flavor'}, + 'allow_create': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cpu': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'disk': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'disk_template': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ram': ('django.db.models.fields.IntegerField', [], {'default': '0'}) + }, + 'db.ipaddress': { + 'Meta': {'unique_together': "(('network', 'address', 'deleted'),)", 'object_name': 'IPAddress'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'floating_ip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'nic': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.NetworkInterface']"}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.ipaddresslog': { + 'Meta': {'object_name': 'IPAddressLog'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}), + 'allocated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'network_id': ('django.db.models.fields.IntegerField', [], {}), + 'released_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'server_id': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.ippooltable': { + 'Meta': {'object_name': 'IPPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ip_pools'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}) + }, + 'db.macprefixpooltable': { + 'Meta': {'object_name': 'MacPrefixPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.network': { + 'Meta': {'object_name': 'Network'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '32', 'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'external_router': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'flavor': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'floating_ip_pool': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'link': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'machines': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.VirtualMachine']", 'through': "orm['db.NetworkInterface']", 'symmetrical': 'False'}), + 'mode': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'network'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '32'}), + 'tags': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.networkinterface': { + 'Meta': {'object_name': 'NetworkInterface'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'device_owner': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'firewall_profile': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'mac': ('django.db.models.fields.CharField', [], {'max_length': '32', 'unique': 'True', 'null': 'True'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.VirtualMachine']"}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'security_groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.SecurityGroup']", 'null': 'True', 'symmetrical': 'False'}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'ACTIVE'", 'max_length': '32'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.quotaholderserial': { + 'Meta': {'ordering': "['serial']", 'object_name': 'QuotaHolderSerial'}, + 'accept': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pending': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'resolved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'serial': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_index': 'True'}) + }, + 'db.securitygroup': { + 'Meta': {'object_name': 'SecurityGroup'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'db.subnet': { + 'Meta': {'object_name': 'Subnet'}, + 'cidr': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'dhcp': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'dns_nameservers': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'gateway': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'host_routes': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'subnets'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.virtualmachine': { + 'Meta': {'object_name': 'VirtualMachine'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '30', 'null': 'True'}), + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machines'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backend_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'buildpercentage': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'flavor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.Flavor']", 'on_delete': 'models.PROTECT'}), + 'hostid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'imageid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'BUILD'", 'max_length': '30'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machine'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'suspended': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'task': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'task_job_id': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}) + }, + 'db.virtualmachinediagnostic': { + 'Meta': {'ordering': "['-created']", 'object_name': 'VirtualMachineDiagnostic'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'details': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'diagnostics'", 'to': "orm['db.VirtualMachine']"}), + 'message': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'source_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}) + }, + 'db.virtualmachinemetadata': { + 'Meta': {'unique_together': "(('meta_key', 'vm'),)", 'object_name': 'VirtualMachineMetadata'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'meta_key': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'meta_value': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'vm': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'metadata'", 'to': "orm['db.VirtualMachine']"}) + } + } + + complete_apps = ['db'] + symmetrical = True diff --git a/snf-cyclades-app/synnefo/db/migrations/0101_auto__add_field_network_subnet_ids.py b/snf-cyclades-app/synnefo/db/migrations/0101_auto__add_field_network_subnet_ids.py new file mode 100644 index 0000000000000000000000000000000000000000..ff5227fed00a5d0d7be389c5a3e81f09f95184ce --- /dev/null +++ b/snf-cyclades-app/synnefo/db/migrations/0101_auto__add_field_network_subnet_ids.py @@ -0,0 +1,240 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'Network.subnet_ids' + db.add_column('db_network', 'subnet_ids', + self.gf('synnefo.db.fields.SeparatedValuesField')(null=True), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'Network.subnet_ids' + db.delete_column('db_network', 'subnet_ids') + + + models = { + 'db.backend': { + 'Meta': {'ordering': "['clustername']", 'object_name': 'Backend'}, + 'clustername': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}), + 'ctotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'dfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'disk_templates': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'dtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'hash': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'hypervisor': ('django.db.models.fields.CharField', [], {'default': "'kvm'", 'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'unique': 'True'}), + 'mfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'mtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'offline': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'password_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), + 'pinst_cnt': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'port': ('django.db.models.fields.PositiveIntegerField', [], {'default': '5080'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}) + }, + 'db.backendnetwork': { + 'Meta': {'unique_together': "(('network', 'backend'),)", 'object_name': 'BackendNetwork'}, + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'backend_networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '30'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + 'db.bridgepooltable': { + 'Meta': {'object_name': 'BridgePoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.flavor': { + 'Meta': {'unique_together': "(('cpu', 'ram', 'disk', 'disk_template'),)", 'object_name': 'Flavor'}, + 'allow_create': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cpu': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'disk': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'disk_template': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ram': ('django.db.models.fields.IntegerField', [], {'default': '0'}) + }, + 'db.ipaddress': { + 'Meta': {'unique_together': "(('network', 'address', 'deleted'),)", 'object_name': 'IPAddress'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'floating_ip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'nic': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.NetworkInterface']"}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.ipaddresslog': { + 'Meta': {'object_name': 'IPAddressLog'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}), + 'allocated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'network_id': ('django.db.models.fields.IntegerField', [], {}), + 'released_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'server_id': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.ippooltable': { + 'Meta': {'object_name': 'IPPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ip_pools'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}) + }, + 'db.macprefixpooltable': { + 'Meta': {'object_name': 'MacPrefixPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.network': { + 'Meta': {'object_name': 'Network'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '32', 'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'external_router': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'flavor': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'floating_ip_pool': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'link': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'machines': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.VirtualMachine']", 'through': "orm['db.NetworkInterface']", 'symmetrical': 'False'}), + 'mode': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'network'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '32'}), + 'subnet_ids': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'tags': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.networkinterface': { + 'Meta': {'object_name': 'NetworkInterface'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'device_owner': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'firewall_profile': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'mac': ('django.db.models.fields.CharField', [], {'max_length': '32', 'unique': 'True', 'null': 'True'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.VirtualMachine']"}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'security_groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.SecurityGroup']", 'null': 'True', 'symmetrical': 'False'}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'ACTIVE'", 'max_length': '32'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.quotaholderserial': { + 'Meta': {'ordering': "['serial']", 'object_name': 'QuotaHolderSerial'}, + 'accept': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pending': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'resolved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'serial': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_index': 'True'}) + }, + 'db.securitygroup': { + 'Meta': {'object_name': 'SecurityGroup'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'db.subnet': { + 'Meta': {'object_name': 'Subnet'}, + 'cidr': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'dhcp': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'dns_nameservers': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'gateway': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'host_routes': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'subnets'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.virtualmachine': { + 'Meta': {'object_name': 'VirtualMachine'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '30', 'null': 'True'}), + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machines'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backend_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'buildpercentage': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'flavor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.Flavor']", 'on_delete': 'models.PROTECT'}), + 'hostid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'imageid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'BUILD'", 'max_length': '30'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machine'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'suspended': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'task': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'task_job_id': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}) + }, + 'db.virtualmachinediagnostic': { + 'Meta': {'ordering': "['-created']", 'object_name': 'VirtualMachineDiagnostic'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'details': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'diagnostics'", 'to': "orm['db.VirtualMachine']"}), + 'message': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'source_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}) + }, + 'db.virtualmachinemetadata': { + 'Meta': {'unique_together': "(('meta_key', 'vm'),)", 'object_name': 'VirtualMachineMetadata'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'meta_key': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'meta_value': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'vm': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'metadata'", 'to': "orm['db.VirtualMachine']"}) + } + } + + complete_apps = ['db'] diff --git a/snf-cyclades-app/synnefo/db/migrations/0102_subnets_to_network.py b/snf-cyclades-app/synnefo/db/migrations/0102_subnets_to_network.py new file mode 100644 index 0000000000000000000000000000000000000000..6c4869c64dcb26120b4f90e81f12bd87767a0d91 --- /dev/null +++ b/snf-cyclades-app/synnefo/db/migrations/0102_subnets_to_network.py @@ -0,0 +1,242 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + + def forwards(self, orm): + "Write your forwards methods here." + n_subs = {} + for (subnet_id, network_id) in orm.Subnet.objects.values_list("id", "network_id"): + subnet_ids = n_subs.setdefault(network_id, []) + subnet_ids.append(subnet_id) + for network_id, subnet_ids in n_subs.items(): + updated = orm.Network.objects.filter(id=network_id).update(subnet_ids=subnet_ids) + assert(updated == 1) + # Note: Remember to use orm['appname.ModelName'] rather than "from appname.models..." + + def backwards(self, orm): + "Write your backwards methods here." + + models = { + 'db.backend': { + 'Meta': {'ordering': "['clustername']", 'object_name': 'Backend'}, + 'clustername': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}), + 'ctotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'dfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'disk_templates': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'dtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'hash': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'hypervisor': ('django.db.models.fields.CharField', [], {'default': "'kvm'", 'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'unique': 'True'}), + 'mfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'mtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'offline': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'password_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), + 'pinst_cnt': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'port': ('django.db.models.fields.PositiveIntegerField', [], {'default': '5080'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}) + }, + 'db.backendnetwork': { + 'Meta': {'unique_together': "(('network', 'backend'),)", 'object_name': 'BackendNetwork'}, + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'backend_networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '30'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + 'db.bridgepooltable': { + 'Meta': {'object_name': 'BridgePoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.flavor': { + 'Meta': {'unique_together': "(('cpu', 'ram', 'disk', 'disk_template'),)", 'object_name': 'Flavor'}, + 'allow_create': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cpu': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'disk': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'disk_template': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ram': ('django.db.models.fields.IntegerField', [], {'default': '0'}) + }, + 'db.ipaddress': { + 'Meta': {'unique_together': "(('network', 'address', 'deleted'),)", 'object_name': 'IPAddress'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'floating_ip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'nic': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.NetworkInterface']"}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.ipaddresslog': { + 'Meta': {'object_name': 'IPAddressLog'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}), + 'allocated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'network_id': ('django.db.models.fields.IntegerField', [], {}), + 'released_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'server_id': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.ippooltable': { + 'Meta': {'object_name': 'IPPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ip_pools'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}) + }, + 'db.macprefixpooltable': { + 'Meta': {'object_name': 'MacPrefixPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.network': { + 'Meta': {'object_name': 'Network'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '32', 'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'external_router': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'flavor': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'floating_ip_pool': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'link': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'machines': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.VirtualMachine']", 'through': "orm['db.NetworkInterface']", 'symmetrical': 'False'}), + 'mode': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'network'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '32'}), + 'subnet_ids': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'tags': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.networkinterface': { + 'Meta': {'object_name': 'NetworkInterface'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'device_owner': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'firewall_profile': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'mac': ('django.db.models.fields.CharField', [], {'max_length': '32', 'unique': 'True', 'null': 'True'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.VirtualMachine']"}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'security_groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.SecurityGroup']", 'null': 'True', 'symmetrical': 'False'}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'ACTIVE'", 'max_length': '32'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.quotaholderserial': { + 'Meta': {'ordering': "['serial']", 'object_name': 'QuotaHolderSerial'}, + 'accept': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pending': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'resolved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'serial': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_index': 'True'}) + }, + 'db.securitygroup': { + 'Meta': {'object_name': 'SecurityGroup'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'db.subnet': { + 'Meta': {'object_name': 'Subnet'}, + 'cidr': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'dhcp': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'dns_nameservers': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'gateway': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'host_routes': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'subnets'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.virtualmachine': { + 'Meta': {'object_name': 'VirtualMachine'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '30', 'null': 'True'}), + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machines'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backend_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'buildpercentage': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'flavor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.Flavor']", 'on_delete': 'models.PROTECT'}), + 'hostid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'imageid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'BUILD'", 'max_length': '30'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machine'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'suspended': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'task': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'task_job_id': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}) + }, + 'db.virtualmachinediagnostic': { + 'Meta': {'ordering': "['-created']", 'object_name': 'VirtualMachineDiagnostic'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'details': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'diagnostics'", 'to': "orm['db.VirtualMachine']"}), + 'message': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'source_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}) + }, + 'db.virtualmachinemetadata': { + 'Meta': {'unique_together': "(('meta_key', 'vm'),)", 'object_name': 'VirtualMachineMetadata'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'meta_key': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'meta_value': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'vm': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'metadata'", 'to': "orm['db.VirtualMachine']"}) + } + } + + complete_apps = ['db'] + symmetrical = True diff --git a/snf-cyclades-app/synnefo/db/migrations/0103_auto__add_field_networkinterface_public.py b/snf-cyclades-app/synnefo/db/migrations/0103_auto__add_field_networkinterface_public.py new file mode 100644 index 0000000000000000000000000000000000000000..fb238ecefa69ca6bef1a1c4cee57ed815445299d --- /dev/null +++ b/snf-cyclades-app/synnefo/db/migrations/0103_auto__add_field_networkinterface_public.py @@ -0,0 +1,241 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'NetworkInterface.public' + db.add_column('db_networkinterface', 'public', + self.gf('django.db.models.fields.BooleanField')(default=False), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'NetworkInterface.public' + db.delete_column('db_networkinterface', 'public') + + + models = { + 'db.backend': { + 'Meta': {'ordering': "['clustername']", 'object_name': 'Backend'}, + 'clustername': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}), + 'ctotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'dfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'disk_templates': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'dtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'hash': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'hypervisor': ('django.db.models.fields.CharField', [], {'default': "'kvm'", 'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'unique': 'True'}), + 'mfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'mtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'offline': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'password_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), + 'pinst_cnt': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'port': ('django.db.models.fields.PositiveIntegerField', [], {'default': '5080'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}) + }, + 'db.backendnetwork': { + 'Meta': {'unique_together': "(('network', 'backend'),)", 'object_name': 'BackendNetwork'}, + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'backend_networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '30'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + 'db.bridgepooltable': { + 'Meta': {'object_name': 'BridgePoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.flavor': { + 'Meta': {'unique_together': "(('cpu', 'ram', 'disk', 'disk_template'),)", 'object_name': 'Flavor'}, + 'allow_create': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cpu': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'disk': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'disk_template': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ram': ('django.db.models.fields.IntegerField', [], {'default': '0'}) + }, + 'db.ipaddress': { + 'Meta': {'unique_together': "(('network', 'address', 'deleted'),)", 'object_name': 'IPAddress'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'floating_ip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'nic': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.NetworkInterface']"}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.ipaddresslog': { + 'Meta': {'object_name': 'IPAddressLog'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}), + 'allocated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'network_id': ('django.db.models.fields.IntegerField', [], {}), + 'released_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'server_id': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.ippooltable': { + 'Meta': {'object_name': 'IPPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ip_pools'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}) + }, + 'db.macprefixpooltable': { + 'Meta': {'object_name': 'MacPrefixPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.network': { + 'Meta': {'object_name': 'Network'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '32', 'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'external_router': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'flavor': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'floating_ip_pool': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'link': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'machines': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.VirtualMachine']", 'through': "orm['db.NetworkInterface']", 'symmetrical': 'False'}), + 'mode': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'network'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '32'}), + 'subnet_ids': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'tags': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.networkinterface': { + 'Meta': {'object_name': 'NetworkInterface'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'device_owner': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'firewall_profile': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'mac': ('django.db.models.fields.CharField', [], {'max_length': '32', 'unique': 'True', 'null': 'True'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.VirtualMachine']"}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'security_groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.SecurityGroup']", 'null': 'True', 'symmetrical': 'False'}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'ACTIVE'", 'max_length': '32'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.quotaholderserial': { + 'Meta': {'ordering': "['serial']", 'object_name': 'QuotaHolderSerial'}, + 'accept': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pending': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'resolved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'serial': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_index': 'True'}) + }, + 'db.securitygroup': { + 'Meta': {'object_name': 'SecurityGroup'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'db.subnet': { + 'Meta': {'object_name': 'Subnet'}, + 'cidr': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'dhcp': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'dns_nameservers': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'gateway': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'host_routes': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'subnets'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.virtualmachine': { + 'Meta': {'object_name': 'VirtualMachine'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '30', 'null': 'True'}), + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machines'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backend_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'buildpercentage': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'flavor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.Flavor']", 'on_delete': 'models.PROTECT'}), + 'hostid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'imageid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'BUILD'", 'max_length': '30'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machine'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'suspended': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'task': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'task_job_id': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}) + }, + 'db.virtualmachinediagnostic': { + 'Meta': {'ordering': "['-created']", 'object_name': 'VirtualMachineDiagnostic'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'details': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'diagnostics'", 'to': "orm['db.VirtualMachine']"}), + 'message': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'source_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}) + }, + 'db.virtualmachinemetadata': { + 'Meta': {'unique_together': "(('meta_key', 'vm'),)", 'object_name': 'VirtualMachineMetadata'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'meta_key': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'meta_value': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'vm': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'metadata'", 'to': "orm['db.VirtualMachine']"}) + } + } + + complete_apps = ['db'] \ No newline at end of file diff --git a/snf-cyclades-app/synnefo/db/migrations/0104_public_to_nics.py b/snf-cyclades-app/synnefo/db/migrations/0104_public_to_nics.py new file mode 100644 index 0000000000000000000000000000000000000000..478c98c4acaf6dad5cd1fc3d5a24cb18aa198b4f --- /dev/null +++ b/snf-cyclades-app/synnefo/db/migrations/0104_public_to_nics.py @@ -0,0 +1,237 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + + def forwards(self, orm): + "Write your forwards methods here." + # Note: Remember to use orm['appname.ModelName'] rather than "from appname.models..." + orm.NetworkInterface.objects.filter(network__public=True).update(public=True) + + def backwards(self, orm): + "Write your backwards methods here." + + models = { + 'db.backend': { + 'Meta': {'ordering': "['clustername']", 'object_name': 'Backend'}, + 'clustername': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}), + 'ctotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'dfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'disk_templates': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'dtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'hash': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'hypervisor': ('django.db.models.fields.CharField', [], {'default': "'kvm'", 'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'unique': 'True'}), + 'mfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'mtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'offline': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'password_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), + 'pinst_cnt': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'port': ('django.db.models.fields.PositiveIntegerField', [], {'default': '5080'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}) + }, + 'db.backendnetwork': { + 'Meta': {'unique_together': "(('network', 'backend'),)", 'object_name': 'BackendNetwork'}, + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'backend_networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '30'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + 'db.bridgepooltable': { + 'Meta': {'object_name': 'BridgePoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.flavor': { + 'Meta': {'unique_together': "(('cpu', 'ram', 'disk', 'disk_template'),)", 'object_name': 'Flavor'}, + 'allow_create': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cpu': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'disk': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'disk_template': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ram': ('django.db.models.fields.IntegerField', [], {'default': '0'}) + }, + 'db.ipaddress': { + 'Meta': {'unique_together': "(('network', 'address', 'deleted'),)", 'object_name': 'IPAddress'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'floating_ip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'nic': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.NetworkInterface']"}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.ipaddresslog': { + 'Meta': {'object_name': 'IPAddressLog'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}), + 'allocated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'network_id': ('django.db.models.fields.IntegerField', [], {}), + 'released_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'server_id': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.ippooltable': { + 'Meta': {'object_name': 'IPPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ip_pools'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}) + }, + 'db.macprefixpooltable': { + 'Meta': {'object_name': 'MacPrefixPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.network': { + 'Meta': {'object_name': 'Network'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '32', 'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'external_router': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'flavor': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'floating_ip_pool': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'link': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'machines': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.VirtualMachine']", 'through': "orm['db.NetworkInterface']", 'symmetrical': 'False'}), + 'mode': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'network'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '32'}), + 'subnet_ids': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'tags': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.networkinterface': { + 'Meta': {'object_name': 'NetworkInterface'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'device_owner': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'firewall_profile': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'mac': ('django.db.models.fields.CharField', [], {'max_length': '32', 'unique': 'True', 'null': 'True'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.VirtualMachine']"}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'security_groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.SecurityGroup']", 'null': 'True', 'symmetrical': 'False'}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'ACTIVE'", 'max_length': '32'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.quotaholderserial': { + 'Meta': {'ordering': "['serial']", 'object_name': 'QuotaHolderSerial'}, + 'accept': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pending': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'resolved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'serial': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_index': 'True'}) + }, + 'db.securitygroup': { + 'Meta': {'object_name': 'SecurityGroup'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'db.subnet': { + 'Meta': {'object_name': 'Subnet'}, + 'cidr': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'dhcp': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'dns_nameservers': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'gateway': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'host_routes': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'subnets'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.virtualmachine': { + 'Meta': {'object_name': 'VirtualMachine'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '30', 'null': 'True'}), + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machines'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backend_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'buildpercentage': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'flavor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.Flavor']", 'on_delete': 'models.PROTECT'}), + 'hostid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'imageid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'BUILD'", 'max_length': '30'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machine'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'suspended': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'task': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'task_job_id': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}) + }, + 'db.virtualmachinediagnostic': { + 'Meta': {'ordering': "['-created']", 'object_name': 'VirtualMachineDiagnostic'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'details': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'diagnostics'", 'to': "orm['db.VirtualMachine']"}), + 'message': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'source_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}) + }, + 'db.virtualmachinemetadata': { + 'Meta': {'unique_together': "(('meta_key', 'vm'),)", 'object_name': 'VirtualMachineMetadata'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'meta_key': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'meta_value': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'vm': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'metadata'", 'to': "orm['db.VirtualMachine']"}) + } + } + + complete_apps = ['db'] + symmetrical = True diff --git a/snf-cyclades-app/synnefo/db/migrations/0105_auto__add_volume__add_volumemetadata__add_unique_volumemetadata_volume.py b/snf-cyclades-app/synnefo/db/migrations/0105_auto__add_volume__add_volumemetadata__add_unique_volumemetadata_volume.py new file mode 100644 index 0000000000000000000000000000000000000000..e17cda53d306d4ea08ec4079b87949d8e16e1534 --- /dev/null +++ b/snf-cyclades-app/synnefo/db/migrations/0105_auto__add_volume__add_volumemetadata__add_unique_volumemetadata_volume.py @@ -0,0 +1,303 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'Volume' + db.create_table('db_volume', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=255, null=True)), + ('description', self.gf('django.db.models.fields.CharField')(max_length=255, null=True)), + ('userid', self.gf('django.db.models.fields.CharField')(max_length=100, db_index=True)), + ('size', self.gf('django.db.models.fields.IntegerField')()), + ('disk_template', self.gf('django.db.models.fields.CharField')(max_length=32)), + ('delete_on_termination', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('source', self.gf('django.db.models.fields.CharField')(max_length=128, null=True)), + ('origin', self.gf('django.db.models.fields.CharField')(max_length=128, null=True)), + ('deleted', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + ('updated', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), + ('status', self.gf('django.db.models.fields.CharField')(default='CREATING', max_length=64)), + ('snapshot_counter', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)), + ('machine', self.gf('django.db.models.fields.related.ForeignKey')(related_name='volumes', null=True, to=orm['db.VirtualMachine'])), + ('index', self.gf('django.db.models.fields.IntegerField')(null=True)), + ('backendjobid', self.gf('django.db.models.fields.PositiveIntegerField')(null=True)), + )) + db.send_create_signal('db', ['Volume']) + + # Adding model 'VolumeMetadata' + db.create_table('db_volumemetadata', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('key', self.gf('django.db.models.fields.CharField')(max_length=64)), + ('value', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('volume', self.gf('django.db.models.fields.related.ForeignKey')(related_name='metadata', to=orm['db.Volume'])), + )) + db.send_create_signal('db', ['VolumeMetadata']) + + # Adding unique constraint on 'VolumeMetadata', fields ['volume', 'key'] + db.create_unique('db_volumemetadata', ['volume_id', 'key']) + + + def backwards(self, orm): + # Removing unique constraint on 'VolumeMetadata', fields ['volume', 'key'] + db.delete_unique('db_volumemetadata', ['volume_id', 'key']) + + # Deleting model 'Volume' + db.delete_table('db_volume') + + # Deleting model 'VolumeMetadata' + db.delete_table('db_volumemetadata') + + + models = { + 'db.backend': { + 'Meta': {'ordering': "['clustername']", 'object_name': 'Backend'}, + 'clustername': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}), + 'ctotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'dfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'disk_templates': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'dtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'hash': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'hypervisor': ('django.db.models.fields.CharField', [], {'default': "'kvm'", 'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'unique': 'True'}), + 'mfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'mtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'offline': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'password_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), + 'pinst_cnt': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'port': ('django.db.models.fields.PositiveIntegerField', [], {'default': '5080'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}) + }, + 'db.backendnetwork': { + 'Meta': {'unique_together': "(('network', 'backend'),)", 'object_name': 'BackendNetwork'}, + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'backend_networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '30'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + 'db.bridgepooltable': { + 'Meta': {'object_name': 'BridgePoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.flavor': { + 'Meta': {'unique_together': "(('cpu', 'ram', 'disk', 'disk_template'),)", 'object_name': 'Flavor'}, + 'allow_create': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cpu': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'disk': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'disk_template': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ram': ('django.db.models.fields.IntegerField', [], {'default': '0'}) + }, + 'db.ipaddress': { + 'Meta': {'unique_together': "(('network', 'address', 'deleted'),)", 'object_name': 'IPAddress'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'floating_ip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'nic': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.NetworkInterface']"}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.ipaddresslog': { + 'Meta': {'object_name': 'IPAddressLog'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}), + 'allocated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'network_id': ('django.db.models.fields.IntegerField', [], {}), + 'released_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'server_id': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.ippooltable': { + 'Meta': {'object_name': 'IPPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ip_pools'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}) + }, + 'db.macprefixpooltable': { + 'Meta': {'object_name': 'MacPrefixPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.network': { + 'Meta': {'object_name': 'Network'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '32', 'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'external_router': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'flavor': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'floating_ip_pool': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'link': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'machines': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.VirtualMachine']", 'through': "orm['db.NetworkInterface']", 'symmetrical': 'False'}), + 'mode': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'network'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '32'}), + 'subnet_ids': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'tags': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.networkinterface': { + 'Meta': {'object_name': 'NetworkInterface'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'device_owner': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'firewall_profile': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'mac': ('django.db.models.fields.CharField', [], {'max_length': '32', 'unique': 'True', 'null': 'True'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.VirtualMachine']"}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'security_groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.SecurityGroup']", 'null': 'True', 'symmetrical': 'False'}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'ACTIVE'", 'max_length': '32'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.quotaholderserial': { + 'Meta': {'ordering': "['serial']", 'object_name': 'QuotaHolderSerial'}, + 'accept': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pending': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'resolved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'serial': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_index': 'True'}) + }, + 'db.securitygroup': { + 'Meta': {'object_name': 'SecurityGroup'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'db.subnet': { + 'Meta': {'object_name': 'Subnet'}, + 'cidr': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'dhcp': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'dns_nameservers': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'gateway': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'host_routes': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'subnets'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.virtualmachine': { + 'Meta': {'object_name': 'VirtualMachine'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '30', 'null': 'True'}), + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machines'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backend_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'buildpercentage': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'flavor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.Flavor']", 'on_delete': 'models.PROTECT'}), + 'hostid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'imageid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'BUILD'", 'max_length': '30'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machine'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'suspended': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'task': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'task_job_id': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}) + }, + 'db.virtualmachinediagnostic': { + 'Meta': {'ordering': "['-created']", 'object_name': 'VirtualMachineDiagnostic'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'details': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'diagnostics'", 'to': "orm['db.VirtualMachine']"}), + 'message': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'source_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}) + }, + 'db.virtualmachinemetadata': { + 'Meta': {'unique_together': "(('meta_key', 'vm'),)", 'object_name': 'VirtualMachineMetadata'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'meta_key': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'meta_value': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'vm': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'metadata'", 'to': "orm['db.VirtualMachine']"}) + }, + 'db.volume': { + 'Meta': {'object_name': 'Volume'}, + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'delete_on_termination': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'disk_template': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'volumes'", 'null': 'True', 'to': "orm['db.VirtualMachine']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'origin': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'snapshot_counter': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'CREATING'", 'max_length': '64'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}) + }, + 'db.volumemetadata': { + 'Meta': {'unique_together': "(('volume', 'key'),)", 'object_name': 'VolumeMetadata'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'volume': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'metadata'", 'to': "orm['db.Volume']"}) + } + } + + complete_apps = ['db'] diff --git a/snf-cyclades-app/synnefo/db/migrations/0106_default_volume.py b/snf-cyclades-app/synnefo/db/migrations/0106_default_volume.py new file mode 100644 index 0000000000000000000000000000000000000000..d31146ca486908ff4c0f06c3f6118bff976c30f3 --- /dev/null +++ b/snf-cyclades-app/synnefo/db/migrations/0106_default_volume.py @@ -0,0 +1,280 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + + def forwards(self, orm): + "Write your forwards methods here." + # Note: Remember to use orm['appname.ModelName'] rather than "from appname.models..." + vms = orm.VirtualMachine.objects.filter(deleted=False)\ + .values("id", "userid", + "flavor__disk", + "flavor__disk_template") + volumes = [orm.Volume(machine_id=vm["id"], + name="boot volume", + description="boot volume", + userid=vm["userid"], + index=0, + size=vm["flavor__disk"], + disk_template=vm["flavor__disk_template"], + delete_on_termination=True, + deleted=False, + status="IN_USE") + for vm in vms] + orm.Volume.objects.bulk_create(volumes) + + def backwards(self, orm): + "Write your backwards methods here." + orm.Volume.objects.filter(index=0).delete() + + models = { + 'db.backend': { + 'Meta': {'ordering': "['clustername']", 'object_name': 'Backend'}, + 'clustername': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}), + 'ctotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'dfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'disk_templates': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'dtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'hash': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'hypervisor': ('django.db.models.fields.CharField', [], {'default': "'kvm'", 'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'unique': 'True'}), + 'mfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'mtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'offline': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'password_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), + 'pinst_cnt': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'port': ('django.db.models.fields.PositiveIntegerField', [], {'default': '5080'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}) + }, + 'db.backendnetwork': { + 'Meta': {'unique_together': "(('network', 'backend'),)", 'object_name': 'BackendNetwork'}, + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'backend_networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '30'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + 'db.bridgepooltable': { + 'Meta': {'object_name': 'BridgePoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.flavor': { + 'Meta': {'unique_together': "(('cpu', 'ram', 'disk', 'disk_template'),)", 'object_name': 'Flavor'}, + 'allow_create': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cpu': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'disk': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'disk_template': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ram': ('django.db.models.fields.IntegerField', [], {'default': '0'}) + }, + 'db.ipaddress': { + 'Meta': {'unique_together': "(('network', 'address', 'deleted'),)", 'object_name': 'IPAddress'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'floating_ip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'nic': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.NetworkInterface']"}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.ipaddresslog': { + 'Meta': {'object_name': 'IPAddressLog'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}), + 'allocated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'network_id': ('django.db.models.fields.IntegerField', [], {}), + 'released_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'server_id': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.ippooltable': { + 'Meta': {'object_name': 'IPPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ip_pools'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}) + }, + 'db.macprefixpooltable': { + 'Meta': {'object_name': 'MacPrefixPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.network': { + 'Meta': {'object_name': 'Network'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '32', 'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'external_router': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'flavor': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'floating_ip_pool': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'link': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'machines': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.VirtualMachine']", 'through': "orm['db.NetworkInterface']", 'symmetrical': 'False'}), + 'mode': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'network'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '32'}), + 'subnet_ids': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'tags': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.networkinterface': { + 'Meta': {'object_name': 'NetworkInterface'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'device_owner': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'firewall_profile': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'mac': ('django.db.models.fields.CharField', [], {'max_length': '32', 'unique': 'True', 'null': 'True'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.VirtualMachine']"}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'security_groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.SecurityGroup']", 'null': 'True', 'symmetrical': 'False'}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'ACTIVE'", 'max_length': '32'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.quotaholderserial': { + 'Meta': {'ordering': "['serial']", 'object_name': 'QuotaHolderSerial'}, + 'accept': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pending': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'resolved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'serial': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_index': 'True'}) + }, + 'db.securitygroup': { + 'Meta': {'object_name': 'SecurityGroup'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'db.subnet': { + 'Meta': {'object_name': 'Subnet'}, + 'cidr': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'dhcp': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'dns_nameservers': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'gateway': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'host_routes': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'subnets'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.virtualmachine': { + 'Meta': {'object_name': 'VirtualMachine'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '30', 'null': 'True'}), + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machines'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backend_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'buildpercentage': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'flavor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.Flavor']", 'on_delete': 'models.PROTECT'}), + 'hostid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'imageid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'BUILD'", 'max_length': '30'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machine'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'suspended': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'task': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'task_job_id': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}) + }, + 'db.virtualmachinediagnostic': { + 'Meta': {'ordering': "['-created']", 'object_name': 'VirtualMachineDiagnostic'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'details': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'diagnostics'", 'to': "orm['db.VirtualMachine']"}), + 'message': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'source_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}) + }, + 'db.virtualmachinemetadata': { + 'Meta': {'unique_together': "(('meta_key', 'vm'),)", 'object_name': 'VirtualMachineMetadata'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'meta_key': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'meta_value': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'vm': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'metadata'", 'to': "orm['db.VirtualMachine']"}) + }, + 'db.volume': { + 'Meta': {'object_name': 'Volume'}, + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'delete_on_termination': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'disk_template': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'volumes'", 'null': 'True', 'to': "orm['db.VirtualMachine']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'origin': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'snapshot_counter': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'CREATING'", 'max_length': '64'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}) + }, + 'db.volumemetadata': { + 'Meta': {'unique_together': "(('volume', 'key'),)", 'object_name': 'VolumeMetadata'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'volume': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'metadata'", 'to': "orm['db.Volume']"}) + } + } + + complete_apps = ['db'] + symmetrical = True diff --git a/snf-cyclades-app/synnefo/db/migrations/0107_auto__add_volumetype__add_field_volume_volume_type__add_field_flavor_v.py b/snf-cyclades-app/synnefo/db/migrations/0107_auto__add_volumetype__add_field_volume_volume_type__add_field_flavor_v.py new file mode 100644 index 0000000000000000000000000000000000000000..271e2dd80322f1dcfefa3c56ab0a47b9bbf8882a --- /dev/null +++ b/snf-cyclades-app/synnefo/db/migrations/0107_auto__add_volumetype__add_field_volume_volume_type__add_field_flavor_v.py @@ -0,0 +1,297 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'VolumeType' + db.create_table('db_volumetype', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('disk_template', self.gf('django.db.models.fields.CharField')(max_length=32)), + ('deleted', self.gf('django.db.models.fields.BooleanField')(default=False)), + )) + db.send_create_signal('db', ['VolumeType']) + + # Adding field 'Volume.volume_type' + db.add_column('db_volume', 'volume_type', + self.gf('django.db.models.fields.related.ForeignKey')(related_name='volumes', null=True, on_delete=models.PROTECT, to=orm['db.VolumeType']), + keep_default=False) + + # Adding field 'Flavor.volume_type' + db.add_column('db_flavor', 'volume_type', + self.gf('django.db.models.fields.related.ForeignKey')(related_name='flavors', null=True, on_delete=models.PROTECT, to=orm['db.VolumeType']), + keep_default=False) + + + def backwards(self, orm): + # Deleting model 'VolumeType' + db.delete_table('db_volumetype') + + # Deleting field 'Volume.volume_type' + db.delete_column('db_volume', 'volume_type_id') + + # Deleting field 'Flavor.volume_type' + db.delete_column('db_flavor', 'volume_type_id') + + + models = { + 'db.backend': { + 'Meta': {'ordering': "['clustername']", 'object_name': 'Backend'}, + 'clustername': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}), + 'ctotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'dfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'disk_templates': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'dtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'hash': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'hypervisor': ('django.db.models.fields.CharField', [], {'default': "'kvm'", 'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'unique': 'True'}), + 'mfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'mtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'offline': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'password_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), + 'pinst_cnt': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'port': ('django.db.models.fields.PositiveIntegerField', [], {'default': '5080'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}) + }, + 'db.backendnetwork': { + 'Meta': {'unique_together': "(('network', 'backend'),)", 'object_name': 'BackendNetwork'}, + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'backend_networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '30'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + 'db.bridgepooltable': { + 'Meta': {'object_name': 'BridgePoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.flavor': { + 'Meta': {'unique_together': "(('cpu', 'ram', 'disk', 'disk_template'),)", 'object_name': 'Flavor'}, + 'allow_create': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cpu': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'disk': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'disk_template': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ram': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'volume_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'flavors'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.VolumeType']"}) + }, + 'db.ipaddress': { + 'Meta': {'unique_together': "(('network', 'address', 'deleted'),)", 'object_name': 'IPAddress'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'floating_ip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'nic': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.NetworkInterface']"}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.ipaddresslog': { + 'Meta': {'object_name': 'IPAddressLog'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}), + 'allocated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'network_id': ('django.db.models.fields.IntegerField', [], {}), + 'released_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'server_id': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.ippooltable': { + 'Meta': {'object_name': 'IPPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ip_pools'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}) + }, + 'db.macprefixpooltable': { + 'Meta': {'object_name': 'MacPrefixPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.network': { + 'Meta': {'object_name': 'Network'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '32', 'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'external_router': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'flavor': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'floating_ip_pool': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'link': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'machines': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.VirtualMachine']", 'through': "orm['db.NetworkInterface']", 'symmetrical': 'False'}), + 'mode': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'network'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '32'}), + 'subnet_ids': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'tags': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.networkinterface': { + 'Meta': {'object_name': 'NetworkInterface'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'device_owner': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'firewall_profile': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'mac': ('django.db.models.fields.CharField', [], {'max_length': '32', 'unique': 'True', 'null': 'True'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.VirtualMachine']"}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'security_groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.SecurityGroup']", 'null': 'True', 'symmetrical': 'False'}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'ACTIVE'", 'max_length': '32'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.quotaholderserial': { + 'Meta': {'ordering': "['serial']", 'object_name': 'QuotaHolderSerial'}, + 'accept': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pending': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'resolved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'serial': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_index': 'True'}) + }, + 'db.securitygroup': { + 'Meta': {'object_name': 'SecurityGroup'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'db.subnet': { + 'Meta': {'object_name': 'Subnet'}, + 'cidr': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'dhcp': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'dns_nameservers': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'gateway': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'host_routes': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'subnets'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.virtualmachine': { + 'Meta': {'object_name': 'VirtualMachine'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '30', 'null': 'True'}), + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machines'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backend_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'buildpercentage': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'flavor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.Flavor']", 'on_delete': 'models.PROTECT'}), + 'hostid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'imageid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'BUILD'", 'max_length': '30'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machine'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'suspended': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'task': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'task_job_id': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}) + }, + 'db.virtualmachinediagnostic': { + 'Meta': {'ordering': "['-created']", 'object_name': 'VirtualMachineDiagnostic'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'details': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'diagnostics'", 'to': "orm['db.VirtualMachine']"}), + 'message': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'source_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}) + }, + 'db.virtualmachinemetadata': { + 'Meta': {'unique_together': "(('meta_key', 'vm'),)", 'object_name': 'VirtualMachineMetadata'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'meta_key': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'meta_value': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'vm': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'metadata'", 'to': "orm['db.VirtualMachine']"}) + }, + 'db.volume': { + 'Meta': {'object_name': 'Volume'}, + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'delete_on_termination': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'disk_template': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'volumes'", 'null': 'True', 'to': "orm['db.VirtualMachine']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'origin': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'snapshot_counter': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'CREATING'", 'max_length': '64'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}), + 'volume_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'volumes'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.VolumeType']"}) + }, + 'db.volumemetadata': { + 'Meta': {'unique_together': "(('volume', 'key'),)", 'object_name': 'VolumeMetadata'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'volume': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'metadata'", 'to': "orm['db.Volume']"}) + }, + 'db.volumetype': { + 'Meta': {'object_name': 'VolumeType'}, + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'disk_template': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + } + } + + complete_apps = ['db'] diff --git a/snf-cyclades-app/synnefo/db/migrations/0108_dtemplate_to_vtype.py b/snf-cyclades-app/synnefo/db/migrations/0108_dtemplate_to_vtype.py new file mode 100644 index 0000000000000000000000000000000000000000..d64d8b76bad68cc46efd35be5319ec2f154d5c48 --- /dev/null +++ b/snf-cyclades-app/synnefo/db/migrations/0108_dtemplate_to_vtype.py @@ -0,0 +1,282 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + + def forwards(self, orm): + "Write your forwards methods here." + for disk_template in orm.Flavor.objects\ + .values_list("disk_template", flat=True)\ + .distinct(): + vtype = orm.VolumeType.objects.create(name=disk_template, + disk_template=disk_template) + orm.Flavor.objects.filter(disk_template=disk_template)\ + .update(volume_type=vtype) + orm.Volume.objects.filter(disk_template=disk_template)\ + .update(volume_type=vtype) + + def backwards(self, orm): + for volume_t in orm.VolumeType.objects.all(): + orm.Flavor.objects.update(disk_template=volume_t.disk_template) + orm.Volume.objects.update(disk_template=volume_t.disk_template) + + models = { + 'db.backend': { + 'Meta': {'ordering': "['clustername']", 'object_name': 'Backend'}, + 'clustername': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}), + 'ctotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'dfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'disk_templates': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'dtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'hash': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'hypervisor': ('django.db.models.fields.CharField', [], {'default': "'kvm'", 'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'unique': 'True'}), + 'mfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'mtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'offline': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'password_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), + 'pinst_cnt': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'port': ('django.db.models.fields.PositiveIntegerField', [], {'default': '5080'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}) + }, + 'db.backendnetwork': { + 'Meta': {'unique_together': "(('network', 'backend'),)", 'object_name': 'BackendNetwork'}, + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'backend_networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '30'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + 'db.bridgepooltable': { + 'Meta': {'object_name': 'BridgePoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.flavor': { + 'Meta': {'unique_together': "(('cpu', 'ram', 'disk', 'disk_template'),)", 'object_name': 'Flavor'}, + 'allow_create': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cpu': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'disk': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'disk_template': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ram': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'volume_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'flavors'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.VolumeType']"}) + }, + 'db.ipaddress': { + 'Meta': {'unique_together': "(('network', 'address', 'deleted'),)", 'object_name': 'IPAddress'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'floating_ip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'nic': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.NetworkInterface']"}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.ipaddresslog': { + 'Meta': {'object_name': 'IPAddressLog'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}), + 'allocated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'network_id': ('django.db.models.fields.IntegerField', [], {}), + 'released_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'server_id': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.ippooltable': { + 'Meta': {'object_name': 'IPPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ip_pools'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}) + }, + 'db.macprefixpooltable': { + 'Meta': {'object_name': 'MacPrefixPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.network': { + 'Meta': {'object_name': 'Network'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '32', 'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'external_router': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'flavor': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'floating_ip_pool': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'link': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'machines': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.VirtualMachine']", 'through': "orm['db.NetworkInterface']", 'symmetrical': 'False'}), + 'mode': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'network'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '32'}), + 'subnet_ids': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'tags': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.networkinterface': { + 'Meta': {'object_name': 'NetworkInterface'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'device_owner': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'firewall_profile': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'mac': ('django.db.models.fields.CharField', [], {'max_length': '32', 'unique': 'True', 'null': 'True'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.VirtualMachine']"}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'security_groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.SecurityGroup']", 'null': 'True', 'symmetrical': 'False'}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'ACTIVE'", 'max_length': '32'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.quotaholderserial': { + 'Meta': {'ordering': "['serial']", 'object_name': 'QuotaHolderSerial'}, + 'accept': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pending': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'resolved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'serial': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_index': 'True'}) + }, + 'db.securitygroup': { + 'Meta': {'object_name': 'SecurityGroup'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'db.subnet': { + 'Meta': {'object_name': 'Subnet'}, + 'cidr': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'dhcp': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'dns_nameservers': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'gateway': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'host_routes': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'subnets'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.virtualmachine': { + 'Meta': {'object_name': 'VirtualMachine'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '30', 'null': 'True'}), + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machines'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backend_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'buildpercentage': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'flavor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.Flavor']", 'on_delete': 'models.PROTECT'}), + 'hostid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'imageid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'BUILD'", 'max_length': '30'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machine'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'suspended': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'task': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'task_job_id': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}) + }, + 'db.virtualmachinediagnostic': { + 'Meta': {'ordering': "['-created']", 'object_name': 'VirtualMachineDiagnostic'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'details': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'diagnostics'", 'to': "orm['db.VirtualMachine']"}), + 'message': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'source_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}) + }, + 'db.virtualmachinemetadata': { + 'Meta': {'unique_together': "(('meta_key', 'vm'),)", 'object_name': 'VirtualMachineMetadata'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'meta_key': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'meta_value': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'vm': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'metadata'", 'to': "orm['db.VirtualMachine']"}) + }, + 'db.volume': { + 'Meta': {'object_name': 'Volume'}, + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'delete_on_termination': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'disk_template': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'volumes'", 'null': 'True', 'to': "orm['db.VirtualMachine']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'origin': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'snapshot_counter': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'CREATING'", 'max_length': '64'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}), + 'volume_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'volumes'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.VolumeType']"}) + }, + 'db.volumemetadata': { + 'Meta': {'unique_together': "(('volume', 'key'),)", 'object_name': 'VolumeMetadata'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'volume': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'metadata'", 'to': "orm['db.Volume']"}) + }, + 'db.volumetype': { + 'Meta': {'object_name': 'VolumeType'}, + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'disk_template': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + } + } + + complete_apps = ['db'] + symmetrical = True diff --git a/snf-cyclades-app/synnefo/db/migrations/0109_auto__del_field_volume_disk_template__chg_field_volume_volume_type__de.py b/snf-cyclades-app/synnefo/db/migrations/0109_auto__del_field_volume_disk_template__chg_field_volume_volume_type__de.py new file mode 100644 index 0000000000000000000000000000000000000000..9697e26c30d809647cb9c6898ecdc445ca4c37b6 --- /dev/null +++ b/snf-cyclades-app/synnefo/db/migrations/0109_auto__del_field_volume_disk_template__chg_field_volume_volume_type__de.py @@ -0,0 +1,307 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Removing unique constraint on 'Flavor', fields ['disk', 'ram', 'cpu', 'disk_template'] + db.delete_unique('db_flavor', ['disk', 'ram', 'cpu', 'disk_template']) + + # Deleting field 'Volume.disk_template' + db.delete_column('db_volume', 'disk_template') + + + # Changing field 'Volume.volume_type' + db.alter_column('db_volume', 'volume_type_id', self.gf('django.db.models.fields.related.ForeignKey')(default=1, on_delete=models.PROTECT, to=orm['db.VolumeType'])) + # Deleting field 'Flavor.disk_template' + db.delete_column('db_flavor', 'disk_template') + + + # Changing field 'Flavor.volume_type' + db.alter_column('db_flavor', 'volume_type_id', self.gf('django.db.models.fields.related.ForeignKey')(default=1, on_delete=models.PROTECT, to=orm['db.VolumeType'])) + # Adding unique constraint on 'Flavor', fields ['disk', 'ram', 'cpu', 'volume_type'] + db.create_unique('db_flavor', ['disk', 'ram', 'cpu', 'volume_type_id']) + + + def backwards(self, orm): + # Removing unique constraint on 'Flavor', fields ['disk', 'ram', 'cpu', 'volume_type'] + db.delete_unique('db_flavor', ['disk', 'ram', 'cpu', 'volume_type_id']) + + # Adding field 'Volume.disk_template' + db.add_column('db_volume', 'disk_template', + self.gf('django.db.models.fields.CharField')(default='none', max_length=32), + keep_default=False) + + + # Changing field 'Volume.volume_type' + db.alter_column('db_volume', 'volume_type_id', self.gf('django.db.models.fields.related.ForeignKey')(null=True, on_delete=models.PROTECT, to=orm['db.VolumeType'])) + # Adding field 'Flavor.disk_template' + db.add_column('db_flavor', 'disk_template', + self.gf('django.db.models.fields.CharField')(default='none', max_length=32), + keep_default=False) + + + # Changing field 'Flavor.volume_type' + db.alter_column('db_flavor', 'volume_type_id', self.gf('django.db.models.fields.related.ForeignKey')(null=True, on_delete=models.PROTECT, to=orm['db.VolumeType'])) + # Adding unique constraint on 'Flavor', fields ['disk', 'ram', 'cpu', 'disk_template'] + db.create_unique('db_flavor', ['disk', 'ram', 'cpu', 'disk_template']) + + + models = { + 'db.backend': { + 'Meta': {'ordering': "['clustername']", 'object_name': 'Backend'}, + 'clustername': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}), + 'ctotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'dfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'disk_templates': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'dtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'hash': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'hypervisor': ('django.db.models.fields.CharField', [], {'default': "'kvm'", 'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'unique': 'True'}), + 'mfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'mtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'offline': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'password_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), + 'pinst_cnt': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'port': ('django.db.models.fields.PositiveIntegerField', [], {'default': '5080'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}) + }, + 'db.backendnetwork': { + 'Meta': {'unique_together': "(('network', 'backend'),)", 'object_name': 'BackendNetwork'}, + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'backend_networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '30'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + 'db.bridgepooltable': { + 'Meta': {'object_name': 'BridgePoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.flavor': { + 'Meta': {'unique_together': "(('cpu', 'ram', 'disk', 'volume_type'),)", 'object_name': 'Flavor'}, + 'allow_create': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cpu': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'disk': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ram': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'volume_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'flavors'", 'on_delete': 'models.PROTECT', 'to': "orm['db.VolumeType']"}) + }, + 'db.ipaddress': { + 'Meta': {'unique_together': "(('network', 'address', 'deleted'),)", 'object_name': 'IPAddress'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'floating_ip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'nic': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.NetworkInterface']"}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.ipaddresslog': { + 'Meta': {'object_name': 'IPAddressLog'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}), + 'allocated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'network_id': ('django.db.models.fields.IntegerField', [], {}), + 'released_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'server_id': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.ippooltable': { + 'Meta': {'object_name': 'IPPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ip_pools'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}) + }, + 'db.macprefixpooltable': { + 'Meta': {'object_name': 'MacPrefixPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.network': { + 'Meta': {'object_name': 'Network'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '32', 'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'external_router': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'flavor': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'floating_ip_pool': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'link': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'machines': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.VirtualMachine']", 'through': "orm['db.NetworkInterface']", 'symmetrical': 'False'}), + 'mode': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'network'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '32'}), + 'subnet_ids': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'tags': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.networkinterface': { + 'Meta': {'object_name': 'NetworkInterface'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'device_owner': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'firewall_profile': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'mac': ('django.db.models.fields.CharField', [], {'max_length': '32', 'unique': 'True', 'null': 'True'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.VirtualMachine']"}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'security_groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.SecurityGroup']", 'null': 'True', 'symmetrical': 'False'}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'ACTIVE'", 'max_length': '32'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.quotaholderserial': { + 'Meta': {'ordering': "['serial']", 'object_name': 'QuotaHolderSerial'}, + 'accept': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pending': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'resolved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'serial': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_index': 'True'}) + }, + 'db.securitygroup': { + 'Meta': {'object_name': 'SecurityGroup'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'db.subnet': { + 'Meta': {'object_name': 'Subnet'}, + 'cidr': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'dhcp': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'dns_nameservers': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'gateway': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'host_routes': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'subnets'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.virtualmachine': { + 'Meta': {'object_name': 'VirtualMachine'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '30', 'null': 'True'}), + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machines'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backend_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'buildpercentage': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'flavor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.Flavor']", 'on_delete': 'models.PROTECT'}), + 'hostid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'imageid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'BUILD'", 'max_length': '30'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machine'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'suspended': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'task': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'task_job_id': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}) + }, + 'db.virtualmachinediagnostic': { + 'Meta': {'ordering': "['-created']", 'object_name': 'VirtualMachineDiagnostic'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'details': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'diagnostics'", 'to': "orm['db.VirtualMachine']"}), + 'message': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'source_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}) + }, + 'db.virtualmachinemetadata': { + 'Meta': {'unique_together': "(('meta_key', 'vm'),)", 'object_name': 'VirtualMachineMetadata'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'meta_key': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'meta_value': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'vm': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'metadata'", 'to': "orm['db.VirtualMachine']"}) + }, + 'db.volume': { + 'Meta': {'object_name': 'Volume'}, + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'delete_on_termination': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'volumes'", 'null': 'True', 'to': "orm['db.VirtualMachine']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'origin': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'snapshot_counter': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'CREATING'", 'max_length': '64'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}), + 'volume_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'volumes'", 'on_delete': 'models.PROTECT', 'to': "orm['db.VolumeType']"}) + }, + 'db.volumemetadata': { + 'Meta': {'unique_together': "(('volume', 'key'),)", 'object_name': 'VolumeMetadata'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'volume': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'metadata'", 'to': "orm['db.Volume']"}) + }, + 'db.volumetype': { + 'Meta': {'object_name': 'VolumeType'}, + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'disk_template': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + } + } + + complete_apps = ['db'] \ No newline at end of file diff --git a/snf-cyclades-app/synnefo/db/migrations/0110_auto__add_field_volume_project.py b/snf-cyclades-app/synnefo/db/migrations/0110_auto__add_field_volume_project.py new file mode 100644 index 0000000000000000000000000000000000000000..83cba3d747237a562e98fa1894110a0562266bd0 --- /dev/null +++ b/snf-cyclades-app/synnefo/db/migrations/0110_auto__add_field_volume_project.py @@ -0,0 +1,276 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'Volume.project' + db.add_column('db_volume', 'project', + self.gf('django.db.models.fields.CharField')(max_length=255, null=True), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'Volume.project' + db.delete_column('db_volume', 'project') + + + models = { + 'db.backend': { + 'Meta': {'ordering': "['clustername']", 'object_name': 'Backend'}, + 'clustername': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}), + 'ctotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'dfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'disk_templates': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'dtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'hash': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'hypervisor': ('django.db.models.fields.CharField', [], {'default': "'kvm'", 'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'unique': 'True'}), + 'mfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'mtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'offline': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'password_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), + 'pinst_cnt': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'port': ('django.db.models.fields.PositiveIntegerField', [], {'default': '5080'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}) + }, + 'db.backendnetwork': { + 'Meta': {'unique_together': "(('network', 'backend'),)", 'object_name': 'BackendNetwork'}, + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'backend_networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '30'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + 'db.bridgepooltable': { + 'Meta': {'object_name': 'BridgePoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.flavor': { + 'Meta': {'unique_together': "(('cpu', 'ram', 'disk', 'volume_type'),)", 'object_name': 'Flavor'}, + 'allow_create': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cpu': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'disk': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ram': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'volume_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'flavors'", 'on_delete': 'models.PROTECT', 'to': "orm['db.VolumeType']"}) + }, + 'db.ipaddress': { + 'Meta': {'unique_together': "(('network', 'address', 'deleted'),)", 'object_name': 'IPAddress'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'floating_ip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'nic': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.NetworkInterface']"}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.ipaddresslog': { + 'Meta': {'object_name': 'IPAddressLog'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}), + 'allocated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'network_id': ('django.db.models.fields.IntegerField', [], {}), + 'released_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'server_id': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.ippooltable': { + 'Meta': {'object_name': 'IPPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ip_pools'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}) + }, + 'db.macprefixpooltable': { + 'Meta': {'object_name': 'MacPrefixPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.network': { + 'Meta': {'object_name': 'Network'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '32', 'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'external_router': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'flavor': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'floating_ip_pool': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'link': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'machines': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.VirtualMachine']", 'through': "orm['db.NetworkInterface']", 'symmetrical': 'False'}), + 'mode': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'network'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '32'}), + 'subnet_ids': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'tags': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.networkinterface': { + 'Meta': {'object_name': 'NetworkInterface'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'device_owner': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'firewall_profile': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'mac': ('django.db.models.fields.CharField', [], {'max_length': '32', 'unique': 'True', 'null': 'True'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.VirtualMachine']"}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'security_groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.SecurityGroup']", 'null': 'True', 'symmetrical': 'False'}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'ACTIVE'", 'max_length': '32'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.quotaholderserial': { + 'Meta': {'ordering': "['serial']", 'object_name': 'QuotaHolderSerial'}, + 'accept': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pending': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'resolved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'serial': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_index': 'True'}) + }, + 'db.securitygroup': { + 'Meta': {'object_name': 'SecurityGroup'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'db.subnet': { + 'Meta': {'object_name': 'Subnet'}, + 'cidr': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'dhcp': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'dns_nameservers': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'gateway': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'host_routes': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'subnets'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.virtualmachine': { + 'Meta': {'object_name': 'VirtualMachine'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '30', 'null': 'True'}), + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machines'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backend_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'buildpercentage': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'flavor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.Flavor']", 'on_delete': 'models.PROTECT'}), + 'hostid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'imageid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'BUILD'", 'max_length': '30'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machine'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'suspended': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'task': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'task_job_id': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}) + }, + 'db.virtualmachinediagnostic': { + 'Meta': {'ordering': "['-created']", 'object_name': 'VirtualMachineDiagnostic'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'details': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'diagnostics'", 'to': "orm['db.VirtualMachine']"}), + 'message': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'source_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}) + }, + 'db.virtualmachinemetadata': { + 'Meta': {'unique_together': "(('meta_key', 'vm'),)", 'object_name': 'VirtualMachineMetadata'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'meta_key': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'meta_value': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'vm': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'metadata'", 'to': "orm['db.VirtualMachine']"}) + }, + 'db.volume': { + 'Meta': {'object_name': 'Volume'}, + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'delete_on_termination': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'volumes'", 'null': 'True', 'to': "orm['db.VirtualMachine']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'origin': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'snapshot_counter': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'CREATING'", 'max_length': '64'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}), + 'volume_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'volumes'", 'on_delete': 'models.PROTECT', 'to': "orm['db.VolumeType']"}) + }, + 'db.volumemetadata': { + 'Meta': {'unique_together': "(('volume', 'key'),)", 'object_name': 'VolumeMetadata'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'volume': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'metadata'", 'to': "orm['db.Volume']"}) + }, + 'db.volumetype': { + 'Meta': {'object_name': 'VolumeType'}, + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'disk_template': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + } + } + + complete_apps = ['db'] \ No newline at end of file diff --git a/snf-cyclades-app/synnefo/db/migrations/0111_volume_project.py b/snf-cyclades-app/synnefo/db/migrations/0111_volume_project.py new file mode 100644 index 0000000000000000000000000000000000000000..106d7af154712d96026cdfb16eb2457731f824f5 --- /dev/null +++ b/snf-cyclades-app/synnefo/db/migrations/0111_volume_project.py @@ -0,0 +1,270 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + + def forwards(self, orm): + orm.Volume.objects.all().update(project=models.F('userid')) + + def backwards(self, orm): + "Write your backwards methods here." + + models = { + 'db.backend': { + 'Meta': {'ordering': "['clustername']", 'object_name': 'Backend'}, + 'clustername': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}), + 'ctotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'dfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'disk_templates': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'dtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'hash': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'hypervisor': ('django.db.models.fields.CharField', [], {'default': "'kvm'", 'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'unique': 'True'}), + 'mfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'mtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'offline': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'password_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), + 'pinst_cnt': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'port': ('django.db.models.fields.PositiveIntegerField', [], {'default': '5080'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}) + }, + 'db.backendnetwork': { + 'Meta': {'unique_together': "(('network', 'backend'),)", 'object_name': 'BackendNetwork'}, + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'backend_networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '30'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + 'db.bridgepooltable': { + 'Meta': {'object_name': 'BridgePoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.flavor': { + 'Meta': {'unique_together': "(('cpu', 'ram', 'disk', 'volume_type'),)", 'object_name': 'Flavor'}, + 'allow_create': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cpu': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'disk': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ram': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'volume_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'flavors'", 'on_delete': 'models.PROTECT', 'to': "orm['db.VolumeType']"}) + }, + 'db.ipaddress': { + 'Meta': {'unique_together': "(('network', 'address', 'deleted'),)", 'object_name': 'IPAddress'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'floating_ip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'nic': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.NetworkInterface']"}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.ipaddresslog': { + 'Meta': {'object_name': 'IPAddressLog'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}), + 'allocated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'network_id': ('django.db.models.fields.IntegerField', [], {}), + 'released_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'server_id': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.ippooltable': { + 'Meta': {'object_name': 'IPPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ip_pools'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}) + }, + 'db.macprefixpooltable': { + 'Meta': {'object_name': 'MacPrefixPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.network': { + 'Meta': {'object_name': 'Network'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '32', 'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'external_router': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'flavor': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'floating_ip_pool': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'link': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'machines': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.VirtualMachine']", 'through': "orm['db.NetworkInterface']", 'symmetrical': 'False'}), + 'mode': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'network'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '32'}), + 'subnet_ids': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'tags': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.networkinterface': { + 'Meta': {'object_name': 'NetworkInterface'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'device_owner': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'firewall_profile': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'mac': ('django.db.models.fields.CharField', [], {'max_length': '32', 'unique': 'True', 'null': 'True'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.VirtualMachine']"}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'security_groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.SecurityGroup']", 'null': 'True', 'symmetrical': 'False'}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'ACTIVE'", 'max_length': '32'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.quotaholderserial': { + 'Meta': {'ordering': "['serial']", 'object_name': 'QuotaHolderSerial'}, + 'accept': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pending': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'resolved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'serial': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_index': 'True'}) + }, + 'db.securitygroup': { + 'Meta': {'object_name': 'SecurityGroup'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'db.subnet': { + 'Meta': {'object_name': 'Subnet'}, + 'cidr': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'dhcp': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'dns_nameservers': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'gateway': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'host_routes': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'subnets'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.virtualmachine': { + 'Meta': {'object_name': 'VirtualMachine'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '30', 'null': 'True'}), + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machines'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backend_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'buildpercentage': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'flavor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.Flavor']", 'on_delete': 'models.PROTECT'}), + 'hostid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'imageid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'BUILD'", 'max_length': '30'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machine'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'suspended': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'task': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'task_job_id': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}) + }, + 'db.virtualmachinediagnostic': { + 'Meta': {'ordering': "['-created']", 'object_name': 'VirtualMachineDiagnostic'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'details': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'diagnostics'", 'to': "orm['db.VirtualMachine']"}), + 'message': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'source_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}) + }, + 'db.virtualmachinemetadata': { + 'Meta': {'unique_together': "(('meta_key', 'vm'),)", 'object_name': 'VirtualMachineMetadata'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'meta_key': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'meta_value': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'vm': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'metadata'", 'to': "orm['db.VirtualMachine']"}) + }, + 'db.volume': { + 'Meta': {'object_name': 'Volume'}, + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'delete_on_termination': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'volumes'", 'null': 'True', 'to': "orm['db.VirtualMachine']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'origin': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'snapshot_counter': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'CREATING'", 'max_length': '64'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}), + 'volume_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'volumes'", 'on_delete': 'models.PROTECT', 'to': "orm['db.VolumeType']"}) + }, + 'db.volumemetadata': { + 'Meta': {'unique_together': "(('volume', 'key'),)", 'object_name': 'VolumeMetadata'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'volume': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'metadata'", 'to': "orm['db.Volume']"}) + }, + 'db.volumetype': { + 'Meta': {'object_name': 'VolumeType'}, + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'disk_template': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + } + } + + complete_apps = ['db'] + symmetrical = True diff --git a/snf-cyclades-app/synnefo/db/migrations/0112_auto__add_field_volume_serial.py b/snf-cyclades-app/synnefo/db/migrations/0112_auto__add_field_volume_serial.py new file mode 100644 index 0000000000000000000000000000000000000000..ab2be19c1141a12d2f1c2094dd0c94a58ecf0ff7 --- /dev/null +++ b/snf-cyclades-app/synnefo/db/migrations/0112_auto__add_field_volume_serial.py @@ -0,0 +1,277 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'Volume.serial' + db.add_column('db_volume', 'serial', + self.gf('django.db.models.fields.related.ForeignKey')(related_name='volume', null=True, on_delete=models.SET_NULL, to=orm['db.QuotaHolderSerial']), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'Volume.serial' + db.delete_column('db_volume', 'serial_id') + + + models = { + 'db.backend': { + 'Meta': {'ordering': "['clustername']", 'object_name': 'Backend'}, + 'clustername': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}), + 'ctotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'dfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'disk_templates': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'dtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'hash': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'hypervisor': ('django.db.models.fields.CharField', [], {'default': "'kvm'", 'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'unique': 'True'}), + 'mfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'mtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'offline': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'password_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), + 'pinst_cnt': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'port': ('django.db.models.fields.PositiveIntegerField', [], {'default': '5080'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}) + }, + 'db.backendnetwork': { + 'Meta': {'unique_together': "(('network', 'backend'),)", 'object_name': 'BackendNetwork'}, + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'backend_networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '30'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + 'db.bridgepooltable': { + 'Meta': {'object_name': 'BridgePoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.flavor': { + 'Meta': {'unique_together': "(('cpu', 'ram', 'disk', 'volume_type'),)", 'object_name': 'Flavor'}, + 'allow_create': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cpu': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'disk': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ram': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'volume_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'flavors'", 'on_delete': 'models.PROTECT', 'to': "orm['db.VolumeType']"}) + }, + 'db.ipaddress': { + 'Meta': {'unique_together': "(('network', 'address', 'deleted'),)", 'object_name': 'IPAddress'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'floating_ip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'nic': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.NetworkInterface']"}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.ipaddresslog': { + 'Meta': {'object_name': 'IPAddressLog'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}), + 'allocated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'network_id': ('django.db.models.fields.IntegerField', [], {}), + 'released_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'server_id': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.ippooltable': { + 'Meta': {'object_name': 'IPPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ip_pools'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}) + }, + 'db.macprefixpooltable': { + 'Meta': {'object_name': 'MacPrefixPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.network': { + 'Meta': {'object_name': 'Network'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '32', 'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'external_router': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'flavor': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'floating_ip_pool': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'link': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'machines': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.VirtualMachine']", 'through': "orm['db.NetworkInterface']", 'symmetrical': 'False'}), + 'mode': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'network'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '32'}), + 'subnet_ids': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'tags': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.networkinterface': { + 'Meta': {'object_name': 'NetworkInterface'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'device_owner': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'firewall_profile': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'mac': ('django.db.models.fields.CharField', [], {'max_length': '32', 'unique': 'True', 'null': 'True'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.VirtualMachine']"}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'security_groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.SecurityGroup']", 'null': 'True', 'symmetrical': 'False'}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'ACTIVE'", 'max_length': '32'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.quotaholderserial': { + 'Meta': {'ordering': "['serial']", 'object_name': 'QuotaHolderSerial'}, + 'accept': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pending': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'resolved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'serial': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_index': 'True'}) + }, + 'db.securitygroup': { + 'Meta': {'object_name': 'SecurityGroup'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'db.subnet': { + 'Meta': {'object_name': 'Subnet'}, + 'cidr': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'dhcp': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'dns_nameservers': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'gateway': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'host_routes': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'subnets'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.virtualmachine': { + 'Meta': {'object_name': 'VirtualMachine'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '30', 'null': 'True'}), + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machines'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backend_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'buildpercentage': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'flavor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.Flavor']", 'on_delete': 'models.PROTECT'}), + 'hostid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'imageid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'BUILD'", 'max_length': '30'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machine'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'suspended': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'task': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'task_job_id': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}) + }, + 'db.virtualmachinediagnostic': { + 'Meta': {'ordering': "['-created']", 'object_name': 'VirtualMachineDiagnostic'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'details': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'diagnostics'", 'to': "orm['db.VirtualMachine']"}), + 'message': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'source_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}) + }, + 'db.virtualmachinemetadata': { + 'Meta': {'unique_together': "(('meta_key', 'vm'),)", 'object_name': 'VirtualMachineMetadata'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'meta_key': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'meta_value': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'vm': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'metadata'", 'to': "orm['db.VirtualMachine']"}) + }, + 'db.volume': { + 'Meta': {'object_name': 'Volume'}, + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'delete_on_termination': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'volumes'", 'null': 'True', 'to': "orm['db.VirtualMachine']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'origin': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'volume'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'snapshot_counter': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'CREATING'", 'max_length': '64'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}), + 'volume_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'volumes'", 'on_delete': 'models.PROTECT', 'to': "orm['db.VolumeType']"}) + }, + 'db.volumemetadata': { + 'Meta': {'unique_together': "(('volume', 'key'),)", 'object_name': 'VolumeMetadata'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'volume': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'metadata'", 'to': "orm['db.Volume']"}) + }, + 'db.volumetype': { + 'Meta': {'object_name': 'VolumeType'}, + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'disk_template': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + } + } + + complete_apps = ['db'] \ No newline at end of file diff --git a/snf-cyclades-app/synnefo/db/migrations/0113_auto__add_index_volume_project__add_index_ipaddress_project__add_index.py b/snf-cyclades-app/synnefo/db/migrations/0113_auto__add_index_volume_project__add_index_ipaddress_project__add_index.py new file mode 100644 index 0000000000000000000000000000000000000000..d89686934dd1e27d7dd0d94569d3283429b03937 --- /dev/null +++ b/snf-cyclades-app/synnefo/db/migrations/0113_auto__add_index_volume_project__add_index_ipaddress_project__add_index.py @@ -0,0 +1,293 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding index on 'Volume', fields ['project'] + db.create_index('db_volume', ['project']) + + # Adding index on 'IPAddress', fields ['project'] + db.create_index('db_ipaddress', ['project']) + + # Adding index on 'VirtualMachine', fields ['project'] + db.create_index('db_virtualmachine', ['project']) + + # Adding index on 'Network', fields ['project'] + db.create_index('db_network', ['project']) + + + def backwards(self, orm): + # Removing index on 'Network', fields ['project'] + db.delete_index('db_network', ['project']) + + # Removing index on 'VirtualMachine', fields ['project'] + db.delete_index('db_virtualmachine', ['project']) + + # Removing index on 'IPAddress', fields ['project'] + db.delete_index('db_ipaddress', ['project']) + + # Removing index on 'Volume', fields ['project'] + db.delete_index('db_volume', ['project']) + + + models = { + 'db.backend': { + 'Meta': {'ordering': "['clustername']", 'object_name': 'Backend'}, + 'clustername': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}), + 'ctotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'dfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'disk_templates': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'dtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'hash': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'hypervisor': ('django.db.models.fields.CharField', [], {'default': "'kvm'", 'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'unique': 'True'}), + 'mfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'mtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'offline': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'password_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), + 'pinst_cnt': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'port': ('django.db.models.fields.PositiveIntegerField', [], {'default': '5080'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}) + }, + 'db.backendnetwork': { + 'Meta': {'unique_together': "(('network', 'backend'),)", 'object_name': 'BackendNetwork'}, + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'backend_networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '30'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + 'db.bridgepooltable': { + 'Meta': {'object_name': 'BridgePoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.flavor': { + 'Meta': {'unique_together': "(('cpu', 'ram', 'disk', 'volume_type'),)", 'object_name': 'Flavor'}, + 'allow_create': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cpu': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'disk': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ram': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'volume_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'flavors'", 'on_delete': 'models.PROTECT', 'to': "orm['db.VolumeType']"}) + }, + 'db.ipaddress': { + 'Meta': {'unique_together': "(('network', 'address', 'deleted'),)", 'object_name': 'IPAddress'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'floating_ip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'nic': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.NetworkInterface']"}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_index': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.ipaddresslog': { + 'Meta': {'object_name': 'IPAddressLog'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}), + 'allocated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'network_id': ('django.db.models.fields.IntegerField', [], {}), + 'released_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'server_id': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.ippooltable': { + 'Meta': {'object_name': 'IPPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ip_pools'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}) + }, + 'db.macprefixpooltable': { + 'Meta': {'object_name': 'MacPrefixPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.network': { + 'Meta': {'object_name': 'Network'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '32', 'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'external_router': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'flavor': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'floating_ip_pool': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'link': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'machines': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.VirtualMachine']", 'through': "orm['db.NetworkInterface']", 'symmetrical': 'False'}), + 'mode': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_index': 'True'}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'network'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '32'}), + 'subnet_ids': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'tags': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.networkinterface': { + 'Meta': {'object_name': 'NetworkInterface'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'device_owner': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'firewall_profile': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'mac': ('django.db.models.fields.CharField', [], {'max_length': '32', 'unique': 'True', 'null': 'True'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.VirtualMachine']"}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'security_groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.SecurityGroup']", 'null': 'True', 'symmetrical': 'False'}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'ACTIVE'", 'max_length': '32'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.quotaholderserial': { + 'Meta': {'ordering': "['serial']", 'object_name': 'QuotaHolderSerial'}, + 'accept': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pending': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'resolved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'serial': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_index': 'True'}) + }, + 'db.securitygroup': { + 'Meta': {'object_name': 'SecurityGroup'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'db.subnet': { + 'Meta': {'object_name': 'Subnet'}, + 'cidr': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'dhcp': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'dns_nameservers': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'gateway': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'host_routes': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'subnets'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.virtualmachine': { + 'Meta': {'object_name': 'VirtualMachine'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '30', 'null': 'True'}), + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machines'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backend_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'buildpercentage': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'flavor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.Flavor']", 'on_delete': 'models.PROTECT'}), + 'hostid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'imageid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'BUILD'", 'max_length': '30'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_index': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machine'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'suspended': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'task': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'task_job_id': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}) + }, + 'db.virtualmachinediagnostic': { + 'Meta': {'ordering': "['-created']", 'object_name': 'VirtualMachineDiagnostic'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'details': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'diagnostics'", 'to': "orm['db.VirtualMachine']"}), + 'message': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'source_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}) + }, + 'db.virtualmachinemetadata': { + 'Meta': {'unique_together': "(('meta_key', 'vm'),)", 'object_name': 'VirtualMachineMetadata'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'meta_key': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'meta_value': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'vm': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'metadata'", 'to': "orm['db.VirtualMachine']"}) + }, + 'db.volume': { + 'Meta': {'object_name': 'Volume'}, + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'delete_on_termination': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'volumes'", 'null': 'True', 'to': "orm['db.VirtualMachine']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'origin': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_index': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'volume'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'snapshot_counter': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'CREATING'", 'max_length': '64'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}), + 'volume_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'volumes'", 'on_delete': 'models.PROTECT', 'to': "orm['db.VolumeType']"}) + }, + 'db.volumemetadata': { + 'Meta': {'unique_together': "(('volume', 'key'),)", 'object_name': 'VolumeMetadata'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'volume': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'metadata'", 'to': "orm['db.Volume']"}) + }, + 'db.volumetype': { + 'Meta': {'object_name': 'VolumeType'}, + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'disk_template': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + } + } + + complete_apps = ['db'] \ No newline at end of file diff --git a/snf-cyclades-app/synnefo/db/migrations/0114_auto.py b/snf-cyclades-app/synnefo/db/migrations/0114_auto.py new file mode 100644 index 0000000000000000000000000000000000000000..2ca5e59dd2abccf776b262d9df686a238a72f350 --- /dev/null +++ b/snf-cyclades-app/synnefo/db/migrations/0114_auto.py @@ -0,0 +1,275 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding index on 'Volume', fields ['deleted'] + db.create_index('db_volume', ['deleted']) + + + def backwards(self, orm): + # Removing index on 'Volume', fields ['deleted'] + db.delete_index('db_volume', ['deleted']) + + + models = { + 'db.backend': { + 'Meta': {'ordering': "['clustername']", 'object_name': 'Backend'}, + 'clustername': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}), + 'ctotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'dfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'disk_templates': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'dtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'hash': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'hypervisor': ('django.db.models.fields.CharField', [], {'default': "'kvm'", 'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'unique': 'True'}), + 'mfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'mtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'offline': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'password_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), + 'pinst_cnt': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'port': ('django.db.models.fields.PositiveIntegerField', [], {'default': '5080'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}) + }, + 'db.backendnetwork': { + 'Meta': {'unique_together': "(('network', 'backend'),)", 'object_name': 'BackendNetwork'}, + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'backend_networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '30'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + 'db.bridgepooltable': { + 'Meta': {'object_name': 'BridgePoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.flavor': { + 'Meta': {'unique_together': "(('cpu', 'ram', 'disk', 'volume_type'),)", 'object_name': 'Flavor'}, + 'allow_create': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cpu': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'disk': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ram': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'volume_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'flavors'", 'on_delete': 'models.PROTECT', 'to': "orm['db.VolumeType']"}) + }, + 'db.ipaddress': { + 'Meta': {'unique_together': "(('network', 'address', 'deleted'),)", 'object_name': 'IPAddress'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'floating_ip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'nic': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.NetworkInterface']"}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_index': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.ipaddresslog': { + 'Meta': {'object_name': 'IPAddressLog'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}), + 'allocated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'network_id': ('django.db.models.fields.IntegerField', [], {}), + 'released_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'server_id': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.ippooltable': { + 'Meta': {'object_name': 'IPPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ip_pools'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}) + }, + 'db.macprefixpooltable': { + 'Meta': {'object_name': 'MacPrefixPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.network': { + 'Meta': {'object_name': 'Network'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '32', 'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'external_router': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'flavor': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'floating_ip_pool': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'link': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'machines': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.VirtualMachine']", 'through': "orm['db.NetworkInterface']", 'symmetrical': 'False'}), + 'mode': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_index': 'True'}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'network'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '32'}), + 'subnet_ids': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'tags': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.networkinterface': { + 'Meta': {'object_name': 'NetworkInterface'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'device_owner': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'firewall_profile': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'mac': ('django.db.models.fields.CharField', [], {'max_length': '32', 'unique': 'True', 'null': 'True'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.VirtualMachine']"}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'security_groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.SecurityGroup']", 'null': 'True', 'symmetrical': 'False'}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'ACTIVE'", 'max_length': '32'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.quotaholderserial': { + 'Meta': {'ordering': "['serial']", 'object_name': 'QuotaHolderSerial'}, + 'accept': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pending': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'resolved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'serial': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_index': 'True'}) + }, + 'db.securitygroup': { + 'Meta': {'object_name': 'SecurityGroup'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'db.subnet': { + 'Meta': {'object_name': 'Subnet'}, + 'cidr': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'dhcp': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'dns_nameservers': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'gateway': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'host_routes': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'subnets'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.virtualmachine': { + 'Meta': {'object_name': 'VirtualMachine'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '30', 'null': 'True'}), + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machines'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backend_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'buildpercentage': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'flavor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.Flavor']", 'on_delete': 'models.PROTECT'}), + 'hostid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'imageid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'BUILD'", 'max_length': '30'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_index': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machine'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'suspended': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'task': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'task_job_id': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}) + }, + 'db.virtualmachinediagnostic': { + 'Meta': {'ordering': "['-created']", 'object_name': 'VirtualMachineDiagnostic'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'details': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'diagnostics'", 'to': "orm['db.VirtualMachine']"}), + 'message': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'source_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}) + }, + 'db.virtualmachinemetadata': { + 'Meta': {'unique_together': "(('meta_key', 'vm'),)", 'object_name': 'VirtualMachineMetadata'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'meta_key': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'meta_value': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'vm': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'metadata'", 'to': "orm['db.VirtualMachine']"}) + }, + 'db.volume': { + 'Meta': {'object_name': 'Volume'}, + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'delete_on_termination': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'volumes'", 'null': 'True', 'to': "orm['db.VirtualMachine']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'origin': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_index': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'volume'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'snapshot_counter': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'CREATING'", 'max_length': '64'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}), + 'volume_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'volumes'", 'on_delete': 'models.PROTECT', 'to': "orm['db.VolumeType']"}) + }, + 'db.volumemetadata': { + 'Meta': {'unique_together': "(('volume', 'key'),)", 'object_name': 'VolumeMetadata'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'volume': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'metadata'", 'to': "orm['db.Volume']"}) + }, + 'db.volumetype': { + 'Meta': {'object_name': 'VolumeType'}, + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'disk_template': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + } + } + + complete_apps = ['db'] \ No newline at end of file diff --git a/snf-cyclades-app/synnefo/db/migrations/0115_auto__add_field_volume_source_version__add_field_virtualmachine_image_.py b/snf-cyclades-app/synnefo/db/migrations/0115_auto__add_field_volume_source_version__add_field_virtualmachine_image_.py new file mode 100644 index 0000000000000000000000000000000000000000..bc6b593b6390ef425999c04c7e6edebee25f8645 --- /dev/null +++ b/snf-cyclades-app/synnefo/db/migrations/0115_auto__add_field_volume_source_version__add_field_virtualmachine_image_.py @@ -0,0 +1,287 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'Volume.source_version' + db.add_column('db_volume', 'source_version', + self.gf('django.db.models.fields.IntegerField')(null=True), + keep_default=False) + + # Adding field 'VirtualMachine.image_version' + db.add_column('db_virtualmachine', 'image_version', + self.gf('django.db.models.fields.IntegerField')(null=True), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'Volume.source_version' + db.delete_column('db_volume', 'source_version') + + # Deleting field 'VirtualMachine.image_version' + db.delete_column('db_virtualmachine', 'image_version') + + + models = { + 'db.backend': { + 'Meta': {'ordering': "['clustername']", 'object_name': 'Backend'}, + 'clustername': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}), + 'ctotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'dfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'disk_templates': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'dtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'hash': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'hypervisor': ('django.db.models.fields.CharField', [], {'default': "'kvm'", 'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'unique': 'True'}), + 'mfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'mtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'offline': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'password_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), + 'pinst_cnt': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'port': ('django.db.models.fields.PositiveIntegerField', [], {'default': '5080'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}) + }, + 'db.backendnetwork': { + 'Meta': {'unique_together': "(('network', 'backend'),)", 'object_name': 'BackendNetwork'}, + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'backend_networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '30'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + 'db.bridgepooltable': { + 'Meta': {'object_name': 'BridgePoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.flavor': { + 'Meta': {'unique_together': "(('cpu', 'ram', 'disk', 'volume_type'),)", 'object_name': 'Flavor'}, + 'allow_create': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cpu': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'disk': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ram': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'volume_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'flavors'", 'on_delete': 'models.PROTECT', 'to': "orm['db.VolumeType']"}) + }, + 'db.ipaddress': { + 'Meta': {'unique_together': "(('network', 'address', 'deleted'),)", 'object_name': 'IPAddress'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'floating_ip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'nic': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.NetworkInterface']"}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_index': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.ipaddresslog': { + 'Meta': {'object_name': 'IPAddressLog'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}), + 'allocated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'network_id': ('django.db.models.fields.IntegerField', [], {}), + 'released_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'server_id': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.ippooltable': { + 'Meta': {'object_name': 'IPPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ip_pools'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}) + }, + 'db.macprefixpooltable': { + 'Meta': {'object_name': 'MacPrefixPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.network': { + 'Meta': {'object_name': 'Network'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '32', 'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'external_router': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'flavor': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'floating_ip_pool': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'link': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'machines': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.VirtualMachine']", 'through': "orm['db.NetworkInterface']", 'symmetrical': 'False'}), + 'mode': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_index': 'True'}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'network'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '32'}), + 'subnet_ids': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'tags': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.networkinterface': { + 'Meta': {'object_name': 'NetworkInterface'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'device_owner': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'firewall_profile': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'mac': ('django.db.models.fields.CharField', [], {'max_length': '32', 'unique': 'True', 'null': 'True'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.VirtualMachine']"}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'security_groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.SecurityGroup']", 'null': 'True', 'symmetrical': 'False'}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'ACTIVE'", 'max_length': '32'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.quotaholderserial': { + 'Meta': {'ordering': "['serial']", 'object_name': 'QuotaHolderSerial'}, + 'accept': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pending': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'resolved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'serial': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_index': 'True'}) + }, + 'db.securitygroup': { + 'Meta': {'object_name': 'SecurityGroup'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'db.subnet': { + 'Meta': {'object_name': 'Subnet'}, + 'cidr': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'dhcp': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'dns_nameservers': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'gateway': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'host_routes': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'subnets'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.virtualmachine': { + 'Meta': {'object_name': 'VirtualMachine'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '30', 'null': 'True'}), + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machines'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backend_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'buildpercentage': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'flavor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.Flavor']", 'on_delete': 'models.PROTECT'}), + 'hostid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image_version': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'imageid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'BUILD'", 'max_length': '30'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_index': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machine'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'suspended': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'task': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'task_job_id': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}) + }, + 'db.virtualmachinediagnostic': { + 'Meta': {'ordering': "['-created']", 'object_name': 'VirtualMachineDiagnostic'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'details': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'diagnostics'", 'to': "orm['db.VirtualMachine']"}), + 'message': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'source_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}) + }, + 'db.virtualmachinemetadata': { + 'Meta': {'unique_together': "(('meta_key', 'vm'),)", 'object_name': 'VirtualMachineMetadata'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'meta_key': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'meta_value': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'vm': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'metadata'", 'to': "orm['db.VirtualMachine']"}) + }, + 'db.volume': { + 'Meta': {'object_name': 'Volume'}, + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'delete_on_termination': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'volumes'", 'null': 'True', 'to': "orm['db.VirtualMachine']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'origin': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_index': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'volume'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'snapshot_counter': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'source_version': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'CREATING'", 'max_length': '64'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}), + 'volume_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'volumes'", 'on_delete': 'models.PROTECT', 'to': "orm['db.VolumeType']"}) + }, + 'db.volumemetadata': { + 'Meta': {'unique_together': "(('volume', 'key'),)", 'object_name': 'VolumeMetadata'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'volume': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'metadata'", 'to': "orm['db.Volume']"}) + }, + 'db.volumetype': { + 'Meta': {'object_name': 'VolumeType'}, + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'disk_template': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + } + } + + complete_apps = ['db'] diff --git a/snf-cyclades-app/synnefo/db/migrations/0116_auto__add_image__add_unique_image_uuid_version.py b/snf-cyclades-app/synnefo/db/migrations/0116_auto__add_image__add_unique_image_uuid_version.py new file mode 100644 index 0000000000000000000000000000000000000000..9d4c35a5e6dc8ddb58eda75091473d75bd35c933 --- /dev/null +++ b/snf-cyclades-app/synnefo/db/migrations/0116_auto__add_image__add_unique_image_uuid_version.py @@ -0,0 +1,312 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'Image' + db.create_table('db_image', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('uuid', self.gf('django.db.models.fields.CharField')(max_length=128)), + ('version', self.gf('django.db.models.fields.IntegerField')()), + ('owner', self.gf('django.db.models.fields.CharField')(max_length=128)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=256)), + ('location', self.gf('django.db.models.fields.TextField')()), + ('mapfile', self.gf('django.db.models.fields.CharField')(max_length=256)), + ('is_public', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('is_snapshot', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('is_system', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('os', self.gf('django.db.models.fields.CharField')(max_length=256)), + ('osfamily', self.gf('django.db.models.fields.CharField')(max_length=256)), + )) + db.send_create_signal('db', ['Image']) + + # Adding unique constraint on 'Image', fields ['uuid', 'version'] + db.create_unique('db_image', ['uuid', 'version']) + + + def backwards(self, orm): + # Removing unique constraint on 'Image', fields ['uuid', 'version'] + db.delete_unique('db_image', ['uuid', 'version']) + + # Deleting model 'Image' + db.delete_table('db_image') + + + models = { + 'db.backend': { + 'Meta': {'ordering': "['clustername']", 'object_name': 'Backend'}, + 'clustername': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}), + 'ctotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'dfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'disk_templates': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'dtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'hash': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'hypervisor': ('django.db.models.fields.CharField', [], {'default': "'kvm'", 'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'unique': 'True'}), + 'mfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'mtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'offline': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'password_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), + 'pinst_cnt': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'port': ('django.db.models.fields.PositiveIntegerField', [], {'default': '5080'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}) + }, + 'db.backendnetwork': { + 'Meta': {'unique_together': "(('network', 'backend'),)", 'object_name': 'BackendNetwork'}, + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'backend_networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '30'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + 'db.bridgepooltable': { + 'Meta': {'object_name': 'BridgePoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.flavor': { + 'Meta': {'unique_together': "(('cpu', 'ram', 'disk', 'volume_type'),)", 'object_name': 'Flavor'}, + 'allow_create': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cpu': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'disk': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ram': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'volume_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'flavors'", 'on_delete': 'models.PROTECT', 'to': "orm['db.VolumeType']"}) + }, + 'db.image': { + 'Meta': {'unique_together': "(('uuid', 'version'),)", 'object_name': 'Image'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'location': ('django.db.models.fields.TextField', [], {}), + 'mapfile': ('django.db.models.fields.CharField', [], {'max_length': '256'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '256'}), + 'os': ('django.db.models.fields.CharField', [], {'max_length': '256'}), + 'osfamily': ('django.db.models.fields.CharField', [], {'max_length': '256'}), + 'owner': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_snapshot': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_system': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'uuid': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'version': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.ipaddress': { + 'Meta': {'unique_together': "(('network', 'address', 'deleted'),)", 'object_name': 'IPAddress'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'floating_ip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'nic': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.NetworkInterface']"}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_index': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.ipaddresslog': { + 'Meta': {'object_name': 'IPAddressLog'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}), + 'allocated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'network_id': ('django.db.models.fields.IntegerField', [], {}), + 'released_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'server_id': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.ippooltable': { + 'Meta': {'object_name': 'IPPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ip_pools'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}) + }, + 'db.macprefixpooltable': { + 'Meta': {'object_name': 'MacPrefixPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.network': { + 'Meta': {'object_name': 'Network'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '32', 'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'external_router': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'flavor': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'floating_ip_pool': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'link': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'machines': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.VirtualMachine']", 'through': "orm['db.NetworkInterface']", 'symmetrical': 'False'}), + 'mode': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_index': 'True'}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'network'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '32'}), + 'subnet_ids': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'tags': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.networkinterface': { + 'Meta': {'object_name': 'NetworkInterface'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'device_owner': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'firewall_profile': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'mac': ('django.db.models.fields.CharField', [], {'max_length': '32', 'unique': 'True', 'null': 'True'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.VirtualMachine']"}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'security_groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.SecurityGroup']", 'null': 'True', 'symmetrical': 'False'}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'ACTIVE'", 'max_length': '32'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.quotaholderserial': { + 'Meta': {'ordering': "['serial']", 'object_name': 'QuotaHolderSerial'}, + 'accept': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pending': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'resolved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'serial': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_index': 'True'}) + }, + 'db.securitygroup': { + 'Meta': {'object_name': 'SecurityGroup'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'db.subnet': { + 'Meta': {'object_name': 'Subnet'}, + 'cidr': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'dhcp': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'dns_nameservers': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'gateway': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'host_routes': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'subnets'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.virtualmachine': { + 'Meta': {'object_name': 'VirtualMachine'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '30', 'null': 'True'}), + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machines'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backend_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'buildpercentage': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'flavor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.Flavor']", 'on_delete': 'models.PROTECT'}), + 'hostid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image_version': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'imageid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'BUILD'", 'max_length': '30'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_index': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machine'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'suspended': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'task': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'task_job_id': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}) + }, + 'db.virtualmachinediagnostic': { + 'Meta': {'ordering': "['-created']", 'object_name': 'VirtualMachineDiagnostic'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'details': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'diagnostics'", 'to': "orm['db.VirtualMachine']"}), + 'message': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'source_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}) + }, + 'db.virtualmachinemetadata': { + 'Meta': {'unique_together': "(('meta_key', 'vm'),)", 'object_name': 'VirtualMachineMetadata'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'meta_key': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'meta_value': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'vm': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'metadata'", 'to': "orm['db.VirtualMachine']"}) + }, + 'db.volume': { + 'Meta': {'object_name': 'Volume'}, + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'delete_on_termination': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'volumes'", 'null': 'True', 'to': "orm['db.VirtualMachine']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'origin': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_index': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'volume'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'snapshot_counter': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'source_version': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'CREATING'", 'max_length': '64'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}), + 'volume_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'volumes'", 'on_delete': 'models.PROTECT', 'to': "orm['db.VolumeType']"}) + }, + 'db.volumemetadata': { + 'Meta': {'unique_together': "(('volume', 'key'),)", 'object_name': 'VolumeMetadata'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'volume': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'metadata'", 'to': "orm['db.Volume']"}) + }, + 'db.volumetype': { + 'Meta': {'object_name': 'VolumeType'}, + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'disk_template': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + } + } + + complete_apps = ['db'] diff --git a/snf-cyclades-app/synnefo/db/migrations/0117_fix_migration_bug.py b/snf-cyclades-app/synnefo/db/migrations/0117_fix_migration_bug.py new file mode 100644 index 0000000000000000000000000000000000000000..24f7ce22ea340eae600dbfd4b0ef490d60ea1e0a --- /dev/null +++ b/snf-cyclades-app/synnefo/db/migrations/0117_fix_migration_bug.py @@ -0,0 +1,301 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + + def forwards(self, orm): + "Write your forwards methods here." + # This migration is the same as 0112. It run again to fix a bug that + # existed in migration 0112 in 0.16rc1. + networks = dict(orm.Network.objects.values_list("id", "subnet_ids")) + n_subs = {} + for (subnet_id, network_id) in orm.Subnet.objects.values_list("id", "network_id"): + if (network_id not in networks or + str(subnet_id) != str(networks[network_id])): + subnet_ids = n_subs.setdefault(network_id, []) + subnet_ids.append(subnet_id) + for network_id, subnet_ids in n_subs.items(): + updated = orm.Network.objects.filter(id=network_id).update(subnet_ids=subnet_ids) + assert(updated == 1) + + + def backwards(self, orm): + "Write your backwards methods here." + + models = { + 'db.backend': { + 'Meta': {'ordering': "['clustername']", 'object_name': 'Backend'}, + 'clustername': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}), + 'ctotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'dfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'disk_templates': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'dtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'hash': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'hypervisor': ('django.db.models.fields.CharField', [], {'default': "'kvm'", 'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'unique': 'True'}), + 'mfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'mtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'offline': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'password_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), + 'pinst_cnt': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'port': ('django.db.models.fields.PositiveIntegerField', [], {'default': '5080'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}) + }, + 'db.backendnetwork': { + 'Meta': {'unique_together': "(('network', 'backend'),)", 'object_name': 'BackendNetwork'}, + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'backend_networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '30'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + 'db.bridgepooltable': { + 'Meta': {'object_name': 'BridgePoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.flavor': { + 'Meta': {'unique_together': "(('cpu', 'ram', 'disk', 'volume_type'),)", 'object_name': 'Flavor'}, + 'allow_create': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cpu': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'disk': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ram': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'volume_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'flavors'", 'on_delete': 'models.PROTECT', 'to': "orm['db.VolumeType']"}) + }, + 'db.image': { + 'Meta': {'unique_together': "(('uuid', 'version'),)", 'object_name': 'Image'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_snapshot': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_system': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'location': ('django.db.models.fields.TextField', [], {}), + 'mapfile': ('django.db.models.fields.CharField', [], {'max_length': '256'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '256'}), + 'os': ('django.db.models.fields.CharField', [], {'max_length': '256'}), + 'osfamily': ('django.db.models.fields.CharField', [], {'max_length': '256'}), + 'owner': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'uuid': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'version': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.ipaddress': { + 'Meta': {'unique_together': "(('network', 'address', 'deleted'),)", 'object_name': 'IPAddress'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'floating_ip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'nic': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.NetworkInterface']"}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_index': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ips'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.ipaddresslog': { + 'Meta': {'object_name': 'IPAddressLog'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}), + 'allocated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'network_id': ('django.db.models.fields.IntegerField', [], {}), + 'released_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'server_id': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.ippooltable': { + 'Meta': {'object_name': 'IPPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ip_pools'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Subnet']"}) + }, + 'db.macprefixpooltable': { + 'Meta': {'object_name': 'MacPrefixPoolTable'}, + 'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'db.network': { + 'Meta': {'object_name': 'Network'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '32', 'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'external_router': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'flavor': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'floating_ip_pool': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'link': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'machines': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.VirtualMachine']", 'through': "orm['db.NetworkInterface']", 'symmetrical': 'False'}), + 'mode': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_index': 'True'}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'network'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '32'}), + 'subnet_ids': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'tags': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.networkinterface': { + 'Meta': {'object_name': 'NetworkInterface'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'device_owner': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'firewall_profile': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'mac': ('django.db.models.fields.CharField', [], {'max_length': '32', 'unique': 'True', 'null': 'True'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.VirtualMachine']"}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'security_groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.SecurityGroup']", 'null': 'True', 'symmetrical': 'False'}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'ACTIVE'", 'max_length': '32'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'db.quotaholderserial': { + 'Meta': {'ordering': "['serial']", 'object_name': 'QuotaHolderSerial'}, + 'accept': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pending': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'resolved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'serial': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True', 'db_index': 'True'}) + }, + 'db.securitygroup': { + 'Meta': {'object_name': 'SecurityGroup'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'db.subnet': { + 'Meta': {'object_name': 'Subnet'}, + 'cidr': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'dhcp': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'dns_nameservers': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'gateway': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'host_routes': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipversion': ('django.db.models.fields.IntegerField', [], {'default': '4'}), + 'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'null': 'True'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'subnets'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Network']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}) + }, + 'db.virtualmachine': { + 'Meta': {'object_name': 'VirtualMachine'}, + 'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '30', 'null': 'True'}), + 'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machines'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}), + 'backend_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}), + 'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}), + 'buildpercentage': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'flavor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.Flavor']", 'on_delete': 'models.PROTECT'}), + 'hostid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image_version': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'imageid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'operstate': ('django.db.models.fields.CharField', [], {'default': "'BUILD'", 'max_length': '30'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_index': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machine'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'suspended': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'task': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'task_job_id': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}) + }, + 'db.virtualmachinediagnostic': { + 'Meta': {'ordering': "['-created']", 'object_name': 'VirtualMachineDiagnostic'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'details': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'diagnostics'", 'to': "orm['db.VirtualMachine']"}), + 'message': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'source_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}) + }, + 'db.virtualmachinemetadata': { + 'Meta': {'unique_together': "(('meta_key', 'vm'),)", 'object_name': 'VirtualMachineMetadata'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'meta_key': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'meta_value': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'vm': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'metadata'", 'to': "orm['db.VirtualMachine']"}) + }, + 'db.volume': { + 'Meta': {'object_name': 'Volume'}, + 'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'delete_on_termination': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'volumes'", 'null': 'True', 'to': "orm['db.VirtualMachine']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'origin': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'project': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_index': 'True'}), + 'serial': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'volume'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['db.QuotaHolderSerial']"}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'snapshot_counter': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'source_version': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'CREATING'", 'max_length': '64'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'userid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}), + 'volume_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'volumes'", 'on_delete': 'models.PROTECT', 'to': "orm['db.VolumeType']"}) + }, + 'db.volumemetadata': { + 'Meta': {'unique_together': "(('volume', 'key'),)", 'object_name': 'VolumeMetadata'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'volume': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'metadata'", 'to': "orm['db.Volume']"}) + }, + 'db.volumetype': { + 'Meta': {'object_name': 'VolumeType'}, + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'disk_template': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + } + } + + complete_apps = ['db'] + symmetrical = True diff --git a/snf-cyclades-app/synnefo/db/models.py b/snf-cyclades-app/synnefo/db/models.py index 5c5f65ae19171b55e2ca82985b0ddfc6e8597202..75bc1bc785485c2a77b588fe9e620841333d9608 100644 --- a/snf-cyclades-app/synnefo/db/models.py +++ b/snf-cyclades-app/synnefo/db/models.py @@ -1,31 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import datetime @@ -49,27 +35,58 @@ import logging log = logging.getLogger(__name__) +class VolumeType(models.Model): + NAME_LENGTH = 255 + DISK_TEMPLATE_LENGTH = 32 + name = models.CharField("Name", max_length=NAME_LENGTH) + disk_template = models.CharField('Disk Template', + max_length=DISK_TEMPLATE_LENGTH) + deleted = models.BooleanField('Deleted', default=False) + + def __str__(self): + return self.__unicode__() + + def __unicode__(self): + return u"<VolumeType %s(disk_template:%s)>" % \ + (self.name, self.disk_template) + + @property + def template(self): + return self.disk_template.split("_")[0] + + @property + def provider(self): + if "_" in self.disk_template: + return self.disk_template.split("_", 1)[1] + else: + return None + + class Flavor(models.Model): cpu = models.IntegerField('Number of CPUs', default=0) ram = models.IntegerField('RAM size in MiB', default=0) disk = models.IntegerField('Disk size in GiB', default=0) - disk_template = models.CharField('Disk template', max_length=32) + volume_type = models.ForeignKey(VolumeType, related_name="flavors", + on_delete=models.PROTECT, null=False) deleted = models.BooleanField('Deleted', default=False) # Whether the flavor can be used to create new servers allow_create = models.BooleanField(default=True, null=False) class Meta: verbose_name = u'Virtual machine flavor' - unique_together = ('cpu', 'ram', 'disk', 'disk_template') + unique_together = ('cpu', 'ram', 'disk', 'volume_type') @property def name(self): """Returns flavor name (generated)""" - return u'C%dR%dD%d%s' % (self.cpu, self.ram, self.disk, - self.disk_template) + return u'C%sR%sD%s%s' % (self.cpu, self.ram, self.disk, + self.volume_type.disk_template) + + def __str__(self): + return self.__unicode__() def __unicode__(self): - return "<%s:%s>" % (str(self.id), self.name) + return u"<%s:%s>" % (self.id, self.name) class Backend(models.Model): @@ -113,8 +130,11 @@ class Backend(models.Model): verbose_name = u'Backend' ordering = ["clustername"] + def __str__(self): + return self.__unicode__() + def __unicode__(self): - return self.clustername + "(id=" + str(self.id) + ")" + return u"%s(id:%s)" % (self.clustername, self.id) @property def backend_id(self): @@ -213,6 +233,9 @@ class QuotaHolderSerial(models.Model): verbose_name = u'Quota Serial' ordering = ["serial"] + def __str__(self): + return self.__unicode__() + def __unicode__(self): return u"<serial: %s>" % self.serial @@ -303,6 +326,7 @@ class VirtualMachine(models.Model): max_length=VIRTUAL_MACHINE_NAME_LENGTH) userid = models.CharField('User ID of the owner', max_length=100, db_index=True, null=False) + project = models.CharField(max_length=255, null=True, db_index=True) backend = models.ForeignKey(Backend, null=True, related_name="virtual_machines", on_delete=models.PROTECT) @@ -310,6 +334,7 @@ class VirtualMachine(models.Model): created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) imageid = models.CharField(max_length=100, null=False) + image_version = models.IntegerField(null=True) hostid = models.CharField(max_length=100) flavor = models.ForeignKey(Flavor, on_delete=models.PROTECT) deleted = models.BooleanField('Deleted', default=False, db_index=True) @@ -379,11 +404,14 @@ class VirtualMachine(models.Model): verbose_name = u'Virtual machine instance' get_latest_by = 'created' + def __str__(self): + return self.__unicode__() + def __unicode__(self): return u"<vm:%s@backend:%s>" % (self.id, self.backend_id) # Error classes - class InvalidBackendIdError(Exception): + class InvalidBackendIdError(ValueError): def __init__(self, value): self.value = value @@ -419,8 +447,35 @@ class VirtualMachineMetadata(models.Model): unique_together = (('meta_key', 'vm'),) verbose_name = u'Key-value pair of metadata for a VM.' + def __str__(self): + return self.__unicode__() + def __unicode__(self): - return u'%s: %s' % (self.meta_key, self.meta_value) + return u'<Metadata %s: %s>' % (self.meta_key, self.meta_value) + + +class Image(models.Model): + """Model representing Images of created VirtualMachines. + + This model stores basic information about Images which have been used to + create VirtualMachines or Volumes. + + """ + + uuid = models.CharField(max_length=128) + version = models.IntegerField(null=False) + owner = models.CharField(max_length=128, null=False) + name = models.CharField(max_length=256, null=False) + location = models.TextField() + mapfile = models.CharField(max_length=256, null=False) + is_public = models.BooleanField(default=False, null=False) + is_snapshot = models.BooleanField(default=False, null=False) + is_system = models.BooleanField(default=False, null=False) + os = models.CharField(max_length=256) + osfamily = models.CharField(max_length=256) + + class Meta: + unique_together = (('uuid', 'version'),) class Network(models.Model): @@ -485,6 +540,7 @@ class Network(models.Model): name = models.CharField('Network Name', max_length=NETWORK_NAME_LENGTH) userid = models.CharField('User ID of the owner', max_length=128, null=True, db_index=True) + project = models.CharField(max_length=255, null=True, db_index=True) flavor = models.CharField('Flavor', max_length=32, null=False) mode = models.CharField('Network Mode', max_length=16, null=True) link = models.CharField('Network Link', max_length=32, null=True) @@ -506,9 +562,13 @@ class Network(models.Model): external_router = models.BooleanField(default=False) serial = models.ForeignKey(QuotaHolderSerial, related_name='network', null=True, on_delete=models.SET_NULL) + subnet_ids = fields.SeparatedValuesField("Subnet IDs", null=True) + + def __str__(self): + return self.__unicode__() def __unicode__(self): - return "<Network: %s>" % str(self.id) + return u"<Network: %s>" % self.id @property def backend_id(self): @@ -586,7 +646,7 @@ class Network(models.Model): free += ip_pool.count_available() return total, free - class InvalidBackendIdError(Exception): + class InvalidBackendIdError(ValueError): def __init__(self, value): self.value = value @@ -633,6 +693,9 @@ class Subnet(models.Model): created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) + def __str__(self): + return self.__unicode__() + def __unicode__(self): msg = u"<Subnet %s, Network: %s, CIDR: %s>" return msg % (self.id, self.network_id, self.cidr) @@ -716,8 +779,11 @@ class BackendNetwork(models.Model): mac_prefix) self.mac_prefix = mac_prefix + def __str__(self): + return self.__unicode__() + def __unicode__(self): - return '<%s@%s>' % (self.network, self.backend) + return u'<BackendNetwork %s@%s>' % (self.network, self.backend) class IPAddress(models.Model): @@ -729,6 +795,7 @@ class IPAddress(models.Model): on_delete=models.SET_NULL) userid = models.CharField("UUID of the owner", max_length=128, null=False, db_index=True) + project = models.CharField(max_length=255, null=True, db_index=True) address = models.CharField("IP Address", max_length=64, null=False) floating_ip = models.BooleanField("Floating IP", null=False, default=False) ipversion = models.IntegerField("IP Version", null=False) @@ -740,6 +807,9 @@ class IPAddress(models.Model): related_name="ips", null=True, on_delete=models.SET_NULL) + def __str__(self): + return self.__unicode__() + def __unicode__(self): ip_type = "floating" if self.floating_ip else "static" return u"<IPAddress: %s, Network: %s, Subnet: %s, Type: %s>"\ @@ -754,10 +824,6 @@ class IPAddress(models.Model): class Meta: unique_together = ("network", "address", "deleted") - @property - def public(self): - return self.network.public - def release_address(self): """Release the IPv4 address.""" if self.ipversion == 4: @@ -784,6 +850,9 @@ class IPAddressLog(models.Model): active = models.BooleanField("Whether IP still allocated to server", default=True) + def __str__(self): + return self.__unicode__() + def __unicode__(self): return u"<Address: %s, Server: %s, Network: %s, Allocated at: %s>"\ % (self.address, self.network_id, self.server_id, @@ -823,16 +892,20 @@ class NetworkInterface(models.Model): security_groups = models.ManyToManyField("SecurityGroup", null=True) state = models.CharField(max_length=32, null=False, default="ACTIVE", choices=STATES) + public = models.BooleanField(default=False) device_owner = models.CharField('Device owner', max_length=128, null=True) + def __str__(self): + return self.__unicode__() + def __unicode__(self): - return "<%s:vm:%s network:%s>" % (self.id, self.machine_id, - self.network_id) + return u"<NIC %s:vm:%s network:%s>" % \ + (self.id, self.machine_id, self.network_id) @property def backend_uuid(self): """Return the backend id by prepending backend-prefix.""" - return "%snic-%s" % (settings.BACKEND_PREFIX_ID, str(self.id)) + return u"%snic-%s" % (settings.BACKEND_PREFIX_ID, str(self.id)) @property def ipv4_address(self): @@ -891,6 +964,9 @@ class PoolTable(models.Model): class BridgePoolTable(PoolTable): manager = pools.BridgePool + def __str__(self): + return self.__unicode__() + def __unicode__(self): return u"<BridgePool id:%s>" % self.id @@ -898,6 +974,9 @@ class BridgePoolTable(PoolTable): class MacPrefixPoolTable(PoolTable): manager = pools.MacPrefixPool + def __str__(self): + return self.__unicode__() + def __unicode__(self): return u"<MACPrefixPool id:%s>" % self.id @@ -909,6 +988,9 @@ class IPPoolTable(PoolTable): on_delete=models.PROTECT, db_index=True, null=True) + def __str__(self): + return self.__unicode__() + def __unicode__(self): return u"<IPv4AdressPool, Subnet: %s>" % self.subnet_id @@ -984,3 +1066,138 @@ class VirtualMachineDiagnostic(models.Model): class Meta: ordering = ['-created'] + + +class Volume(models.Model): + """Model representing a detachable block storage device.""" + + STATUS_VALUES = ( + ("CREATING", "The volume is being created"), + ("AVAILABLE", "The volume is ready to be attached to an instance"), + ("ATTACHING", "The volume is attaching to an instance"), + ("DETACHING", "The volume is detaching from an instance"), + ("IN_USE", "The volume is attached to an instance"), + ("DELETING", "The volume is being deleted"), + ("DELETED", "The volume has been deleted"), + ("ERROR", "An error has occured with the volume"), + ("ERROR_DELETING", "There was an error deleting this volume"), + ("BACKING_UP", "The volume is being backed up"), + ("RESTORING_BACKUP", "A backup is being restored to the volume"), + ("ERROR_RESTORING", "There was an error restoring a backup from the" + " volume") + ) + + NAME_LENGTH = 255 + DESCRIPTION_LENGTH = 255 + SOURCE_IMAGE_PREFIX = "image:" + SOURCE_SNAPSHOT_PREFIX = "snapshot:" + SOURCE_VOLUME_PREFIX = "volume:" + + name = models.CharField("Name", max_length=NAME_LENGTH, null=True) + description = models.CharField("Description", + max_length=DESCRIPTION_LENGTH, null=True) + userid = models.CharField("Owner's UUID", max_length=100, null=False, + db_index=True) + project = models.CharField(max_length=255, null=True, db_index=True) + size = models.IntegerField("Volume size in GB", null=False) + volume_type = models.ForeignKey(VolumeType, related_name="volumes", + on_delete=models.PROTECT, null=False) + + delete_on_termination = models.BooleanField("Delete on Server Termination", + default=True, null=False) + + source = models.CharField(max_length=128, null=True) + source_version = models.IntegerField(null=True) + origin = models.CharField(max_length=128, null=True) + + deleted = models.BooleanField("Deleted", default=False, null=False, + db_index=True) + # Datetime fields + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + # Status + status = models.CharField("Status", max_length=64, + choices=STATUS_VALUES, + default="CREATING", null=False) + snapshot_counter = models.PositiveIntegerField(default=0, null=False) + + machine = models.ForeignKey("VirtualMachine", + related_name="volumes", + null=True) + index = models.IntegerField("Index", null=True) + backendjobid = models.PositiveIntegerField(null=True) + serial = models.ForeignKey(QuotaHolderSerial, related_name='volume', + null=True, on_delete=models.SET_NULL) + + @property + def backend_volume_uuid(self): + return u"%svol-%d" % (settings.BACKEND_PREFIX_ID, self.id) + + @property + def backend_disk_uuid(self): + return u"%sdisk-%d" % (settings.BACKEND_PREFIX_ID, self.id) + + @property + def source_image_id(self): + src = self.source + if src and src.startswith(self.SOURCE_IMAGE_PREFIX): + return src[len(self.SOURCE_IMAGE_PREFIX):] + else: + return None + + @property + def source_snapshot_id(self): + src = self.source + if src and src.startswith(self.SOURCE_SNAPSHOT_PREFIX): + return src[len(self.SOURCE_SNAPSHOT_PREFIX):] + else: + return None + + @property + def source_volume_id(self): + src = self.source + if src and src.startswith(self.SOURCE_VOLUME_PREFIX): + return src[len(self.SOURCE_VOLUME_PREFIX):] + else: + return None + + @staticmethod + def prefix_source(source_id, source_type): + if source_type == "volume": + return Volume.SOURCE_VOLUME_PREFIX + str(source_id) + if source_type == "snapshot": + return Volume.SOURCE_SNAPSHOT_PREFIX + str(source_id) + if source_type == "image": + return Volume.SOURCE_IMAGE_PREFIX + str(source_id) + elif source_type == "blank": + return None + + def __str__(self): + return self.__unicode__() + + def __unicode__(self): + return u"<Volume %s:vm:%s>" % (self.id, self.machine_id) + + +class Metadata(models.Model): + KEY_LENGTH = 64 + VALUE_LENGTH = 255 + key = models.CharField("Metadata Key", max_length=KEY_LENGTH) + value = models.CharField("Metadata Value", max_length=VALUE_LENGTH) + + class Meta: + abstract = True + + def __str__(self): + return self.__unicode__() + + def __unicode__(self): + return u"<%s: %s>" % (self.key, self.value) + + +class VolumeMetadata(Metadata): + volume = models.ForeignKey("Volume", related_name="metadata") + + class Meta: + unique_together = (("volume", "key"),) + verbose_name = u"Key-Value pair of Volumes metadata" diff --git a/snf-cyclades-app/synnefo/db/models_factory.py b/snf-cyclades-app/synnefo/db/models_factory.py index d6aeefdb0c21174aacd22d4e0805e2458203fc99..f1df537c2fd226b5f25a8446a9df73189bafd861 100644 --- a/snf-cyclades-app/synnefo/db/models_factory.py +++ b/snf-cyclades-app/synnefo/db/models_factory.py @@ -1,37 +1,20 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import factory +from factory.fuzzy import FuzzyChoice from synnefo.db import models from random import choice from string import letters, digits @@ -60,13 +43,23 @@ def random_string(x): return ''.join([choice(digits + letters) for i in range(x)]) +class VolumeTypeFactory(factory.DjangoModelFactory): + FACTORY_FOR = models.VolumeType + FACTORY_DJANGO_GET_OR_CREATE = ("disk_template",) + name = factory.Sequence(prefix_seq("vtype")) + disk_template = FuzzyChoice( + choices=["file", "plain", "drbd", "ext_archipelago"] + ) + deleted = False + + class FlavorFactory(factory.DjangoModelFactory): FACTORY_FOR = models.Flavor cpu = factory.Sequence(lambda n: n + 2, type=int) ram = factory.Sequence(lambda n: n * 512, type=int) - disk = factory.Sequence(lambda n: n * 10, type=int) - disk_template = 'drbd' + disk = factory.Sequence(lambda n: n * 1, type=int) + volume_type = factory.SubFactory(VolumeTypeFactory) deleted = False @@ -87,7 +80,7 @@ class BackendFactory(factory.DjangoModelFactory): pinst_cnt = 2 ctotal = 80 - disk_templates = ["file", "plain", "drbd"] + disk_templates = ["file", "plain", "drbd", "ext"] class DrainedBackend(BackendFactory): @@ -110,6 +103,18 @@ class VirtualMachineFactory(factory.DjangoModelFactory): suspended = False #operstate = factory.Sequence(round_seq_first(FACTORY_FOR.OPER_STATES)) operstate = "STARTED" + project = factory.LazyAttribute(lambda a: a.userid) + + +class VolumeFactory(factory.DjangoModelFactory): + FACTORY_FOR = models.Volume + userid = factory.Sequence(user_seq()) + size = factory.Sequence(lambda x: x, type=int) + name = factory.Sequence(lambda x: "volume-name-"+x, type=str) + machine = factory.SubFactory(VirtualMachineFactory, + userid=factory.SelfAttribute('..userid')) + volume_type = factory.SubFactory(VolumeTypeFactory) + project = factory.LazyAttribute(lambda a: a.userid) class DeletedVirtualMachine(VirtualMachineFactory): @@ -159,6 +164,7 @@ class NetworkFactory(factory.DjangoModelFactory): public = False deleted = False state = "ACTIVE" + project = factory.LazyAttribute(lambda a: a.userid) class DeletedNetwork(NetworkFactory): @@ -183,6 +189,7 @@ class NetworkInterfaceFactory(factory.DjangoModelFactory): index = factory.Sequence(lambda x: x, type=int) mac = factory.Sequence(lambda n: 'aa:{0}{0}:{0}{0}:aa:{0}{0}:{0}{0}' .format(hex(int(n) % 15)[2:3])) + public = factory.LazyAttribute(lambda self: self.network.public) state = "ACTIVE" firewall_profile =\ factory.Sequence(round_seq_first(FACTORY_FOR.FIREWALL_PROFILES)) @@ -237,6 +244,7 @@ class IPv4AddressFactory(factory.DjangoModelFactory): nic = factory.SubFactory(NetworkInterfaceFactory, userid=factory.SelfAttribute('..userid'), network=factory.SelfAttribute('..network')) + project = factory.LazyAttribute(lambda a: a.userid) class IPv6AddressFactory(IPv4AddressFactory): diff --git a/snf-cyclades-app/synnefo/db/pools/__init__.py b/snf-cyclades-app/synnefo/db/pools/__init__.py index b1d91d3859d10e8aebc4444b00432933d93f1432..1269c5e8229c69ea8698c0b37db9b80050411aaf 100644 --- a/snf-cyclades-app/synnefo/db/pools/__init__.py +++ b/snf-cyclades-app/synnefo/db/pools/__init__.py @@ -1,31 +1,17 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from bitarray import bitarray from base64 import b64encode, b64decode diff --git a/snf-cyclades-app/synnefo/db/pools/tests.py b/snf-cyclades-app/synnefo/db/pools/tests.py index b8c72e61e114a02a991a18e420b0ccceacd5a4e0..eb62cdd37d827d4d59618b865e9bb1ed0bb7c2fa 100644 --- a/snf-cyclades-app/synnefo/db/pools/tests.py +++ b/snf-cyclades-app/synnefo/db/pools/tests.py @@ -1,35 +1,17 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.test import TestCase diff --git a/snf-cyclades-app/synnefo/db/query.py b/snf-cyclades-app/synnefo/db/query.py index 364288a71998277d3d1b3f7180815adbaa2b1ad4..825b02d914671be307ed0f8f2012036f4e5de01c 100644 --- a/snf-cyclades-app/synnefo/db/query.py +++ b/snf-cyclades-app/synnefo/db/query.py @@ -1,31 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from synnefo.db.models import IPAddress diff --git a/snf-cyclades-app/synnefo/db/tests.py b/snf-cyclades-app/synnefo/db/tests.py index 258c4f56080fc11e9e853fcb9632139c07e3e85e..04e1bc4feb262673aa139a9ba8779815ab068783 100644 --- a/snf-cyclades-app/synnefo/db/tests.py +++ b/snf-cyclades-app/synnefo/db/tests.py @@ -1,45 +1,33 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # Unit Tests for db # # Provides automated tests for db module -from django.test import TestCase - +from django.test import TestCase, TransactionTestCase +from django.db import transaction as django_transaction from django.conf import settings + # Import pool tests from synnefo.db.pools.tests import * - from synnefo.db.models import * + from synnefo.db import models_factory as mfact from synnefo.db.pools import IPPool, EmptyPool +from synnefo.db import transaction as cyclades_transaction from django.db import IntegrityError from django.core.exceptions import MultipleObjectsReturned @@ -51,7 +39,7 @@ class FlavorTest(TestCase): def test_flavor_name(self): """Test a flavor object name method.""" flavor = mfact.FlavorFactory(cpu=1, ram=1024, disk=40, - disk_template="temp") + volume_type__disk_template="temp") self.assertEqual( flavor.name, "C1R1024D40temp", "flavor.name is not" " generated correctly. Name is %s instead of C1R1024D40temp" % @@ -264,3 +252,55 @@ class AESTest(TestCase): '91490231234814234812348913289481294812398421893489' self.assertRaises(ValueError, aes.encrypt_db_charfield, 'la') aes.SECRET_ENCRYPTION_KEY = old + + +class TransactionException(Exception): + + """A dummy exception specifically for the transaction tests.""" + + pass + + +class TransactionTest(TransactionTestCase): + + """Check if cyclades transactions work properly. + + TODO: Add multi-db tests. + """ + + def good_transaction(self): + mfact.VirtualMachineFactory() + + def bad_transaction(self): + self.good_transaction() + raise TransactionException + + def test_good_transaction(self): + django_transaction.commit_on_success(self.good_transaction)() + self.assertEqual(VirtualMachine.objects.count(), 1) + + def test_bad_transaction(self): + with self.assertRaises(TransactionException): + django_transaction.commit_on_success(self.bad_transaction)() + self.assertEqual(VirtualMachine.objects.count(), 0) + + def test_good_transaction_custom_decorator(self): + cyclades_transaction.commit_on_success(self.good_transaction)() + self.assertEqual(VirtualMachine.objects.count(), 1) + + def test_bad_transaction_custom_decorator(self): + with self.assertRaises(TransactionException): + cyclades_transaction.commit_on_success(self.bad_transaction)() + self.assertEqual(VirtualMachine.objects.count(), 0) + + def test_bad_transaction_custom_decorator_incorrect_dbs(self): + settings.DATABASES['cyclades'] = settings.DATABASES['default'] + with self.assertRaises(TransactionException): + cyclades_transaction.commit_on_success(self.bad_transaction)() + self.assertEqual(VirtualMachine.objects.count(), 0) + settings.DATABASES.pop("cyclades") + + def test_bad_transaction_custom_decorator_using(self): + with self.assertRaises(TransactionException): + cyclades_transaction.commit_on_success(using="default")(self.bad_transaction)() + self.assertEqual(VirtualMachine.objects.count(), 0) diff --git a/snf-cyclades-app/synnefo/db/transaction.py b/snf-cyclades-app/synnefo/db/transaction.py new file mode 100644 index 0000000000000000000000000000000000000000..58ce020c56576fa4b1394222717bfec8470f2f08 --- /dev/null +++ b/snf-cyclades-app/synnefo/db/transaction.py @@ -0,0 +1,54 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# Astakos-specific support for transactions in multiple databases. This file +# provides the entry points for the "commit_on_success"/"commit_manually" +# Django transaction functions. + +"""Cyclades-specific support for transactions in multiple databases. + +This file provides the entry points for the following Django transaction +functions: + * commit_on_success + * commit_manually + * commit + * rollback +""" + + +from django.db import transaction + +from snf_django.utils.transaction import _transaction_func +from snf_django.utils.db import select_db + + +def commit(using=None): + using = select_db("db") if using is None else using + transaction.commit(using=using) + + +def rollback(using=None): + using = select_db("db") if using is None else using + transaction.rollback(using=using) + + +def commit_on_success(using=None): + method = transaction.commit_on_success + return _transaction_func("db", method, using) + + +def commit_manually(using=None): + method = transaction.commit_manually + return _transaction_func("db", method, using) diff --git a/snf-cyclades-app/synnefo/db/utils.py b/snf-cyclades-app/synnefo/db/utils.py index ac7e3086841fb7f3226ef43ab412709ac3298040..1a924f6b3efab8b2fd903b8b576b49edddef210c 100644 --- a/snf-cyclades-app/synnefo/db/utils.py +++ b/snf-cyclades-app/synnefo/db/utils.py @@ -1,31 +1,17 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import re _VALID_MAC_RE = re.compile("^([0-9a-f]{2}:){5}[0-9a-f]{2}$", re.I) diff --git a/snf-cyclades-app/synnefo/helpdesk/templates/helpdesk/vms_list.html b/snf-cyclades-app/synnefo/helpdesk/templates/helpdesk/vms_list.html index 3a00bb67be1757a643a60954c716fbd5ed18957d..4f5b2d014b39eca415c8a287cabc76300a78d06c 100644 --- a/snf-cyclades-app/synnefo/helpdesk/templates/helpdesk/vms_list.html +++ b/snf-cyclades-app/synnefo/helpdesk/templates/helpdesk/vms_list.html @@ -35,12 +35,12 @@ <dt>Flavor</dt><dd>{{ vm.flavor.cpu }}, {{ vm.flavor.disk }}, {{ vm.flavor.ram }}, - {{ vm.flavor.disk_template }}</dd> + {{ vm.flavor.volume_type.disk_template }}</dd> </dl> </div> <div class="tab-pane" id="metadata{{ vm.pk }}"> <dl class="dl-horizontal well"> - {% for meta in vm.metadata.all %} + {% for meta in vm.metadata.all %} <dt>{{ meta.meta_key }}</dt><dd>{{ meta.meta_value }}</dd> {% empty %} <dt>No metadata</dt> diff --git a/snf-cyclades-app/synnefo/helpdesk/templatetags/helpdesk_tags.py b/snf-cyclades-app/synnefo/helpdesk/templatetags/helpdesk_tags.py index c9ae6f1940bc4b033cba2e6f4dba1bf9909010b1..90119469f838797b368c5718fbba442ab4880062 100644 --- a/snf-cyclades-app/synnefo/helpdesk/templatetags/helpdesk_tags.py +++ b/snf-cyclades-app/synnefo/helpdesk/templatetags/helpdesk_tags.py @@ -1,31 +1,17 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django import template diff --git a/snf-cyclades-app/synnefo/helpdesk/tests.py b/snf-cyclades-app/synnefo/helpdesk/tests.py index b1dd8e7622437fa073776b025576b608ff512810..f8b693f37e3592c2ab6454597f834b905e241d4c 100644 --- a/snf-cyclades-app/synnefo/helpdesk/tests.py +++ b/snf-cyclades-app/synnefo/helpdesk/tests.py @@ -1,39 +1,20 @@ -# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. -# - -import mock +from mock import patch from django.test import TestCase, Client from django.conf import settings from django.core.urlresolvers import reverse @@ -122,8 +103,8 @@ def get_user_mock(request, *args, **kwargs): } -@mock.patch("astakosclient.AstakosClient", new=AstakosClientMock) -@mock.patch("snf_django.lib.astakos.get_user", new=get_user_mock) +@patch("astakosclient.AstakosClient", new=AstakosClientMock) +@patch("snf_django.lib.astakos.get_user", new=get_user_mock) class HelpdeskTests(TestCase): """ Helpdesk tests. Test correctness of permissions and returned data. @@ -336,8 +317,6 @@ class HelpdeskTests(TestCase): self.assertEqual(vms.count(), 0) def test_start_shutdown(self): - from synnefo.logic import backend - self.vm1 = mfactory.VirtualMachineFactory(userid=USER1) pk = self.vm1.pk @@ -348,24 +327,25 @@ class HelpdeskTests(TestCase): data={'token': '0001'}) self.assertEqual(r.status_code, 403) - backend.shutdown_instance = shutdown = mock.Mock() - shutdown.return_value = 1 self.vm1.operstate = 'STARTED' self.vm1.save() - with mocked_quotaholder(): - r = self.client.post(reverse('helpdesk-vm-shutdown', args=(pk,)), - data={'token': '0001'}, user_token='0001') - self.assertEqual(r.status_code, 302) - self.assertTrue(shutdown.called) - self.assertEqual(len(shutdown.mock_calls), 1) + with patch("synnefo.logic.backend.shutdown_instance") as shutdown: + shutdown.return_value = 1 + with mocked_quotaholder(): + r = self.client.post( + reverse('helpdesk-vm-shutdown', args=(pk,)), + data={'token': '0001'}, user_token='0001') + self.assertEqual(r.status_code, 302) + self.assertTrue(shutdown.called) + self.assertEqual(len(shutdown.mock_calls), 1) - backend.startup_instance = startup = mock.Mock() - startup.return_value = 2 self.vm1.operstate = 'STOPPED' self.vm1.save() - with mocked_quotaholder(): - r = self.client.post(reverse('helpdesk-vm-start', args=(pk,)), - data={'token': '0001'}, user_token='0001') - self.assertEqual(r.status_code, 302) - self.assertTrue(startup.called) - self.assertEqual(len(startup.mock_calls), 1) + with patch("synnefo.logic.backend.startup_instance") as startup: + startup.return_value = 2 + with mocked_quotaholder(): + r = self.client.post(reverse('helpdesk-vm-start', args=(pk,)), + data={'token': '0001'}, user_token='0001') + self.assertEqual(r.status_code, 302) + self.assertTrue(startup.called) + self.assertEqual(len(startup.mock_calls), 1) diff --git a/snf-cyclades-app/synnefo/helpdesk/views.py b/snf-cyclades-app/synnefo/helpdesk/views.py index f8a88eea7fcc41b7bd8216a4508f5b4b3e6bf63a..cc66ecdc7ac007732faa9d82bbdc2a61a59d57d4 100644 --- a/snf-cyclades-app/synnefo/helpdesk/views.py +++ b/snf-cyclades-app/synnefo/helpdesk/views.py @@ -1,35 +1,17 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import re import logging diff --git a/snf-cyclades-app/synnefo/logic/allocators/default_allocator.py b/snf-cyclades-app/synnefo/logic/allocators/default_allocator.py index 07612009a2f657180f3f607136b16da622509ff8..4429c52384688f502ae7c979ea9ba3bd9a6a1ae7 100644 --- a/snf-cyclades-app/synnefo/logic/allocators/default_allocator.py +++ b/snf-cyclades-app/synnefo/logic/allocators/default_allocator.py @@ -1,35 +1,17 @@ -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from __future__ import division import logging diff --git a/snf-cyclades-app/synnefo/logic/backend.py b/snf-cyclades-app/synnefo/logic/backend.py index 685c52b3c40fc7ac67190e9904adb7aae101b576..b7243673d94fda6882ddbe880719e061d9f8dd13 100644 --- a/snf-cyclades-app/synnefo/logic/backend.py +++ b/snf-cyclades-app/synnefo/logic/backend.py @@ -1,40 +1,23 @@ -# Copyright 2011-2014 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.conf import settings -from django.db import transaction +from synnefo.db import transaction +from django.utils import simplejson as json from datetime import datetime, timedelta -from synnefo.db.models import (VirtualMachine, Network, +from synnefo.db.models import (VirtualMachine, Network, Volume, BackendNetwork, BACKEND_STATUSES, pooled_rapi_client, VirtualMachineDiagnostic, Flavor, IPAddress, IPAddressLog) @@ -43,6 +26,9 @@ from synnefo import quotas from synnefo.api.util import release_resource from synnefo.util.mac2eui64 import mac2eui64 from synnefo.logic import rapi +from synnefo import volume +from synnefo.plankton.backend import (OBJECT_AVAILABLE, OBJECT_UNAVAILABLE, + OBJECT_ERROR) from logging import getLogger log = getLogger(__name__) @@ -58,7 +44,9 @@ _reverse_tags = dict((v.split(':')[3], k) for k, v in _firewall_tags.items()) SIMPLE_NIC_FIELDS = ["state", "mac", "network", "firewall_profile", "index"] COMPLEX_NIC_FIELDS = ["ipv4_address", "ipv6_address"] NIC_FIELDS = SIMPLE_NIC_FIELDS + COMPLEX_NIC_FIELDS -UNKNOWN_NIC_PREFIX = "unknown-" +DISK_FIELDS = ["status", "size", "index"] +UNKNOWN_NIC_PREFIX = "unknown-nic-" +UNKNOWN_DISK_PREFIX = "unknown-disk-" def handle_vm_quotas(vm, job_id, job_opcode, job_status, job_fields): @@ -74,8 +62,6 @@ def handle_vm_quotas(vm, job_id, job_opcode, job_status, job_fields): """ if job_status not in rapi.JOB_STATUS_FINALIZED: return vm - print vm - print vm # Check successful completion of a job will trigger any quotable change in # the VM state. @@ -109,19 +95,23 @@ def handle_vm_quotas(vm, job_id, job_opcode, job_status, job_fields): vm.task_job_id, job_id, vm.serial) reason = ("client: dispatcher, resource: %s, ganeti_job: %s" % (vm, job_id)) - serial = quotas.handle_resource_commission( - vm, action, - action_fields=job_fields, - commission_name=reason, - force=True, - auto_accept=True) + try: + serial = quotas.handle_resource_commission( + vm, action, + action_fields=job_fields, + commission_name=reason, + force=True, + auto_accept=True) + except: + log.exception("Error while handling new commission") + raise log.debug("Issued new commission: %s", serial) return vm @transaction.commit_on_success def process_op_status(vm, etime, jobid, opcode, status, logmsg, nics=None, - job_fields=None): + disks=None, job_fields=None): """Process a job progress notification from the backend Process an incoming message from the backend (currently Ganeti). @@ -130,10 +120,23 @@ def process_op_status(vm, etime, jobid, opcode, status, logmsg, nics=None, """ # See #1492, #1031, #1111 why this line has been removed - #if (opcode not in [x[0] for x in VirtualMachine.BACKEND_OPCODES] or + # if (opcode not in [x[0] for x in VirtualMachine.BACKEND_OPCODES] or if status not in [x[0] for x in BACKEND_STATUSES]: raise VirtualMachine.InvalidBackendMsgError(opcode, status) + if opcode == "OP_INSTANCE_SNAPSHOT": + for disk_id, disk_info in job_fields.get("disks", []): + snapshot_info = disk_info.get("snapshot_info", None) + if snapshot_info is not None: + snapshot_info = json.loads(snapshot_info) + snapshot_id = snapshot_info["snapshot_id"] + update_snapshot(snapshot_id, user_id=vm.userid, job_id=jobid, + job_status=status, etime=etime) + else: + log.warning("Snapshot job '%s' for instance '%s' contains" + " no info for the created snapshot.", jobid, vm) + return + vm.backendjobid = jobid vm.backendjobstatus = status vm.backendopcode = opcode @@ -151,44 +154,52 @@ def process_op_status(vm, etime, jobid, opcode, status, logmsg, nics=None, state_for_success = VirtualMachine.OPER_STATE_FROM_OPCODE.get(opcode) if status == rapi.JOB_STATUS_SUCCESS: - # If job succeeds, change operating state if needed if state_for_success is not None: new_operstate = state_for_success - beparams = job_fields.get("beparams", None) + beparams = job_fields.get("beparams") if beparams: - # Change the flavor of the VM - new_flavor = _process_resize(vm, beparams) + cpu = beparams.get("vcpus") + ram = beparams.get("maxmem") + new_flavor = find_new_flavor(vm, cpu=cpu, ram=ram) - # Update backendtime only for jobs that have been successfully + # XXX: Update backendtime only for jobs that have been successfully # completed, since only these jobs update the state of the VM. Else a # "race condition" may occur when a successful job (e.g. # OP_INSTANCE_REMOVE) completes before an error job and messages arrive # in reversed order. vm.backendtime = etime - if status in rapi.JOB_STATUS_FINALIZED and nics is not None: - # Update the NICs of the VM - _process_net_status(vm, etime, nics) - + if status in rapi.JOB_STATUS_FINALIZED: + if nics is not None: + update_vm_nics(vm, nics, etime) + if disks is not None: + # XXX: Replace the job fields with mocked changes as produced by + # the diff between the DB and Ganeti disks. This is required in + # order to update quotas for disks that changed, but not from this + # job! + disk_changes = update_vm_disks(vm, disks, etime) + job_fields["disks"] = disk_changes + + vm_deleted = False # Special case: if OP_INSTANCE_CREATE fails --> ERROR if opcode == 'OP_INSTANCE_CREATE' and status in (rapi.JOB_STATUS_CANCELED, rapi.JOB_STATUS_ERROR): new_operstate = "ERROR" vm.backendtime = etime - # Update state of associated NICs + # Update state of associated attachments vm.nics.all().update(state="ERROR") + vm.volumes.all().update(status="ERROR") elif opcode == 'OP_INSTANCE_REMOVE': # Special case: OP_INSTANCE_REMOVE fails for machines in ERROR, # when no instance exists at the Ganeti backend. # See ticket #799 for all the details. if (status == rapi.JOB_STATUS_SUCCESS or (status == rapi.JOB_STATUS_ERROR and not vm_exists_in_backend(vm))): - # VM has been deleted + vm_deleted = True for nic in vm.nics.all(): - # Release the IP + # but first release the IP remove_nic_ips(nic) - # And delete the NIC. nic.delete() vm.deleted = True new_operstate = state_for_success @@ -204,6 +215,11 @@ def process_op_status(vm, etime, jobid, opcode, status, logmsg, nics=None, vm.task = None vm.task_job_id = None + # Update VM's state and flavor after handling of quotas, since computation + # of quotas depends on these attributes + if vm_deleted: + vm.volumes.filter(deleted=False).update(deleted=True, status="DELETED", + machine=None) if new_operstate is not None: vm.operstate = new_operstate if new_flavor is not None: @@ -212,41 +228,103 @@ def process_op_status(vm, etime, jobid, opcode, status, logmsg, nics=None, vm.save() -def _process_resize(vm, beparams): - """Change flavor of a VirtualMachine based on new beparams.""" +def find_new_flavor(vm, cpu=None, ram=None): + """Find VM's new flavor based on the new CPU and RAM""" + if cpu is None and ram is None: + return None + old_flavor = vm.flavor - vcpus = beparams.get("vcpus", old_flavor.cpu) - ram = beparams.get("maxmem", old_flavor.ram) - if vcpus == old_flavor.cpu and ram == old_flavor.ram: - return + ram = ram if ram is not None else old_flavor.ram + cpu = cpu if cpu is not None else old_flavor.cpu + if cpu == old_flavor.cpu and ram == old_flavor.ram: + return None + try: - new_flavor = Flavor.objects.get(cpu=vcpus, ram=ram, - disk=old_flavor.disk, - disk_template=old_flavor.disk_template) + new_flavor = Flavor.objects.get( + cpu=cpu, ram=ram, disk=old_flavor.disk, + volume_type_id=old_flavor.volume_type_id) except Flavor.DoesNotExist: - raise Exception("Cannot find flavor for VM") + raise Exception("There is no flavor to match the instance specs!" + " Instance: %s CPU: %s RAM %s: Disk: %s VolumeType: %s" + % (vm.backend_vm_id, cpu, ram, old_flavor.disk, + old_flavor.volume_type_id)) + log.info("Flavor of VM '%s' changed from '%s' to '%s'", vm, + old_flavor.name, new_flavor.name) return new_flavor -@transaction.commit_on_success -def process_net_status(vm, etime, nics): - """Wrap _process_net_status inside transaction.""" - _process_net_status(vm, etime, nics) +def nics_are_equal(db_nic, gnt_nic): + """Check if DB and Ganeti NICs are equal.""" + for field in NIC_FIELDS: + if getattr(db_nic, field) != gnt_nic[field]: + return False + return True + + +def parse_instance_nics(gnt_nics): + """Parse NICs of a Ganeti instance""" + nics = [] + for index, gnic in enumerate(gnt_nics): + nic_name = gnic.get("name", None) + if nic_name is not None: + nic_id = utils.id_from_nic_name(nic_name) + else: + # Unknown NIC + nic_id = UNKNOWN_NIC_PREFIX + str(index) + + network_name = gnic.get('network', '') + network_id = utils.id_from_network_name(network_name) + network = Network.objects.get(id=network_id) + subnet6 = network.subnet6 + + # Get the new nic info + mac = gnic.get('mac') + ipv4 = gnic.get('ip') + ipv6 = mac2eui64(mac, subnet6.cidr) if subnet6 else None + + firewall = gnic.get('firewall') + firewall_profile = _reverse_tags.get(firewall) + if not firewall_profile and network.public: + firewall_profile = settings.DEFAULT_FIREWALL_PROFILE + + nic_info = { + 'index': index, + 'network': network, + 'mac': mac, + 'ipv4_address': ipv4, + 'ipv6_address': ipv6, + 'firewall_profile': firewall_profile, + 'state': 'ACTIVE'} + + nics.append((nic_id, nic_info)) + return dict(nics) + +def update_vm_nics(vm, nics, etime=None): + """Update VM's NICs to match with the NICs of the Ganeti instance -def _process_net_status(vm, etime, nics): - """Process a net status notification from the backend + This function will update the VM's NICs(update, delete or create) and + return a list of quotable changes. - Process an incoming message from the Ganeti backend, - detailing the NIC configuration of a VM instance. + @param vm: The VirtualMachine the NICs belong to + @type vm: VirtualMachine object + @param nics: The NICs of the Ganeti instance + @type nics: List of dictionaries with NIC information + @param etime: The datetime the Ganeti instance had these NICs + @type etime: datetime - Update the state of the VM in the DB accordingly. + @return: List of quotable changes (add/remove NIC) (currently empty list) + @rtype: List of dictionaries """ - ganeti_nics = process_ganeti_nics(nics) - db_nics = dict([(nic.id, nic) - for nic in vm.nics.select_related("network") - .prefetch_related("ips")]) + try: + ganeti_nics = parse_instance_nics(nics) + except Network.InvalidBackendIdError as e: + log.warning("Server %s is connected to unknown network %s" + " Cannot reconcile server." % (vm.id, str(e))) + return [] + db_nics = dict([(nic.id, nic) for nic in vm.nics.select_related("network") + .prefetch_related("ips")]) for nic_name in set(db_nics.keys()) | set(ganeti_nics.keys()): db_nic = db_nics.get(nic_name) @@ -287,90 +365,7 @@ def _process_net_status(vm, etime, nics): new_address=gnt_ipv6_address, version=6) - vm.backendtime = etime - vm.save() - - -def change_address_of_port(port, userid, old_address, new_address, version): - """Change.""" - if old_address is not None: - msg = ("IPv%s Address of server '%s' changed from '%s' to '%s'" - % (version, port.machine_id, old_address, new_address)) - log.error(msg) - - # Remove the old IP address - remove_nic_ips(port, version=version) - - if version == 4: - ipaddress = ips.allocate_ip(port.network, userid, address=new_address) - ipaddress.nic = port - ipaddress.save() - elif version == 6: - subnet6 = port.network.subnet6 - ipaddress = IPAddress.objects.create(userid=userid, - network=port.network, - subnet=subnet6, - nic=port, - address=new_address, - ipversion=6) - else: - raise ValueError("Unknown version: %s" % version) - - # New address log - ip_log = IPAddressLog.objects.create(server_id=port.machine_id, - network_id=port.network_id, - address=new_address, - active=True) - log.info("Created IP log entry '%s' for address '%s' to server '%s'", - ip_log.id, new_address, port.machine_id) - - return ipaddress - - -def nics_are_equal(db_nic, gnt_nic): - for field in NIC_FIELDS: - if getattr(db_nic, field) != gnt_nic[field]: - return False - return True - - -def process_ganeti_nics(ganeti_nics): - """Process NIC dict from ganeti""" - new_nics = [] - for index, gnic in enumerate(ganeti_nics): - nic_name = gnic.get("name", None) - if nic_name is not None: - nic_id = utils.id_from_nic_name(nic_name) - else: - # Put as default value the index. If it is an unknown NIC to - # synnefo it will be created automaticaly. - nic_id = UNKNOWN_NIC_PREFIX + str(index) - network_name = gnic.get('network', '') - network_id = utils.id_from_network_name(network_name) - network = Network.objects.get(id=network_id) - - # Get the new nic info - mac = gnic.get('mac') - ipv4 = gnic.get('ip') - subnet6 = network.subnet6 - ipv6 = mac2eui64(mac, subnet6.cidr) if subnet6 else None - - firewall = gnic.get('firewall') - firewall_profile = _reverse_tags.get(firewall) - if not firewall_profile and network.public: - firewall_profile = settings.DEFAULT_FIREWALL_PROFILE - - nic_info = { - 'index': index, - 'network': network, - 'mac': mac, - 'ipv4_address': ipv4, - 'ipv6_address': ipv6, - 'firewall_profile': firewall_profile, - 'state': 'ACTIVE'} - - new_nics.append((nic_id, nic_info)) - return dict(new_nics) + return [] def remove_nic_ips(nic, version=None): @@ -426,6 +421,198 @@ def terminate_active_ipaddress_log(nic, ip): ip_log.save() +def change_address_of_port(port, userid, old_address, new_address, version): + """Change.""" + if old_address is not None: + msg = ("IPv%s Address of server '%s' changed from '%s' to '%s'" + % (version, port.machine_id, old_address, new_address)) + log.error(msg) + + # Remove the old IP address + remove_nic_ips(port, version=version) + + if version == 4: + ipaddress = ips.allocate_ip(port.network, userid, address=new_address) + ipaddress.nic = port + ipaddress.save() + elif version == 6: + subnet6 = port.network.subnet6 + ipaddress = IPAddress.objects.create(userid=userid, + network=port.network, + subnet=subnet6, + nic=port, + address=new_address, + ipversion=6) + else: + raise ValueError("Unknown version: %s" % version) + + # New address log + ip_log = IPAddressLog.objects.create(server_id=port.machine_id, + network_id=port.network_id, + address=new_address, + active=True) + log.info("Created IP log entry '%s' for address '%s' to server '%s'", + ip_log.id, new_address, port.machine_id) + + return ipaddress + + +def update_vm_disks(vm, disks, etime=None): + """Update VM's disks to match with the disks of the Ganeti instance + + This function will update the VM's disks(update, delete or create) and + return a list of quotable changes. + + @param vm: The VirtualMachine the disks belong to + @type vm: VirtualMachine object + @param disks: The disks of the Ganeti instance + @type disks: List of dictionaries with disk information + @param etime: The datetime the Ganeti instance had these disks + @type etime: datetime + + @return: List of quotable changes (add/remove disk) + @rtype: List of dictionaries + + """ + gnt_disks = parse_instance_disks(disks) + db_disks = dict([(disk.id, disk) + for disk in vm.volumes.filter(deleted=False)]) + + db_keys = set(db_disks.keys()) + gnt_keys = set(gnt_disks.keys()) + skip_db_stale = False + + changes = [] + + # Disks that exist in Ganeti but not in DB + for disk_name in (gnt_keys - db_keys): + gnt_disk = gnt_disks[disk_name] + if ((str(disk_name).startswith(UNKNOWN_DISK_PREFIX)) and + (len(db_keys - gnt_keys) > 0)): + log.warning("Ganeti disk '%s' of VM '%s' does not exist in DB," + " while there are stale DB volumes. Cannot" + " automatically fix this issue.", disk_name, vm) + skip_db_stale = True + else: + log.warning("Automatically adopting unknown disk '%s' of" + " instance '%s'", disk_name, vm) + adopt_instance_disk(vm, gnt_disk) + + # Disks that exist in DB but not in Ganeti + for disk_name in (db_keys - gnt_keys): + db_disk = db_disks[disk_name] + if db_disk.status != "DELETING" and skip_db_stale: + continue + if disk_is_stale(vm, disk): + log.debug("Removing stale disk '%s'", db_disk) + db_disk.status = "DELETED" + db_disk.deleted = True + db_disk.save() + changes.append(("remove", db_disk, {})) + else: + log.info("disk '%s' is still being created" % db_disk) + + # Disks that exist both in DB and in Ganeti + for disk_name in (db_keys & gnt_keys): + db_disk = db_disks[disk_name] + gnt_disk = gnt_disks[disk_name] + if not disks_are_equal(db_disk, gnt_disk): # Modified Disk + if gnt_disk["size"] != db_disk.size: + # Size of the disk has changed! TODO: Fix flavor! + size_delta = gnt_disk["size"] - db_disk.size + changes.append(("modify", db_disk, {"size_delta": size_delta})) + if db_disk.status == "CREATING": + # Disk has been created + changes.append(("add", db_disk, {})) + # Update the disk in DB with the values from Ganeti disk + [setattr(db_disk, f, gnt_disk[f]) for f in DISK_FIELDS] + db_disk.save() + + return changes + + +def disks_are_equal(db_disk, gnt_disk): + """Check if DB and Ganeti disks are equal""" + for field in DISK_FIELDS: + if getattr(db_disk, field) != gnt_disk[field]: + return False + return True + + +def parse_instance_disks(gnt_disks): + """Parse disks of a Ganeti instance""" + disks = [] + for index, gnt_disk in enumerate(gnt_disks): + disk_name = gnt_disk.get("name", None) + if disk_name is not None: + disk_id = utils.id_from_disk_name(disk_name) + else: # Unknown disk + disk_id = UNKNOWN_DISK_PREFIX + str(index) + + disk_info = { + 'index': index, + 'size': gnt_disk["size"] >> 10, # Size in GB + 'uuid': gnt_disk['uuid'], + 'status': "IN_USE"} + + disks.append((disk_id, disk_info)) + return dict(disks) + + +def adopt_instance_disk(server, gnt_disk): + """Create a new Cyclades Volume by adopting an existing Ganeti Disk.""" + disk_uuid = gnt_disk["uuid"] + disk_size = gnt_disk["size"] + disk_index = gnt_disk.get("index", 0) + vol = Volume.objects.create(userid=server.userid, + project=server.project, + size=disk_size, + volume_type=server.flavor.volume_type, + name="", + machine=server, + description=None, + delete_on_termination=True, + source="blank", + source_version=None, + origin=None, + index=disk_index, + status="CREATING") + + with pooled_rapi_client(server) as c: + jobid = c.ModifyInstance(instance=server.backend_vm_id, + disks=[("modify", disk_uuid, + {"name": vol.backend_volume_uuid})]) + log.info("Adopting disk '%s' of instance '%s' to volume '%s'. jobid: %s", + disk_uuid, server, vol, jobid) + return vol + + +def snapshot_state_from_job_status(job_status): + if job_status in rapi.JOB_STATUS_FINALIZED: + if (job_status == rapi.JOB_STATUS_SUCCESS): + return OBJECT_AVAILABLE + else: + return OBJECT_ERROR + else: + return OBJECT_UNAVAILABLE + + +def update_snapshot(snapshot_id, user_id, job_id, job_status, etime): + """Update a snapshot based on the result of the Ganeti job. + + Update the status of the snapshot in the Pithos DB. This is required to + be performed by Cyclades, since Pithos has no way to know whether the + Ganeti job that will create the snapshot has been completed or not. + + """ + + state = snapshot_state_from_job_status(job_status) + if state != OBJECT_UNAVAILABLE: + log.debug("Updating state of snapshot '%s' to '%s'", snapshot_id, + state) + volume.util.update_snapshot_state(snapshot_id, user_id, state=state) + + @transaction.commit_on_success def process_network_status(back_network, etime, jobid, opcode, status, logmsg): if status not in [x[0] for x in BACKEND_STATUSES]: @@ -584,8 +771,8 @@ def process_create_progress(vm, etime, progress): # successful creation gets processed before the 'ganeti-create-progress' # message? [vkoukis] # - #if not vm.operstate == 'BUILD': - # raise VirtualMachine.IllegalState("VM is not in building state") + # if not vm.operstate == 'BUILD': + # raise VirtualMachine.IllegalState("VM is not in building state") vm.buildpercentage = percentage vm.backendtime = etime @@ -611,7 +798,7 @@ def create_instance_diagnostic(vm, message, source, level="DEBUG", etime=None, details=details) -def create_instance(vm, nics, flavor, image): +def create_instance(vm, nics, volumes, flavor, image): """`image` is a dictionary which should contain the keys: 'backend_id', 'format' and 'metadata' @@ -629,15 +816,27 @@ def create_instance(vm, nics, flavor, image): kw['name'] = vm.backend_vm_id # Defined in settings.GANETI_CREATEINSTANCE_KWARGS - kw['disk_template'] = flavor.disk_template - kw['disks'] = [{"size": flavor.disk * 1024}] - provider = flavor.disk_provider - if provider: - kw['disks'][0]['provider'] = provider - kw['disks'][0]['origin'] = flavor.disk_origin - extra_disk_params = settings.GANETI_DISK_PROVIDER_KWARGS.get(provider) - if extra_disk_params is not None: - kw["disks"][0].update(extra_disk_params) + kw['disk_template'] = volumes[0].volume_type.template + disks = [] + for vol in volumes: + disk = {"name": vol.backend_volume_uuid, + "size": vol.size * 1024} + provider = vol.volume_type.provider + if provider is not None: + disk["provider"] = provider + if provider in settings.GANETI_CLONE_PROVIDERS: + disk["origin"] = vol.origin + disk["origin_size"] = vol.origin_size + extra_disk_params = settings.GANETI_DISK_PROVIDER_KWARGS\ + .get(provider) + if extra_disk_params is not None: + disk.update(extra_disk_params) + disks.append(disk) + + kw["disks"] = disks + + # --no-wait-for-sync option for DRBD disks + kw["wait_for_sync"] = settings.GANETI_DISKS_WAIT_FOR_SYNC kw['nics'] = [{"name": nic.backend_uuid, "network": nic.network.backend_id, @@ -659,7 +858,7 @@ def create_instance(vm, nics, flavor, image): # Do not specific a node explicitly, have # Ganeti use an iallocator instead - #kw['pnode'] = rapi.GetNodes()[0] + # kw['pnode'] = rapi.GetNodes()[0] kw['dry_run'] = settings.TEST @@ -671,7 +870,7 @@ def create_instance(vm, nics, flavor, image): kw['osparams'] = { 'config_url': vm.config_url, # Store image id and format to Ganeti - 'img_id': image['backend_id'], + 'img_id': image['pithosmap'], 'img_format': image['format']} # Use opportunistic locking @@ -732,34 +931,6 @@ def resize_instance(vm, vcpus, memory): return client.ModifyInstance(vm.backend_vm_id, beparams=beparams) -def get_instance_console(vm): - # RAPI GetInstanceConsole() returns endpoints to the vnc_bind_address, - # which is a cluster-wide setting, either 0.0.0.0 or 127.0.0.1, and pretty - # useless (see #783). - # - # Until this is fixed on the Ganeti side, construct a console info reply - # directly. - # - # WARNING: This assumes that VNC runs on port network_port on - # the instance's primary node, and is probably - # hypervisor-specific. - # - log.debug("Getting console for vm %s", vm) - - console = {} - console['kind'] = 'vnc' - - with pooled_rapi_client(vm) as client: - i = client.GetInstance(vm.backend_vm_id) - - if vm.backend.hypervisor == "kvm" and i['hvparams']['serial_console']: - raise Exception("hv parameter serial_console cannot be true") - console['host'] = i['pnode'] - console['port'] = i['network_port'] - - return console - - def get_instance_info(vm): with pooled_rapi_client(vm) as client: return client.GetInstance(vm.backend_vm_id) @@ -803,10 +974,28 @@ def job_is_still_running(vm, job_id=None): raise e +def disk_is_stale(vm, disk, timeout=60): + """Check if a disk is stale or exists in the Ganeti backend.""" + # First check the state of the disk + if disk.status == "CREATING": + if datetime.now() < disk.created + timedelta(seconds=timeout): + # Do not check for too recent disks to avoid the time overhead + return False + if job_is_still_running(vm, job_id=disk.backendjobid): + return False + else: + # If job has finished, check that the disk exists, because the + # message may have been lost or stuck in the queue. + vm_info = get_instance_info(vm) + if disk.backend_volume_uuid in vm_info["disk.names"]: + return False + return True + + def nic_is_stale(vm, nic, timeout=60): """Check if a NIC is stale or exists in the Ganeti backend.""" # First check the state of the NIC and if there is a pending CONNECT - if nic.state in ["BUILD", "DOWN"]: + if nic.state in ["BUILD", "DOWN", "ERROR"]: if datetime.now() < nic.created + timedelta(seconds=timeout): # Do not check for too recent NICs to avoid the time overhead return False @@ -865,7 +1054,7 @@ def _create_network(network, backend): gateway = None gateway6 = None for _subnet in network.subnets.all(): - if _subnet.dhcp and not "nfdhcpd" in tags: + if _subnet.dhcp and "nfdhcpd" not in tags: tags.append("nfdhcpd") if _subnet.ipversion == 4: subnet = _subnet.cidr @@ -922,7 +1111,7 @@ def connect_network(network, backend, depends=[], group=None): for group in groups: job_id = client.ConnectNetwork(network.backend_id, group, network.mode, network.link, - conflicts_check, + conflicts_check=conflicts_check, depends=depends) job_ids.append(job_id) return job_ids @@ -1035,6 +1224,65 @@ def set_firewall_profile(vm, profile, nic): return None +def attach_volume(vm, volume, depends=[]): + log.debug("Attaching volume %s to vm %s", volume, vm) + + disk = {"size": int(volume.size) << 10, + "name": volume.backend_volume_uuid} + + disk_provider = volume.volume_type.provider + if disk_provider is not None: + disk["provider"] = disk_provider + + if volume.origin is not None: + disk["origin"] = volume.origin + disk["origin_size"] = volume.origin_size + + extra_disk_params = settings.GANETI_DISK_PROVIDER_KWARGS\ + .get(disk_provider) + if extra_disk_params is not None: + disk.update(extra_disk_params) + + kwargs = { + "instance": vm.backend_vm_id, + "disks": [("add", "-1", disk)], + "wait_for_sync": settings.GANETI_DISKS_WAIT_FOR_SYNC, + "depends": depends, + } + if vm.backend.use_hotplug(): + kwargs["hotplug_if_possible"] = True + if settings.TEST: + kwargs["dry_run"] = True + + with pooled_rapi_client(vm) as client: + return client.ModifyInstance(**kwargs) + + +def detach_volume(vm, volume, depends=[]): + log.debug("Removing volume %s from vm %s", volume, vm) + kwargs = { + "instance": vm.backend_vm_id, + "disks": [("remove", volume.backend_volume_uuid, {})], + "depends": depends, + } + if vm.backend.use_hotplug(): + kwargs["hotplug_if_possible"] = True + if settings.TEST: + kwargs["dry_run"] = True + + with pooled_rapi_client(vm) as client: + return client.ModifyInstance(**kwargs) + + +def snapshot_instance(vm, volume, snapshot_name, snapshot_id): + reason = json.dumps({"snapshot_id": snapshot_id}) + disks = [(volume.backend_volume_uuid, {"snapshot_name": snapshot_name})] + with pooled_rapi_client(vm) as client: + return client.SnapshotInstance(instance=vm.backend_vm_id, + disks=disks, + reason=reason) + + def get_instances(backend, bulk=True): with pooled_rapi_client(backend) as c: return c.GetInstances(bulk=bulk) @@ -1129,9 +1377,9 @@ def update_backend_disk_templates(backend): backend.save() -## -## Synchronized operations for reconciliation -## +# +# Synchronized operations for reconciliation +# def create_network_synced(network, backend): diff --git a/snf-cyclades-app/synnefo/logic/backend_allocator.py b/snf-cyclades-app/synnefo/logic/backend_allocator.py index 8264c158b800fb58965641578078322878afc383..4cdac4f09b8db27ed93f242d0b8acd8ba38f95ed 100644 --- a/snf-cyclades-app/synnefo/logic/backend_allocator.py +++ b/snf-cyclades-app/synnefo/logic/backend_allocator.py @@ -1,31 +1,17 @@ -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import logging import datetime @@ -98,7 +84,7 @@ def get_available_backends(flavor): excluded. """ - disk_template = flavor.disk_template + disk_template = flavor.volume_type.disk_template # Ganeti knows only the 'ext' disk template, but the flavors disk template # includes the provider. if disk_template.startswith("ext_"): @@ -122,7 +108,7 @@ def flavor_disk(flavor): """ Get flavor's 'real' disk size """ - if flavor.disk_template == 'drbd': + if flavor.volume_type.disk_template == 'drbd': return flavor.disk * 1024 * 2 else: return flavor.disk * 1024 diff --git a/snf-cyclades-app/synnefo/logic/callbacks.py b/snf-cyclades-app/synnefo/logic/callbacks.py index 89e51a5fdf7b009b5de9c94607ebc641b2cbcf6d..079613fd5d09c08449d292289fd1bc6c97473767 100644 --- a/snf-cyclades-app/synnefo/logic/callbacks.py +++ b/snf-cyclades-app/synnefo/logic/callbacks.py @@ -1,31 +1,17 @@ -# Copyright 2011-2014 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # Callback functions used by the dispatcher to process incoming notifications # from AMQP queues. @@ -34,7 +20,7 @@ import logging import json from functools import wraps -from django.db import transaction +from synnefo.db import transaction from synnefo.db.models import (Backend, VirtualMachine, Network, BackendNetwork, pooled_rapi_client) from synnefo.logic import utils, backend as backend_mod, rapi @@ -183,6 +169,7 @@ def update_db(vm, msg, event_time): jobID = msg["jobId"] logmsg = msg["logmsg"] nics = msg.get("instance_nics", None) + disks = msg.get("instance_disks", None) job_fields = msg.get("job_fields", {}) result = msg.get("result", []) @@ -224,6 +211,7 @@ def update_db(vm, msg, event_time): backend_mod.process_op_status(vm, event_time, jobID, operation, status, logmsg, nics=nics, + disks=disks, job_fields=job_fields) log.debug("Done processing ganeti-op-status msg for vm %s.", diff --git a/snf-cyclades-app/synnefo/logic/commands.py b/snf-cyclades-app/synnefo/logic/commands.py new file mode 100644 index 0000000000000000000000000000000000000000..364bed19235f8b8bf1d221581373e7cdbe229089 --- /dev/null +++ b/snf-cyclades-app/synnefo/logic/commands.py @@ -0,0 +1,148 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import logging + +from functools import wraps +from synnefo.db import transaction + +from django.conf import settings +from snf_django.lib.api import faults +from synnefo import quotas +from synnefo.db.models import VirtualMachine + + +log = logging.getLogger(__name__) + + +def validate_server_action(vm, action): + if vm.deleted: + raise faults.BadRequest("Server '%s' has been deleted." % vm.id) + + # Destroying a server should always be permitted + if action == "DESTROY": + return + + # Check that there is no pending action + pending_action = vm.task + if pending_action: + if pending_action == "BUILD": + raise faults.BuildInProgress("Server '%s' is being built." % vm.id) + raise faults.BadRequest("Cannot perform '%s' action while there is a" + " pending '%s'." % (action, pending_action)) + + # Reassigning is permitted in any state + if action == "REASSIGN": + return + + # Check if action can be performed to VM's operstate + operstate = vm.operstate + if operstate == "ERROR": + raise faults.BadRequest("Cannot perform '%s' action while server is" + " in 'ERROR' state." % action) + elif operstate == "BUILD" and action != "BUILD": + raise faults.BuildInProgress("Server '%s' is being built." % vm.id) + elif (action == "START" and operstate != "STOPPED") or\ + (action == "STOP" and operstate != "STARTED") or\ + (action == "RESIZE" and operstate != "STOPPED") or\ + (action in ["CONNECT", "DISCONNECT"] + and operstate != "STOPPED" + and not settings.GANETI_USE_HOTPLUG) or \ + (action in ["ATTACH_VOLUME", "DETACH_VOLUME"] + and operstate != "STOPPED" + and not settings.GANETI_USE_HOTPLUG): + raise faults.BadRequest("Cannot perform '%s' action while server is" + " in '%s' state." % (action, operstate)) + return + + +def server_command(action, action_fields=None): + """Handle execution of a server action. + + Helper function to validate and execute a server action, handle quota + commission and update the 'task' of the VM in the DB. + + 1) Check if action can be performed. If it can, then there must be no + pending task (with the exception of DESTROY). + 2) Handle previous commission if unresolved: + * If it is not pending and it to accept, then accept + * If it is not pending and to reject or is pending then reject it. Since + the action can be performed only if there is no pending task, then there + can be no pending commission. The exception is DESTROY, but in this case + the commission can safely be rejected, and the dispatcher will generate + the correct ones! + 3) Issue new commission and associate it with the VM. Also clear the task. + 4) Send job to ganeti + 5) Update task and commit + """ + def decorator(func): + @wraps(func) + @transaction.commit_on_success + def wrapper(vm, *args, **kwargs): + user_id = vm.userid + validate_server_action(vm, action) + vm.action = action + + commission_name = "client: api, resource: %s" % vm + quotas.handle_resource_commission(vm, action=action, + action_fields=action_fields, + commission_name=commission_name) + vm.save() + + # XXX: Special case for server creation! + if action == "BUILD": + serial = vm.serial + serial.pending = False + serial.accept = True + serial.save() + # Perform a commit, because the VirtualMachine must be saved to + # DB before the OP_INSTANCE_CREATE job in enqueued in Ganeti. + # Otherwise, messages will arrive from snf-dispatcher about + # this instance, before the VM is stored in DB. + transaction.commit() + # After committing the locks are released. Refetch the instance + # to guarantee x-lock. + vm = VirtualMachine.objects.select_for_update().get(id=vm.id) + # XXX: Special case for server creation: we must accept the + # commission because the VM has been stored in DB. Also, if + # communication with Ganeti fails, the job will never reach + # Ganeti, and the commission will never be resolved. + quotas.accept_resource_serial(vm) + + # Send the job to Ganeti and get the associated jobID + try: + job_id = func(vm, *args, **kwargs) + except Exception as e: + if vm.serial is not None and action != "BUILD": + # Since the job never reached Ganeti, reject the commission + log.debug("Rejecting commission: '%s', could not perform" + " action '%s': %s" % (vm.serial, action, e)) + transaction.rollback() + quotas.reject_serial(vm.serial) + transaction.commit() + raise + + log.info("user: %s, vm: %s, action: %s, job_id: %s, serial: %s", + user_id, vm.id, action, job_id, vm.serial) + + # store the new task in the VM + if job_id is not None: + vm.task = action + vm.task_job_id = job_id + vm.save() + + return vm + return wrapper + return decorator diff --git a/snf-cyclades-app/synnefo/logic/dispatcher.py b/snf-cyclades-app/synnefo/logic/dispatcher.py index cac533909f0a8af3d95d7a97228e59b113cdab5c..3d772a5ccf576e2705437f65b09159ca51105c3a 100755 --- a/snf-cyclades-app/synnefo/logic/dispatcher.py +++ b/snf-cyclades-app/synnefo/logic/dispatcher.py @@ -1,32 +1,18 @@ #!/usr/bin/env python -# Copyright 2011-2014 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. """ Message queue setup, dispatch and admin @@ -50,6 +36,9 @@ from django.db import close_connection import time +import json +import socket +import traceback import daemon import daemon.runner from lockfile import LockTimeout @@ -63,8 +52,11 @@ import setproctitle from synnefo.lib.amqp import AMQPClient from synnefo.logic import callbacks from synnefo.logic import queues +from synnefo.db.models import Backend, pooled_rapi_client import logging +import select +import errno log = logging.getLogger("dispatcher") log_amqp = logging.getLogger("amqp") @@ -77,6 +69,24 @@ LOGGERS = [log, log_amqp, log_logic] DISPATCHER_RECONNECT_TIMEOUT = 600 +# Time out after S Seconds while waiting messages from Ganeti clusters to +# arrive. Warning: During this period snf-dispatcher will not consume any other +# messages. +HEARTBEAT_TIMEOUT = 5 +# Seconds that the heartbeat queue will exist while there are no consumers. +HEARTBEAT_QUEUE_TTL = 120 +# Time out after S seconds while waiting acknowledgment from snf-dispatcher +# that the status check has started. +CHECK_TOOL_ACK_TIMEOUT = 10 +# Time out after S seconds while waiting for the status report from +# snf-dispatcher to arrive. +CHECK_TOOL_REPORT_TIMEOUT = 30 + + +def get_hostname(): + return socket.gethostbyaddr(socket.gethostname())[0] + + class Dispatcher: debug = False @@ -101,7 +111,12 @@ class Dispatcher: " to a different host. Verify that" " snf-ganeti-eventd is running!!", timeout) self.client.reconnect(timeout=1) - except SystemExit: + except select.error as e: + if e[0] != errno.EINTR: + log.exception("Caught unexpected exception: %s", e) + else: + break + except (SystemExit, KeyboardInterrupt): break except Exception as e: log.exception("Caught unexpected exception: %s", e) @@ -113,7 +128,8 @@ class Dispatcher: def _init(self): log.info("Initializing") - self.client = AMQPClient(logger=log_amqp) + # Set confirm buffer to 1 for heartbeat messages + self.client = AMQPClient(logger=log_amqp, confirm_buffer=1) # Connect to AMQP host self.client.connect() @@ -124,7 +140,6 @@ class Dispatcher: type="topic") self.client.exchange_declare(exchange=exchange_dl, type="topic") - for queue in queues.QUEUES: # Queues are mirrored to all RabbitMQ brokers self.client.queue_declare(queue=queue, mirrored=True, @@ -161,33 +176,199 @@ class Dispatcher: log.debug("Binding %s(%s) to queue %s with handler %s", exchange, routing_key, queue, binding[3]) + # Declare the queue that will be used for receiving requests, e.g. a + # status check request + hostname, pid = get_hostname(), os.getpid() + queue = queues.get_dispatcher_request_queue(hostname, pid) + self.client.queue_declare(queue=queue, mirrored=True, ttl=60) + self.client.basic_consume(queue=queue, callback=handle_request) + log.debug("Binding %s(%s) to queue %s with handler 'hadle_request'", + exchange, routing_key, queue) + + +def handle_request(client, msg): + """Callback function for handling requests. + + Currently only 'status-check' action is supported. + + """ + + client.basic_ack(msg) + log.debug("Received request message: %s", msg) + body = json.loads(msg["body"]) + reply_to = None + try: + reply_to = body["reply_to"] + reply_to = reply_to.encode("utf-8") + action = body["action"] + assert(action == "status-check") + except (KeyError, AssertionError) as e: + log.warning("Invalid request message: %s. Error: %s", msg, e) + if reply_to is not None: + msg = {"status": "failed", + "reason": "Invalid request"} + client.basic_publish("", reply_to, json.dumps(msg)) + return + + msg = {"action": action, "status": "started"} + client.basic_publish("", reply_to, json.dumps(msg)) + + # Declare 'heartbeat' queue and bind it to the exchange. The queue is + # declared with a 'ttl' option in order to be automatically deleted. + hostname, pid = get_hostname(), os.getpid() + queue = queues.get_dispatcher_heartbeat_queue(hostname, pid) + exchange = settings.EXCHANGE_GANETI + routing_key = queues.EVENTD_HEARTBEAT_ROUTING_KEY + client.queue_declare(queue=queue, mirrored=False, ttl=HEARTBEAT_QUEUE_TTL) + client.queue_bind(queue=queue, exchange=exchange, + routing_key=routing_key) + log.debug("Binding %s(%s) to queue %s", exchange, routing_key, queue) + + backends = Backend.objects.filter(offline=False) + status = {} + + _OK = "ok" + _FAIL = "fail" + # Add cluster tag to trigger snf-ganeti-eventd + tag = "snf:eventd:heartbeat:%s:%s" % (hostname, pid) + for backend in backends: + cluster = backend.clustername + status[cluster] = {"RAPI": _FAIL, "eventd": _FAIL} + try: + with pooled_rapi_client(backend) as rapi: + rapi.AddClusterTags(tags=[tag], dry_run=True) + except: + log.exception("Failed to send job to Ganeti cluster '%s' during" + " status check" % cluster) + continue + status[cluster]["RAPI"] = _OK + + start = time.time() + while time.time() - start <= HEARTBEAT_TIMEOUT: + msg = client.basic_get(queue, no_ack=True) + if msg is None: + time.sleep(0.1) + continue + log.debug("Received heartbeat msg: %s", msg) + try: + body = json.loads(msg["body"]) + cluster = body["cluster"] + status[cluster]["eventd"] = _OK + except: + log.error("Received invalid heartbat msg: %s", msg) + if not filter(lambda x: x["eventd"] == _FAIL, status.values()): + break + + # Send back status report + client.basic_publish("", reply_to, json.dumps({"status": status})) + def parse_arguments(args): from optparse import OptionParser - default_pid_file = \ - os.path.join(".", "var", "run", "synnefo", "dispatcher.pid")[1:] - parser = OptionParser() - parser.add_option("-d", "--debug", action="store_true", default=False, - dest="debug", help="Enable debug mode") - parser.add_option("-w", "--workers", default=2, dest="workers", - help="Number of workers to spawn", type="int") + default_pid_file = "/var/run/synnefo/snf-dispatcher.pid" + description = ("The Synnefo Dispatcher Daemon consumes messages from an" + " AMQP broker and properly updates the Cyclades DB. These" + " messages are mostly asynchronous notifications about the" + " progress of jobs in the Ganeti backends.") + parser = OptionParser(description=description) + parser.add_option("-d", "--debug", action="store_true", + default=False, dest="debug", + help="Enable debug mode (do not turn into deamon)") parser.add_option("-p", "--pid-file", dest="pid_file", default=default_pid_file, - help="Save PID to file (default: %s)" % default_pid_file) + help=("Location of PID file (default: %s)" + % default_pid_file)) parser.add_option("--purge-queues", action="store_true", default=False, dest="purge_queues", - help="Remove all declared queues (DANGEROUS!)") + help="Remove all queues (DANGEROUS!)") parser.add_option("--purge-exchanges", action="store_true", default=False, dest="purge_exchanges", - help="Remove all exchanges. Implies deleting all queues \ - first (DANGEROUS!)") + help=("Remove all exchanges. Implies deleting all queues" + " first (DANGEROUS!)")) parser.add_option("--drain-queue", dest="drain_queue", - help="Strips a queue from all outstanding messages") + help="Drain a queue from all outstanding messages") + parser.add_option("--status-check", dest="status_check", + default=False, action="store_true", + help="Trigger a status check for a running" + " snf-dispatcher process, that will check" + " communication between snf-dispatcher and Ganeti" + " backends via AMQP brokers") return parser.parse_args(args) +def check_dispatcher_status(pid_file): + """Check the status of a running snf-dispatcher process. + + Check the status of a running snf-dispatcher process, the PID of which is + contained in the 'pid_file'. This function will send a 'status-check' + message to the running snf-dispatcher, wait for dispatcher's response and + pretty-print the results. + + """ + dispatcher_pid = pidlockfile.read_pid_from_pidfile(pid_file) + if dispatcher_pid is None: + sys.stdout.write("snf-dispatcher with PID file '%s' is not running." + " PID file does not exist\n" % pid_file) + sys.exit(1) + sys.stdout.write("snf-dispatcher (PID: %s): running\n" % dispatcher_pid) + + hostname = get_hostname() + local_queue = "snf:check_tool:%s:%s" % (hostname, os.getpid()) + dispatcher_queue = queues.get_dispatcher_request_queue(hostname, + dispatcher_pid) + + log_amqp.setLevel(logging.WARNING) + try: + client = AMQPClient(logger=log_amqp) + client.connect() + client.queue_declare(queue=local_queue, mirrored=False, exclusive=True) + client.basic_consume(queue=local_queue, callback=lambda x, y: 0, + no_ack=True) + msg = json.dumps({"action": "status-check", "reply_to": local_queue}) + client.basic_publish("", dispatcher_queue, msg) + except: + sys.stdout.write("Error while connecting with AMQP\nError:\n") + traceback.print_exc() + sys.exit(1) + + sys.stdout.write("AMQP -> snf-dispatcher: ") + msg = client.basic_wait(timeout=CHECK_TOOL_ACK_TIMEOUT) + if msg is None: + sys.stdout.write("fail\n") + sys.stdout.write("ERROR: No reply from snf-dipatcher after '%s'" + " seconds.\n" % CHECK_TOOL_ACK_TIMEOUT) + sys.exit(1) + else: + try: + body = json.loads(msg["body"]) + assert(body["action"] == "status-check"), "Invalid action" + assert(body["status"] == "started"), "Invalid status" + sys.stdout.write("ok\n") + except Exception as e: + sys.stdout.write("Received invalid msg from snf-dispatcher:" + " msg: %s error: %s\n" % (msg, e)) + sys.exit(1) + + msg = client.basic_wait(timeout=CHECK_TOOL_REPORT_TIMEOUT) + if msg is None: + sys.stdout.write("fail\n") + sys.stdout.write("ERROR: No status repot after '%s' seconds.\n" + % CHECK_TOOL_REPORT_TIMEOUT) + sys.exit(1) + + sys.stdout.write("Backends:\n") + status = json.loads(msg["body"])["status"] + for backend, bstatus in sorted(status.items()): + sys.stdout.write(" * %s: \n" % backend) + sys.stdout.write(" snf-dispatcher -> ganeti: %s\n" % + bstatus["RAPI"]) + sys.stdout.write(" snf-ganeti-eventd -> AMQP: %s\n" % + bstatus["eventd"]) + sys.exit(0) + + def purge_queues(): """ Delete declared queues from RabbitMQ. Use with care! @@ -283,7 +464,7 @@ def setup_logging(opts): import logging formatter = logging.Formatter("%(asctime)s %(name)s %(module)s" " [%(levelname)s] %(message)s") - if opts.debug: + if opts.debug or opts.status_check: log_handler = logging.StreamHandler() log_handler.setFormatter(formatter) else: @@ -307,6 +488,10 @@ def main(): setproctitle.setproctitle(sys.argv[0]) setup_logging(opts) + if opts.status_check: + check_dispatcher_status(opts.pid_file) + return + # Special case for the clean up queues action if opts.purge_queues: purge_queues() diff --git a/snf-cyclades-app/synnefo/logic/ips.py b/snf-cyclades-app/synnefo/logic/ips.py index b7971f1ba0a0389bf9ae92d4ebfc6018dd6e5d4c..a9881d94202c864c6acbed5d17589a7e6b787c95 100644 --- a/snf-cyclades-app/synnefo/logic/ips.py +++ b/snf-cyclades-app/synnefo/logic/ips.py @@ -1,46 +1,87 @@ -# Copyright 2013-2014 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import logging +import functools from snf_django.lib.api import faults -from django.db import transaction +from synnefo.db import transaction from synnefo import quotas from synnefo.db import pools from synnefo.db.models import (IPPoolTable, IPAddress, Network) log = logging.getLogger(__name__) +def validate_ip_action(ip, action, silent=True): + """Check if an action can apply on an IP address. + + Arguments: + ip: The target IP address. + action: The name of the action (in capital letters). + silent: If set to True, suppress exceptions. + + Returns: + A `(success, message)` tuple. `success` is a boolean value that + shows if the action can apply on an IP, and `message` explains + why the action cannot apply on an IP. + + If an action can apply on an IP, this function will always return + `(True, None)`. + + Exceptions: + faults.Conflict: When the action cannot apply on an ip due to a + conflict. + faults.BadRequest: When the action is unknown/malformed. + """ + def fail(e=Exception, msg=""): + if silent: + return False, msg + else: + raise e(msg) + + if action == "DELETE": + if ip.nic: + # This is safe, you also need for_update to attach floating IP to + # instance. + server = ip.nic.machine + if server is None: + msg = ("IP '%s' is used by port '%s'" % (ip.id, ip.nic_id)) + else: + msg = ("IP '%s' is used by server '%s'" % + (ip.id, ip.nic.machine_id)) + return fail(faults.Conflict, msg) + elif action == "REASSIGN": + pass + else: + return fail(faults.BadRequest, "Unknown action: {}.".format(action)) + + return True, None + + +def ip_command(action): + """Common wrapper for IP commands.""" + def decorator(func): + @functools.wraps(func) + @transaction.commit_on_success() + def wrapper(ip, *args, **kwargs): + validate_ip_action(ip, action, silent=False) + return func(ip, *args, **kwargs) + return wrapper + return decorator + + def allocate_ip_from_pools(pool_rows, userid, address=None, floating_ip=False): """Try to allocate a value from a number of pools. @@ -109,7 +150,6 @@ def allocate_public_ip(userid, floating_ip=False, backend=None, networks=None): be used. """ - ip_pool_rows = IPPoolTable.objects.select_for_update()\ .prefetch_related("subnet__network")\ .filter(subnet__deleted=False)\ @@ -143,7 +183,7 @@ def allocate_public_ip(userid, floating_ip=False, backend=None, networks=None): @transaction.commit_on_success -def create_floating_ip(userid, network=None, address=None): +def create_floating_ip(userid, network=None, address=None, project=None): if network is None: floating_ip = allocate_public_ip(userid, floating_ip=True) else: @@ -159,6 +199,10 @@ def create_floating_ip(userid, network=None, address=None): floating_ip = allocate_ip(network, userid, address=address, floating_ip=True) + if project is None: + project = userid + floating_ip.project = project + floating_ip.save() # Issue commission (quotas) quotas.issue_and_accept_commission(floating_ip) transaction.commit() @@ -197,20 +241,8 @@ def get_free_floating_ip(userid, network=None): raise faults.Conflict(msg) -@transaction.commit_on_success +@ip_command("DELETE") def delete_floating_ip(floating_ip): - if floating_ip.nic: - # This is safe, you also need for_update to attach floating IP to - # instance. - server = floating_ip.nic.machine - if server is None: - msg = ("Floating IP '%s' is used by port '%s'" % - (floating_ip.id, floating_ip.nic_id)) - else: - msg = ("Floating IP '%s' is used by server '%s'" % - (floating_ip.id, floating_ip.nic.machine_id)) - raise faults.Conflict(msg) - # Lock network to prevent deadlock Network.objects.select_for_update().get(id=floating_ip.network_id) @@ -226,3 +258,15 @@ def delete_floating_ip(floating_ip): log.info("Deleted floating IP '%s' of user '%s", floating_ip, floating_ip.userid) floating_ip.delete() + + +@ip_command("REASSIGN") +def reassign_floating_ip(floating_ip, project): + action_fields = {"to_project": project, + "from_project": floating_ip.project} + log.info("Reassigning floating IP %s from project %s to %s", + floating_ip, floating_ip.project, project) + floating_ip.project = project + floating_ip.save() + quotas.issue_and_accept_commission(floating_ip, action="REASSIGN", + action_fields=action_fields) diff --git a/snf-cyclades-app/synnefo/logic/management/commands/backend-add.py b/snf-cyclades-app/synnefo/logic/management/commands/backend-add.py index c1358a043b58df7c9dd6012fe0cab2f0987102db..5b2380ac9a637d68116e1d158952120373c63364 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/backend-add.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/backend-add.py @@ -1,39 +1,25 @@ -# Copyright 2011-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of GRNET S.A. -# -import sys from optparse import make_option -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import CommandError from synnefo.db.models import Backend, Network from django.db.utils import IntegrityError from synnefo.logic import backend as backend_mod +from snf_django.management.commands import SynnefoCommand from synnefo.management.common import check_backend_credentials from snf_django.management.utils import pprint_table @@ -41,11 +27,11 @@ from snf_django.management.utils import pprint_table HYPERVISORS = [h[0] for h in Backend.HYPERVISORS] -class Command(BaseCommand): +class Command(SynnefoCommand): can_import_settings = True help = 'Create a new backend.' - option_list = BaseCommand.option_list + ( + option_list = SynnefoCommand.option_list + ( make_option('--clustername', dest='clustername'), make_option('--port', dest='port', default=5080), make_option('--user', dest='username'), @@ -85,72 +71,74 @@ class Command(BaseCommand): if options['check']: check_backend_credentials(clustername, port, username, password) - create_backend(clustername, port, username, password, - hypervisor=options["hypervisor"], - initialize=options["init"]) - - -def create_backend(clustername, port, username, password, hypervisor=None, - initialize=True, stream=sys.stdout): - kw = {"clustername": clustername, - "port": port, - "username": username, - "password": password, - "drained": True} - - if hypervisor: - kw["hypervisor"] = hypervisor - - # Create the new backend in database - try: - backend = Backend.objects.create(**kw) - except IntegrityError as e: - raise CommandError("Cannot create backend: %s\n" % e) - - stream.write("Successfully created backend with id %d\n" % backend.id) - - if not initialize: - return - - stream.write("Retrieving backend resources:\n") - resources = backend_mod.get_physical_resources(backend) - attr = ['mfree', 'mtotal', 'dfree', 'dtotal', 'pinst_cnt', 'ctotal'] - - table = [[str(resources[x]) for x in attr]] - pprint_table(stream, table, attr) - - backend_mod.update_backend_resources(backend, resources) - backend_mod.update_backend_disk_templates(backend) - - networks = Network.objects.filter(deleted=False, public=True) - if not networks: - return - - stream.write("Creating the following public:\n") - headers = ("ID", "Name", 'IPv4 Subnet', "IPv6 Subnet", 'Mac Prefix') - table = [] - - for net in networks: - subnet4 = net.subnet4.cidr if net.subnet4 else None - subnet6 = net.subnet6.cidr if net.subnet6 else None - table.append((net.id, net.backend_id, subnet4, - subnet6, str(net.mac_prefix))) - pprint_table(stream, table, headers) - - for net in networks: - net.create_backend_network(backend) - result = backend_mod.create_network_synced(net, backend) - if result[0] != "success": - stream.write('\nError Creating Network %s: %s\n' % - (net.backend_id, result[1])) - else: - stream.write('Successfully created Network: %s\n' % - net.backend_id) - result = backend_mod.connect_network_synced(network=net, - backend=backend) - if result[0] != "success": - stream.write('\nError Connecting Network %s: %s\n' % - (net.backend_id, result[1])) - else: - stream.write('Successfully connected Network: %s\n' % - net.backend_id) + self.create_backend(clustername, port, username, password, + hypervisor=options["hypervisor"], + initialize=options["init"]) + + def create_backend(self, clustername, port, username, password, + hypervisor=None, initialize=True): + kw = {"clustername": clustername, + "port": port, + "username": username, + "password": password, + "drained": True} + + if hypervisor: + kw["hypervisor"] = hypervisor + + # Create the new backend in database + try: + backend = Backend.objects.create(**kw) + except IntegrityError as e: + raise CommandError("Cannot create backend: %s\n" % e) + + self.stderr.write("Successfully created backend with id %d\n" + % backend.id) + + if not initialize: + return + + self.stderr.write("Retrieving backend resources:\n") + resources = backend_mod.get_physical_resources(backend) + attr = ['mfree', 'mtotal', 'dfree', + 'dtotal', 'pinst_cnt', 'ctotal'] + + table = [[str(resources[x]) for x in attr]] + pprint_table(self.stdout, table, attr) + + backend_mod.update_backend_resources(backend, resources) + backend_mod.update_backend_disk_templates(backend) + + networks = Network.objects.filter(deleted=False, public=True) + if not networks: + return + + self.stderr.write("Creating the following public:\n") + headers = ("ID", "Name", 'IPv4 Subnet', + "IPv6 Subnet", 'Mac Prefix') + table = [] + + for net in networks: + subnet4 = net.subnet4.cidr if net.subnet4 else None + subnet6 = net.subnet6.cidr if net.subnet6 else None + table.append((net.id, net.backend_id, subnet4, + subnet6, str(net.mac_prefix))) + pprint_table(self.stdout, table, headers) + + for net in networks: + net.create_backend_network(backend) + result = backend_mod.create_network_synced(net, backend) + if result[0] != "success": + self.stderr.write('\nError Creating Network %s: %s\n' + % (net.backend_id, result[1])) + else: + self.stderr.write('Successfully created Network: %s\n' + % net.backend_id) + result = backend_mod.connect_network_synced(network=net, + backend=backend) + if result[0] != "success": + self.stderr.write('\nError Connecting Network %s: %s\n' + % (net.backend_id, result[1])) + else: + self.stderr.write('Successfully connected Network: %s\n' + % net.backend_id) diff --git a/snf-cyclades-app/synnefo/logic/management/commands/backend-list.py b/snf-cyclades-app/synnefo/logic/management/commands/backend-list.py index 04f44d5bfe08af7fba47182256de7a0832d93b9b..32f22ec30056e9dce0a05f37c4c97fc336732d7a 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/backend-list.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/backend-list.py @@ -1,35 +1,17 @@ -# Copyright 2012-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from synnefo.db.models import Backend from snf_django.management.commands import ListCommand @@ -55,7 +37,8 @@ class Command(ListCommand): for network in util.backend_public_networks(backend): total, free = network.ip_count() total_ips += total - free_ips += free + if not network.drained: + free_ips += free return "%s/%s" % (free_ips, total_ips) FIELDS = { diff --git a/snf-cyclades-app/synnefo/logic/management/commands/backend-modify.py b/snf-cyclades-app/synnefo/logic/management/commands/backend-modify.py index a8fcee2e32181895052f57bf01adfa3f93881e1a..63d2fffa116c7d7e2ab44f073e506f6711b7caa8 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/backend-modify.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/backend-modify.py @@ -1,51 +1,35 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import CommandError + +from snf_django.management.commands import SynnefoCommand from synnefo.db.models import Backend from snf_django.management.utils import parse_bool -from synnefo.management.common import (get_backend, check_backend_credentials) +from synnefo.management import common HYPERVISORS = [h[0] for h in Backend.HYPERVISORS] -class Command(BaseCommand): +class Command(SynnefoCommand): output_transaction = True - args = "<backend ID>" + args = "<backend_id>" help = "Modify a backend" - option_list = BaseCommand.option_list + ( + option_list = SynnefoCommand.option_list + ( make_option('--clustername', dest='clustername', help="Set backend's clustername"), @@ -82,7 +66,7 @@ class Command(BaseCommand): if len(args) != 1: raise CommandError("Please provide a backend ID") - backend = get_backend(args[0]) + backend = common.get_resource("backend", args[0], for_update=True) # Ensure fields correspondence with options and Backend model credentials_changed = False @@ -95,8 +79,10 @@ class Command(BaseCommand): if credentials_changed: # check credentials, if any of them changed! - check_backend_credentials(backend.clustername, backend.port, - backend.username, backend.password) + common.check_backend_credentials(backend.clustername, + backend.port, + backend.username, + backend.password) if options['drained']: backend.drained = parse_bool(options['drained'], strict=True) if options['offline']: diff --git a/snf-cyclades-app/synnefo/logic/management/commands/backend-remove.py b/snf-cyclades-app/synnefo/logic/management/commands/backend-remove.py index efdb96f78ad75d12f2773c3be58f2e4b853f1990..1636d84ae677f87b9ac615dbbdcec60e1355a80f 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/backend-remove.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/backend-remove.py @@ -1,38 +1,27 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # -from django.core.management.base import BaseCommand, CommandError -from synnefo.management.common import get_backend +from django.core.management.base import CommandError + +from snf_django.management.commands import SynnefoCommand +from synnefo.management import common from synnefo.logic import backend as backend_mod from synnefo.db.models import Backend -from django.db import transaction, models +from django.db import models +from synnefo.db import transaction HELP_MSG = """\ @@ -42,7 +31,8 @@ this Backend. Removal of a backend will fail if the backend hosts any non-deleted instances.""" -class Command(BaseCommand): +class Command(SynnefoCommand): + args = "<backend_id>" help = HELP_MSG def handle(self, *args, **options): @@ -50,7 +40,7 @@ class Command(BaseCommand): if len(args) < 1: raise CommandError("Please provide a backend ID") - backend = get_backend(args[0]) + backend = common.get_resource("backend", args[0], for_update=True) write("Trying to remove backend: %s\n" % backend.clustername) diff --git a/snf-cyclades-app/synnefo/logic/management/commands/backend-update-status.py b/snf-cyclades-app/synnefo/logic/management/commands/backend-update-status.py index b54a9ee5cf83438a55e4f0be84212c74eadba6b3..fe632e4499b65c1e5e87fdad06702797e5d165eb 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/backend-update-status.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/backend-update-status.py @@ -1,34 +1,19 @@ -# Copyright 2011-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of GRNET S.A. -# -from django.core.management.base import BaseCommand - +from snf_django.management.commands import SynnefoCommand from synnefo.db.models import Backend from synnefo.logic import backend as backend_mod @@ -41,7 +26,7 @@ This command updates: """ -class Command(BaseCommand): +class Command(SynnefoCommand): help = HELP_MSG def handle(self, **options): diff --git a/snf-cyclades-app/synnefo/logic/management/commands/cyclades-astakos-migrate-013.py b/snf-cyclades-app/synnefo/logic/management/commands/cyclades-astakos-migrate-013.py index ebcef0fa7e81c11425e1a14d1f17ed3c64f5ab09..1df1494b308623b7406980e9eabcf8220029e5a6 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/cyclades-astakos-migrate-013.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/cyclades-astakos-migrate-013.py @@ -1,35 +1,17 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import itertools import warnings @@ -38,7 +20,7 @@ import functools from optparse import make_option from django.core.management.base import NoArgsCommand, CommandError, BaseCommand -from django.db import transaction +from synnefo.db import transaction from django.conf import settings from synnefo.api.util import get_existing_users diff --git a/snf-cyclades-app/synnefo/logic/management/commands/flavor-create.py b/snf-cyclades-app/synnefo/logic/management/commands/flavor-create.py index a69d7fcd5e5adc85f381c28a72e41a6cafad6ae9..b6957238a80171b16319369bd8af313b4334918a 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/flavor-create.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/flavor-create.py @@ -1,56 +1,53 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from itertools import product from optparse import make_option -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import CommandError + +from snf_django.management.commands import SynnefoCommand +from synnefo.db.models import Flavor, VolumeType + + +HELP_MSG = """Create one or more flavors. -from synnefo.db.models import Flavor +Create one or more flavors (virtual hardware templates) that define the +compute, memory and storage capacity of virtual servers. The flavors that will +be created are those belonging to the cartesian product of the arguments. +To create a flavor you must specify the following arguments: + * cpu: Number of virtual CPUs. + * ram: Size of virtual RAM (MB). + * disk: Size of virtual disk (GB). + * volume_type_id: ID of the volyme type defining the volume's disk + template. +""" -class Command(BaseCommand): + +class Command(SynnefoCommand): output_transaction = True + help = HELP_MSG - option_list = BaseCommand.option_list + ( + option_list = SynnefoCommand.option_list + ( make_option("-n", "--dry-run", dest="dry_run", action="store_true"), ) args = "<cpu>[,<cpu>,...] " \ "<ram>[,<ram>,...] " \ "<disk>[,<disk>,...] " \ - "<disk template>[,<disk template>,...]" - help = "Create one or more flavors.\n\nThe flavors that will be created"\ - " are those belonging to the cartesian product of the arguments" + "<volume_type_id>[,<volume_type_id>,...]" def handle(self, *args, **options): if len(args) != 4: @@ -59,31 +56,46 @@ class Command(BaseCommand): cpus = args[0].split(',') rams = args[1].split(',') disks = args[2].split(',') - templates = args[3].split(',') + + volume_types = [] + volume_type_ids = args[3].split(',') + for vol_t_id in volume_type_ids: + try: + vol_t_id = int(vol_t_id) + volume_types.append(VolumeType.objects.get(id=vol_t_id, + deleted=False)) + except ValueError: + raise CommandError("Invalid volume type ID: '%s'" % vol_t_id) + except (VolumeType.DoesNotExist, ValueError): + raise CommandError("Volume type with ID '%s' does not exist." + " Use 'snf-manage volume-type-list' to find" + " out available volume types." % vol_t_id) flavors = [] - for cpu, ram, disk, template in product(cpus, rams, disks, templates): + for cpu, ram, disk, volume_type in product(cpus, rams, disks, + volume_types): try: - flavors.append((int(cpu), int(ram), int(disk), template)) + flavors.append((int(cpu), int(ram), int(disk), volume_type)) except ValueError: raise CommandError("Invalid values") - for cpu, ram, disk, template in flavors: + for cpu, ram, disk, volume_type in flavors: if options["dry_run"]: flavor = Flavor(cpu=cpu, ram=ram, disk=disk, - disk_template=template) + volume_type=volume_type) self.stdout.write("Creating flavor '%s'\n" % (flavor.name,)) else: flavor, created = \ Flavor.objects.get_or_create(cpu=cpu, ram=ram, disk=disk, - disk_template=template) + volume_type=volume_type) if created: self.stdout.write("Created flavor '%s'\n" % (flavor.name,)) else: self.stdout.write("Flavor '%s' already exists\n" % flavor.name) if flavor.deleted: - msg = "Flavor '%s' is marked as deleted. Use"\ - " 'snf-manage flavor-modify' to restore this flavor\n"\ - % flavor.name + msg = "Flavor '%s' is marked as deleted." \ + " Use 'snf-manage flavor-modify' to" \ + " restore this flavor\n" \ + % flavor.name self.stdout.write(msg) diff --git a/snf-cyclades-app/synnefo/logic/management/commands/flavor-list.py b/snf-cyclades-app/synnefo/logic/management/commands/flavor-list.py index 3bc3900ba34551fa877a80aae228bc91e6eb769e..e4cec5c5c49f98f2624a67bff9bbd1c7cf3b2b0b 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/flavor-list.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/flavor-list.py @@ -1,35 +1,17 @@ -# Copyright 2012-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from snf_django.management.commands import ListCommand from synnefo.db.models import Flavor, VirtualMachine @@ -40,6 +22,7 @@ class Command(ListCommand): object_class = Flavor deleted_field = "deleted" + select_related = ("volume_type", ) def get_vms(flavor): return VirtualMachine.objects.filter(flavor=flavor, deleted=False)\ @@ -51,11 +34,12 @@ class Command(ListCommand): "cpu": ("cpu", "Number of CPUs"), "ram": ("ram", "Size(MB) of RAM"), "disk": ("disk", "Size(GB) of disk"), - "template": ("disk_template", "Disk template"), + "volume_type": ("volume_type_id", "Volume Type ID"), + "template": ("volume_type.disk_template", "Disk template"), "allow_create": ("allow_create", "Whether servers can be created from" " this flavor"), "vms": (get_vms, "Number of active servers using this flavor") } - fields = ["id", "name", "cpu", "ram", "disk", "template", "allow_create", - "vms"] + fields = ["id", "name", "cpu", "ram", "disk", "template", "volume_type", + "allow_create", "vms"] diff --git a/snf-cyclades-app/synnefo/logic/management/commands/flavor-modify.py b/snf-cyclades-app/synnefo/logic/management/commands/flavor-modify.py index fe7bff7aa0c6687542e80c1db9da27ee24981c9f..4ef49acf18ff1ec4ad9ddd53e15c0a7a4d9d69bb 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/flavor-modify.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/flavor-modify.py @@ -1,40 +1,24 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option -from django.core.management.base import BaseCommand, CommandError -from synnefo.management.common import get_flavor +from django.core.management.base import CommandError + +from snf_django.management.commands import SynnefoCommand +from synnefo.management.common import get_resource from snf_django.management.utils import parse_bool @@ -42,11 +26,11 @@ from logging import getLogger log = getLogger(__name__) -class Command(BaseCommand): - args = "<flavor id>" +class Command(SynnefoCommand): + args = "<flavor_id>" help = "Modify a flavor" - option_list = BaseCommand.option_list + ( + option_list = SynnefoCommand.option_list + ( make_option( "--deleted", dest="deleted", @@ -67,7 +51,7 @@ class Command(BaseCommand): if len(args) != 1: raise CommandError("Please provide a flavor ID") - flavor = get_flavor(args[0], for_update=True) + flavor = get_resource("flavor", args[0], for_update=True) deleted = options['deleted'] diff --git a/snf-cyclades-app/synnefo/logic/management/commands/floating-ip-attach.py b/snf-cyclades-app/synnefo/logic/management/commands/floating-ip-attach.py index d11c1f33a66bbc7d7d52123dd3b66eb0b26629cd..e3a7541d759f8a224a38438bc38b13d0774508c9 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/floating-ip-attach.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/floating-ip-attach.py @@ -1,47 +1,32 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import CommandError + +from snf_django.management.commands import SynnefoCommand from synnefo.management import common from synnefo.logic import servers -class Command(BaseCommand): +class Command(SynnefoCommand): + args = "<floating_ip_id>" help = "Attach a floating IP to a VM or router" - option_list = BaseCommand.option_list + ( + option_list = SynnefoCommand.option_list + ( make_option( '--machine', dest='machine', @@ -61,9 +46,9 @@ class Command(BaseCommand): raise CommandError('Please give either a server or a router id') #get the vm - vm = common.get_vm(device, for_update=True) - floating_ip = common.get_floating_ip_by_id(floating_ip_id, - for_update=True) + vm = common.get_resource("server", device, for_update=True) + floating_ip = common.get_resource("floating-ip", floating_ip_id, + for_update=True) servers.create_port(vm.userid, floating_ip.network, use_ipaddress=floating_ip, machine=vm) diff --git a/snf-cyclades-app/synnefo/logic/management/commands/floating-ip-create.py b/snf-cyclades-app/synnefo/logic/management/commands/floating-ip-create.py index 7fe91d2cfe06329a8de5642a16810f3244a60fbd..f194bdd77c0d2adbc5aa74e6f32b8807c6d0225d 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/floating-ip-create.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/floating-ip-create.py @@ -1,48 +1,31 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option -from django.core.management.base import BaseCommand, CommandError -from synnefo.management.common import convert_api_faults +from django.core.management.base import CommandError + +from snf_django.management.commands import SynnefoCommand +from synnefo.management import common from synnefo.logic import ips -from synnefo.api import util -class Command(BaseCommand): +class Command(SynnefoCommand): help = "Allocate a new floating IP" - option_list = BaseCommand.option_list + ( + option_list = SynnefoCommand.option_list + ( make_option( '--network', dest='network_id', @@ -52,34 +35,36 @@ class Command(BaseCommand): dest='address', help="The address to be allocated"), make_option( - '--owner', - dest='owner', + '--user', + dest='user', default=None, help='The owner of the floating IP'), ) - @convert_api_faults + @common.convert_api_faults def handle(self, *args, **options): if args: raise CommandError("Command doesn't accept any arguments") network_id = options['network_id'] address = options['address'] - owner = options['owner'] + user = options['user'] - if not owner: - raise CommandError("'owner' is required for floating IP creation") + if not user: + raise CommandError("'user' is required for floating IP creation") if network_id is not None: - network = util.get_network(network_id, owner, for_update=True, - non_deleted=True) + network = common.get_resource("network", network_id, + for_update=True) + if network.deleted: + raise CommandError("Network '%s' is deleted" % network.id) if not network.floating_ip_pool: raise CommandError("Network '%s' is not a floating IP pool." % network) else: network = None - floating_ip = ips.create_floating_ip(userid=owner, + floating_ip = ips.create_floating_ip(userid=user, network=network, address=address) diff --git a/snf-cyclades-app/synnefo/logic/management/commands/floating-ip-detach.py b/snf-cyclades-app/synnefo/logic/management/commands/floating-ip-detach.py index 0fa8a6fd79de5f633133e999cd5a177f0b5129b0..d2e9df344271b58e96218fe2c21cd479d5206b25 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/floating-ip-detach.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/floating-ip-detach.py @@ -1,44 +1,29 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. #from optparse import make_option -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import CommandError + +from snf_django.management.commands import SynnefoCommand from synnefo.management import common from synnefo.logic import servers -class Command(BaseCommand): +class Command(SynnefoCommand): + args = "<floating_ip_id>" help = "Detach a floating IP from a VM or router" @common.convert_api_faults @@ -49,8 +34,8 @@ class Command(BaseCommand): floating_ip_id = args[0] #get the floating-ip - floating_ip = common.get_floating_ip_by_id(floating_ip_id, - for_update=True) + floating_ip = common.get_resource("floating-ip", floating_ip_id, + for_update=True) if not floating_ip.nic: raise CommandError('This floating IP is not attached to a device') diff --git a/snf-cyclades-app/synnefo/logic/management/commands/floating-ip-list.py b/snf-cyclades-app/synnefo/logic/management/commands/floating-ip-list.py index 8b02576ebf07ae39bc08b13de7c3ee745d8514f4..32fe847b667c7fcfd178f6f9b089c0594ada7f45 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/floating-ip-list.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/floating-ip-list.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from synnefo.db.models import IPAddress from snf_django.management.commands import ListCommand diff --git a/snf-cyclades-app/synnefo/logic/management/commands/floating-ip-remove.py b/snf-cyclades-app/synnefo/logic/management/commands/floating-ip-remove.py index 3ab761ae8e3cd420730cfb897f5bed78a34bab5a..6cd46dd32b1be50dca1ff3cf8bd65617b918f9e1 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/floating-ip-remove.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/floating-ip-remove.py @@ -1,39 +1,21 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. #from optparse import make_option -from django.db import transaction +from synnefo.db import transaction from django.core.management.base import CommandError from snf_django.management.commands import RemoveCommand from synnefo.management import common @@ -41,7 +23,7 @@ from synnefo.logic import ips class Command(RemoveCommand): - args = "<Floating-IP ID> [<Floating-IP ID> ...]" + args = "<floating_ip_id> [<floating_ip_id> ...]" help = "Release a floating IP" @common.convert_api_faults @@ -57,8 +39,9 @@ class Command(RemoveCommand): for floating_ip_id in args: self.stdout.write("\n") try: - floating_ip = common.get_floating_ip_by_id(floating_ip_id, - for_update=True) + floating_ip = common.get_resource("floating-ip", + floating_ip_id, + for_update=True) ips.delete_floating_ip(floating_ip) self.stdout.write("Deleted floating IP '%s'.\n" % floating_ip_id) diff --git a/snf-cyclades-app/synnefo/logic/management/commands/ip-list.py b/snf-cyclades-app/synnefo/logic/management/commands/ip-list.py index df7979a8fb335b23370adb95e55681927b9210b0..ef363e79fc7b8f1d4a804d1aa2e5a08e5b39d97c 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/ip-list.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/ip-list.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from snf_django.management.commands import ListCommand diff --git a/snf-cyclades-app/synnefo/logic/management/commands/network-create.py b/snf-cyclades-app/synnefo/logic/management/commands/network-create.py index 75b6dc6c013a16ac72e83da72114c2e3f1ed69c5..46048ec13be24e214829635949359f32f0ec7459 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/network-create.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/network-create.py @@ -1,39 +1,23 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import CommandError + +from snf_django.management.commands import SynnefoCommand from synnefo.management.common import convert_api_faults from snf_django.management.utils import parse_bool @@ -41,25 +25,24 @@ from synnefo.db.models import Network from synnefo.logic import networks, subnets from synnefo.management import pprint -import ipaddr NETWORK_FLAVORS = Network.FLAVORS.keys() -class Command(BaseCommand): +class Command(SynnefoCommand): can_import_settings = True output_transaction = True help = "Create a new network" - option_list = BaseCommand.option_list + ( + option_list = SynnefoCommand.option_list + ( make_option( '--name', dest='name', help="Name of the network"), make_option( - '--owner', - dest='owner', + '--user', + dest='user', help="The owner of the network"), make_option( '--subnet', @@ -163,7 +146,7 @@ class Command(BaseCommand): link = options['link'] mac_prefix = options['mac_prefix'] tags = options['tags'] - userid = options["owner"] + userid = options["user"] allocation_pools = options["allocation_pools"] floating_ip_pool = parse_bool(options["floating_ip_pool"]) dhcp = parse_bool(options["dhcp"]) @@ -188,7 +171,7 @@ class Command(BaseCommand): " subnet") if not (userid or public): - raise CommandError("'owner' is required for private networks") + raise CommandError("'user' is required for private networks") network = networks.create(userid=userid, name=name, flavor=flavor, public=public, mode=mode, diff --git a/snf-cyclades-app/synnefo/logic/management/commands/network-inspect.py b/snf-cyclades-app/synnefo/logic/management/commands/network-inspect.py index 85b952a7a14e2e7628e6e6966d5da157d4c0f302..31217680bd4feb5581ac2f19145b08207dc06ed9 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/network-inspect.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/network-inspect.py @@ -1,67 +1,61 @@ -# Copyright 2012-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import CommandError + +from snf_django.management.commands import SynnefoCommand from synnefo.management import pprint, common +from snf_django.management.utils import parse_bool -class Command(BaseCommand): +class Command(SynnefoCommand): help = "Inspect a network on DB and Ganeti." - args = "<Network ID>" + args = "<network_id>" - option_list = BaseCommand.option_list + ( + option_list = SynnefoCommand.option_list + ( make_option( - '--displayname', - action='store_true', - dest='displayname', + "--display-mails", + action="store_true", + dest="displaymail", default=False, - help="Display both uuid and display name"), - ) + help="Display both UUID and email"), + make_option( + "--backends", + dest="backends", + choices=["True", "False"], + metavar="True|False", + default="True", + help="Inspect state of network in all Ganeti backends") + ) def handle(self, *args, **options): if len(args) != 1: raise CommandError("Please provide a network ID.") - network = common.get_network(args[0]) - displayname = options['displayname'] + network = common.get_resource("network", args[0]) + display_mails = options["displaymail"] - pprint.pprint_network(network, display_mails=displayname, + pprint.pprint_network(network, display_mails=display_mails, stdout=self.stdout) self.stdout.write("\n\n") pprint.pprint_network_subnets(network, stdout=self.stdout) self.stdout.write("\n\n") pprint.pprint_network_backends(network, stdout=self.stdout) - self.stdout.write("\n\n") - pprint.pprint_network_in_ganeti(network, stdout=self.stdout) + backends = parse_bool(options["backends"]) + if backends: + self.stdout.write("\n\n") + pprint.pprint_network_in_ganeti(network, stdout=self.stdout) diff --git a/snf-cyclades-app/synnefo/logic/management/commands/network-list.py b/snf-cyclades-app/synnefo/logic/management/commands/network-list.py index 171ce2e09262189f842e2a080a57bcd5f9ed8742..99f582bdfbe48dfc3eb22d313b49187def866792 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/network-list.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/network-list.py @@ -1,35 +1,17 @@ -# Copyright 2012-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option diff --git a/snf-cyclades-app/synnefo/logic/management/commands/network-modify.py b/snf-cyclades-app/synnefo/logic/management/commands/network-modify.py index 947ea7f9c6060bd34bd85dd9c9c7d1c4c7a12ec1..83dfcf29e00e8c6a083bccf523401855ff262094 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/network-modify.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/network-modify.py @@ -1,59 +1,42 @@ -# Copyright 2012-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import CommandError from synnefo.db.models import Backend -from synnefo.management.common import (get_network, get_backend) +from synnefo.management.common import get_resource +from snf_django.management.commands import SynnefoCommand from snf_django.management.utils import parse_bool from synnefo.logic import networks, backend as backend_mod -from django.db import transaction +from synnefo.db import transaction -class Command(BaseCommand): - args = "<Network ID>" +class Command(SynnefoCommand): + args = "<network_id>" help = "Modify a network." - option_list = BaseCommand.option_list + ( + option_list = SynnefoCommand.option_list + ( make_option( '--name', dest='name', metavar='NAME', help="Rename a network"), make_option( - '--userid', + '--user', dest='userid', help="Change the owner of the network."), make_option( @@ -68,7 +51,7 @@ class Command(BaseCommand): metavar="True|False", choices=["True", "False"], help="Convert network to a floating IP pool. During this" - " conversation the network will be created to all" + " conversion the network will be created to all" " available Ganeti backends."), make_option( '--add-reserved-ips', @@ -95,7 +78,7 @@ class Command(BaseCommand): if len(args) != 1: raise CommandError("Please provide a network ID") - network = get_network(args[0]) + network = get_resource("network", args[0]) new_name = options.get("name") if new_name is not None: @@ -159,7 +142,7 @@ class Command(BaseCommand): add_to_backend = options["add_to_backend"] if add_to_backend is not None: - backend = get_backend(add_to_backend) + backend = get_resource("backend", add_to_backend) bnet, jobs = backend_mod.ensure_network_is_active(backend, network.id) if jobs: @@ -168,7 +151,7 @@ class Command(BaseCommand): remove_from_backend = options["remove_from_backend"] if remove_from_backend is not None: - backend = get_backend(remove_from_backend) + backend = get_resource("backend", remove_from_backend) if network.nics.filter(machine__backend=backend, machine__deleted=False).exists(): msg = "Cannot remove. There are still connected VMs to this"\ diff --git a/snf-cyclades-app/synnefo/logic/management/commands/network-remove.py b/snf-cyclades-app/synnefo/logic/management/commands/network-remove.py index 2f2b8665996ed41efc1287c38be7b99fa021503d..3fc6c4d519ceecf4d7c2c545b1b5e246fd7f32f0 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/network-remove.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/network-remove.py @@ -1,46 +1,32 @@ -# Copyright 2011-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # from django.core.management.base import CommandError from snf_django.management.commands import RemoveCommand from snf_django.lib.api import faults from synnefo.logic import networks -from synnefo.management.common import get_network, convert_api_faults +from synnefo.management import common class Command(RemoveCommand): can_import_settings = True - args = "<Network ID> [<Network ID> ...]" + args = "<network_id> [<network_id> ...]" help = "Remove a network from the Database, and Ganeti" - @convert_api_faults + @common.convert_api_faults def handle(self, *args, **options): if not args: raise CommandError("Please provide a network ID") @@ -52,7 +38,8 @@ class Command(RemoveCommand): for network_id in args: self.stdout.write("\n") try: - network = get_network(network_id, for_update=True) + network = common.get_resource("network", network_id, + for_update=True) self.stdout.write('Removing network: %s\n' % network.backend_id) diff --git a/snf-cyclades-app/synnefo/logic/management/commands/pool-create.py b/snf-cyclades-app/synnefo/logic/management/commands/pool-create.py index 1eb59740e00e4611a6650e0aa94ab90e9216897d..4c5d29674b0c83c5ba9d076ec719c03a7655af9f 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/pool-create.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/pool-create.py @@ -1,48 +1,32 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import CommandError from optparse import make_option + +from snf_django.management.commands import SynnefoCommand from synnefo.db.utils import validate_mac from synnefo.management.common import pool_table_from_type POOL_CHOICES = ['bridge', 'mac-prefix'] -class Command(BaseCommand): +class Command(SynnefoCommand): help = "Create a new pool of resources." output_transaction = True - option_list = BaseCommand.option_list + ( + option_list = SynnefoCommand.option_list + ( make_option("--type", dest="type", choices=POOL_CHOICES, help="Type of pool. Choices:" diff --git a/snf-cyclades-app/synnefo/logic/management/commands/pool-list.py b/snf-cyclades-app/synnefo/logic/management/commands/pool-list.py index 640f79d1fdd7794badc4a0c330879b3de65147be..740c76813908b2ca7a2f79c91866bca57fa96f6c 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/pool-list.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/pool-list.py @@ -1,47 +1,28 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. - -from django.core.management.base import BaseCommand +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option +from snf_django.management.commands import SynnefoCommand from synnefo.management.common import pool_table_from_type POOL_CHOICES = ['bridge', 'mac-prefix'] -class Command(BaseCommand): +class Command(SynnefoCommand): help = "List available pools" output_transaction = True - option_list = BaseCommand.option_list + ( + option_list = SynnefoCommand.option_list + ( make_option('--type', dest='type', choices=POOL_CHOICES, help="Type of pool" diff --git a/snf-cyclades-app/synnefo/logic/management/commands/pool-modify.py b/snf-cyclades-app/synnefo/logic/management/commands/pool-modify.py index 7388f1e3a802b3e1fd741c27fdeda5b03f58a8ed..8f9d9f7bd26a14ed14e972cbdceddd0db3f624ba 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/pool-modify.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/pool-modify.py @@ -1,48 +1,32 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import CommandError from optparse import make_option + +from snf_django.management.commands import SynnefoCommand from synnefo.management.common import pool_table_from_type POOL_CHOICES = ['bridge', 'mac-prefix'] -class Command(BaseCommand): - args = "<pool ID>" +class Command(SynnefoCommand): + args = "<pool_id>" help = "Modify a pool" output_transaction = True - option_list = BaseCommand.option_list + ( + option_list = SynnefoCommand.option_list + ( make_option('--type', dest='type', choices=POOL_CHOICES, help="Type of pool" diff --git a/snf-cyclades-app/synnefo/logic/management/commands/pool-remove.py b/snf-cyclades-app/synnefo/logic/management/commands/pool-remove.py index e6e90c0a182818cfe4fc46ff6590a0c0313c04eb..f2d2bb1c82e7fba7b14c4f6989eac0e3db0eced4 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/pool-remove.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/pool-remove.py @@ -1,35 +1,17 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.core.management.base import CommandError from snf_django.management.commands import RemoveCommand @@ -42,9 +24,9 @@ POOL_CHOICES = ['bridge', 'mac-prefix'] class Command(RemoveCommand): help = "Remove a pool." - args = "<pool ID>" + args = "<pool_id>" output_transaction = True - option_list = RemoveCommand.option_list + ( + command_option_list = RemoveCommand.command_option_list + ( make_option("--type", dest="type", choices=POOL_CHOICES, help="Type of pool" diff --git a/snf-cyclades-app/synnefo/logic/management/commands/pool-show.py b/snf-cyclades-app/synnefo/logic/management/commands/pool-show.py index 40858933478e0d92f3f3c509904d336f0f8e5600..6a2447dc588982c411d694b383ebf7fec6668b4e 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/pool-show.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/pool-show.py @@ -1,50 +1,33 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import CommandError from optparse import make_option from synnefo.db.pools import bitarray_to_map from synnefo.management import pprint, common +from snf_django.management.commands import SynnefoCommand POOL_CHOICES = ['bridge', 'mac-prefix'] -class Command(BaseCommand): - args = "<pool ID>" +class Command(SynnefoCommand): + args = "<pool_id>" help = "Show a pool" output_transaction = True - option_list = BaseCommand.option_list + ( + option_list = SynnefoCommand.option_list + ( make_option('--type', dest='type', choices=POOL_CHOICES, help="Type of pool" @@ -62,6 +45,8 @@ class Command(BaseCommand): try: pool_id = int(args[0]) pool_row = pool_table.objects.get(id=pool_id) + except IndexError: + raise CommandError("Please provide a pool ID") except (ValueError, pool_table.DoesNotExist): raise CommandError("Invalid pool ID") diff --git a/snf-cyclades-app/synnefo/logic/management/commands/port-create.py b/snf-cyclades-app/synnefo/logic/management/commands/port-create.py index aaea096197b8e2426be9a9506e6abf983c9768e7..cb75b93403c1d6b9b47d1de5bfd6570c40d548e6 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/port-create.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/port-create.py @@ -1,43 +1,26 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import CommandError from synnefo.api import util from synnefo.management import common, pprint from snf_django.management.utils import parse_bool +from snf_django.management.commands import SynnefoCommand from synnefo.logic import servers HELP_MSG = """Create a new port. @@ -48,17 +31,17 @@ Otherwise, the port will get an IP address for each Subnet that is associated with the network.""" -class Command(BaseCommand): +class Command(SynnefoCommand): help = HELP_MSG - option_list = BaseCommand.option_list + ( + option_list = SynnefoCommand.option_list + ( make_option( "--name", dest="name", default=None, help="Name of the port."), make_option( - "--owner", + "--user", dest="user_id", default=None, help="UUID of the owner of the Port."), @@ -99,7 +82,7 @@ class Command(BaseCommand): default="True", choices=["True", "False"], metavar="True|False", - help="Wait for Ganeti jobs to complete."), + help="Wait for Ganeti jobs to complete. [Default: True]"), ) @common.convert_api_faults @@ -127,12 +110,12 @@ class Command(BaseCommand): owner = None if server_id: owner = "vm" - vm = common.get_vm(server_id, for_update=True) + vm = common.get_resource("server", server_id, for_update=True) #if vm.router: # raise CommandError("Server '%s' does not exist." % server_id) elif router_id: owner = "router" - vm = common.get_vm(router_id, for_update=True) + vm = common.get_resource("server", router_id, for_update=True) if not vm.router: raise CommandError("Router '%s' does not exist." % router_id) @@ -143,21 +126,20 @@ class Command(BaseCommand): raise CommandError("Please specify the owner of the port.") # get the network - network = common.get_network(network_id) + network = common.get_resource("network", network_id) # Get either floating IP or fixed ip address ipaddress = None floating_ip_id = options["floating_ip_id"] ipv4_address = options["ipv4_address"] if floating_ip_id: - ipaddress = common.get_floating_ip_by_id(floating_ip_id, - for_update=True) + ipaddress = common.get_resource("floating-ip", floating_ip_id, + for_update=True) if ipv4_address is not None and ipaddress.address != ipv4_address: raise CommandError("Floating IP address '%s' is different from" " specified address '%s'" % (ipaddress.address, ipv4_address)) - # validate security groups sg_list = [] if security_group_ids: diff --git a/snf-cyclades-app/synnefo/logic/management/commands/port-inspect.py b/snf-cyclades-app/synnefo/logic/management/commands/port-inspect.py index 87e7a292c0aceb05e41489769d0e82a175a2bea0..b1a2f29937aa942e598e0e1a6eb2cbbbf01dcbbd 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/port-inspect.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/port-inspect.py @@ -1,48 +1,31 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import CommandError +from snf_django.management.commands import SynnefoCommand from synnefo.management.common import convert_api_faults from synnefo.management import pprint, common -class Command(BaseCommand): +class Command(SynnefoCommand): help = "Inspect a port on DB and Ganeti" - args = "<port ID>" + args = "<port_id>" - option_list = BaseCommand.option_list + ( + option_list = SynnefoCommand.option_list + ( make_option( '--jobs', action='store_true', @@ -50,11 +33,11 @@ class Command(BaseCommand): default=False, help="Show non-archived jobs concerning port."), make_option( - '--displayname', + '--display-mails', action='store_true', - dest='displayname', + dest='displaymails', default=False, - help="Display both uuid and display name"), + help="Display both uuid and email"), ) @convert_api_faults @@ -62,9 +45,11 @@ class Command(BaseCommand): if len(args) != 1: raise CommandError("Please provide a port ID") - port = common.get_port(args[0]) + port = common.get_resource("port", args[0]) + display_mails = options['displaymails'] - pprint.pprint_port(port, stdout=self.stdout) + pprint.pprint_port(port, display_mails=display_mails, + stdout=self.stdout) self.stdout.write('\n\n') pprint.pprint_port_ips(port, stdout=self.stdout) diff --git a/snf-cyclades-app/synnefo/logic/management/commands/port-list.py b/snf-cyclades-app/synnefo/logic/management/commands/port-list.py index 7b2398bb3d2a661c0c4c2da43b9cc4e0f0a13454..e9d7e0e97a94ad7007b246b19a4889decb3b156e 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/port-list.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/port-list.py @@ -1,35 +1,17 @@ -# Copyright 2012-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option diff --git a/snf-cyclades-app/synnefo/logic/management/commands/port-remove.py b/snf-cyclades-app/synnefo/logic/management/commands/port-remove.py index e416821f2928d8c8c8e674ed6cf6e7bab548111e..35839aee8c910af52868650cd08fa69d4ba32fb1 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/port-remove.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/port-remove.py @@ -1,31 +1,17 @@ -# Copyright 2011-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # from optparse import make_option @@ -38,16 +24,16 @@ from snf_django.management.commands import RemoveCommand class Command(RemoveCommand): can_import_settings = True - args = "<Port ID> [<Port ID> ...]" + args = "<port_id> [<port_id> ...]" help = "Remove a port from the Database and from the VMs attached to" - option_list = RemoveCommand.option_list + ( + command_option_list = RemoveCommand.command_option_list + ( make_option( "--wait", dest="wait", default="True", choices=["True", "False"], metavar="True|False", - help="Wait for Ganeti jobs to complete."), + help="Wait for Ganeti jobs to complete. [Default: True]"), ) @common.convert_api_faults @@ -62,7 +48,7 @@ class Command(RemoveCommand): for port_id in args: self.stdout.write("\n") try: - port = common.get_port(port_id, for_update=True) + port = common.get_resource("port", port_id, for_update=True) servers.delete_port(port) diff --git a/snf-cyclades-app/synnefo/logic/management/commands/queue-inspect.py b/snf-cyclades-app/synnefo/logic/management/commands/queue-inspect.py index 80d74c7a4e108908ff59313c37e4e53dcaabd514..54d52fc41c6015f994bd9fe653de45d2dbf25761 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/queue-inspect.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/queue-inspect.py @@ -1,47 +1,34 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # import pprint from optparse import make_option -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import CommandError from synnefo.lib.amqp import AMQPClient +from snf_django.management.commands import SynnefoCommand -class Command(BaseCommand): - args = "<queue name>" +class Command(SynnefoCommand): + args = "<queue_name>" help = "Inspect the messages of a queue. Close all other clients in "\ "order to be able to inspect unacknowledged messages." - option_list = BaseCommand.option_list + ( + option_list = SynnefoCommand.option_list + ( make_option('--no-requeue', action='store_false', dest='requeue', default=True, help="Do not requeue the messages"), make_option('-i', '--interactive', action='store_true', default=False, @@ -59,7 +46,7 @@ class Command(BaseCommand): client = AMQPClient() client.connect() - pp = pprint.PrettyPrinter(indent=4, width=4) + pp = pprint.PrettyPrinter(indent=4, width=4, stream=self.stdout) more_msgs = True counter = 0 @@ -68,9 +55,9 @@ class Command(BaseCommand): msg = client.basic_get(queue=queue) if msg: counter += 1 - print sep - print 'Message %d:' % counter - print sep + self.stderr.write(sep + "\n") + self.stderr.write('Message %d:\n' % counter) + self.stderr.write(sep + "\n") pp.pprint(msg) if not requeue or interactive: if interactive and not get_user_confirmation(): diff --git a/snf-cyclades-app/synnefo/logic/management/commands/queue-retry.py b/snf-cyclades-app/synnefo/logic/management/commands/queue-retry.py index 2a697e2e2a9cc6d3b7bca9c06a66b4348629aace..a7f7147751d4470a87f6e0ec238d7bbf559178ad 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/queue-retry.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/queue-retry.py @@ -1,36 +1,22 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of GRNET S.A. -# -from django.core.management.base import BaseCommand from optparse import make_option from synnefo.lib.amqp import AMQPClient +from snf_django.management.commands import SynnefoCommand from synnefo.logic import queues @@ -39,10 +25,10 @@ import logging log = logging.getLogger("") -class Command(BaseCommand): +class Command(SynnefoCommand): help = "Resend messages from dead letter queues to original exchange""" - option_list = BaseCommand.option_list + ( + option_list = SynnefoCommand.option_list + ( make_option( '--keep-zombies', action='store_true', diff --git a/snf-cyclades-app/synnefo/logic/management/commands/reconcile-networks.py b/snf-cyclades-app/synnefo/logic/management/commands/reconcile-networks.py index e86d609554cf344c8c1660e4be655f9b476f3a7b..ff132c329e739200d62ada019119065aa7b6fa93 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/reconcile-networks.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/reconcile-networks.py @@ -1,31 +1,17 @@ -# Copyright 2011-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # """Reconciliation management command @@ -36,11 +22,12 @@ logic/reconciliation.py for a description of reconciliation rules. """ import logging from optparse import make_option -from django.core.management.base import BaseCommand + from synnefo.logic import reconciliation +from snf_django.management.commands import SynnefoCommand -class Command(BaseCommand): +class Command(SynnefoCommand): help = """Reconcile contents of Synnefo DB with state of Ganeti backend Network reconciliation can detect and fix the following cases: @@ -53,7 +40,7 @@ Network reconciliation can detect and fix the following cases: """ can_import_settings = True - option_list = BaseCommand.option_list + ( + option_list = SynnefoCommand.option_list + ( make_option('--fix-all', action='store_true', dest='fix', default=False, help='Fix all issues.'), diff --git a/snf-cyclades-app/synnefo/logic/management/commands/reconcile-pools.py b/snf-cyclades-app/synnefo/logic/management/commands/reconcile-pools.py index 5a15961acdc6d0d33312a45e4c7bf830f6c5764e..4a1ca03b7ea016637b6b82a437ddc558b4154fd3 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/reconcile-pools.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/reconcile-pools.py @@ -1,36 +1,23 @@ -# Copyright 2011-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # import logging from optparse import make_option -from django.core.management.base import BaseCommand + from synnefo.logic import reconciliation +from snf_django.management.commands import SynnefoCommand HELP_MSG = """\ @@ -46,10 +33,10 @@ The pools for the following resources are checked: * Pool of IPv4 addresses for each network""" -class Command(BaseCommand): +class Command(SynnefoCommand): help = HELP_MSG - option_list = BaseCommand.option_list + ( + option_list = SynnefoCommand.option_list + ( make_option("--fix", action="store_true", dest="fix", default=False, help='Fix all issues.'), diff --git a/snf-cyclades-app/synnefo/logic/management/commands/reconcile-servers.py b/snf-cyclades-app/synnefo/logic/management/commands/reconcile-servers.py index f2b0fbbc716b8f1a4a4e266ab475cff3f24f75eb..f89508c3e7f217a001759284d2b76a4f0f6ad01b 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/reconcile-servers.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/reconcile-servers.py @@ -1,31 +1,17 @@ -# Copyright 2011-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # """Reconciliation management command @@ -38,17 +24,19 @@ import sys import logging import subprocess from optparse import make_option -from django.core.management.base import BaseCommand -from synnefo.management.common import get_backend + +from snf_django.management.commands import SynnefoCommand +from synnefo.management.common import get_resource from synnefo.logic import reconciliation from snf_django.management.utils import parse_bool -class Command(BaseCommand): +class Command(SynnefoCommand): can_import_settings = True + umask = 0o007 help = 'Reconcile contents of Synnefo DB with state of Ganeti backend' - option_list = BaseCommand.option_list + ( + option_list = SynnefoCommand.option_list + ( make_option('--backend-id', default=None, dest='backend-id', help='Reconcilie VMs only for this backend'), make_option("--parallel", @@ -68,12 +56,18 @@ class Command(BaseCommand): make_option('--fix-unsynced-nics', action='store_true', dest='fix_unsynced_nics', default=False, help='Fix unsynced nics between DB and Ganeti'), + make_option('--fix-unsynced-disks', action='store_true', + dest='fix_unsynced_disks', default=False, + help='Fix unsynced disks between DB and Ganeti'), make_option('--fix-unsynced-flavors', action='store_true', dest='fix_unsynced_flavors', default=False, help='Fix unsynced flavors between DB and Ganeti'), make_option('--fix-pending-tasks', action='store_true', dest='fix_pending_tasks', default=False, help='Fix servers with stale pending tasks.'), + make_option('--fix-unsynced-snapshots', action='store_true', + dest='fix_unsynced_snapshots', default=False, + help='Fix unsynced snapshots.'), make_option('--fix-all', action='store_true', dest='fix_all', default=False, help='Enable all --fix-* arguments'), ) @@ -87,7 +81,7 @@ class Command(BaseCommand): def handle(self, **options): backend_id = options['backend-id'] if backend_id: - backends = [get_backend(backend_id)] + backends = [get_resource("backend", backend_id)] else: backends = reconciliation.get_online_backends() diff --git a/snf-cyclades-app/synnefo/logic/management/commands/server-create.py b/snf-cyclades-app/synnefo/logic/management/commands/server-create.py index 7c60ed70d354bfd4b24f9926d2228700c90f5abf..a2c981441c26ad21ae9765951b1d91c0c8bd29d6 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/server-create.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/server-create.py @@ -1,41 +1,25 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import CommandError + from synnefo.management import common, pprint from snf_django.management.utils import parse_bool +from snf_django.management.commands import SynnefoCommand from synnefo.logic import servers @@ -47,23 +31,24 @@ backend-id. """ -class Command(BaseCommand): +class Command(SynnefoCommand): help = "Create a new VM." + HELP_MSG + umask = 0o007 - option_list = BaseCommand.option_list + ( + option_list = SynnefoCommand.option_list + ( make_option("--backend-id", dest="backend_id", help="Unique identifier of the Ganeti backend." " Use snf-manage backend-list to find out" " available backends."), make_option("--name", dest="name", help="An arbitrary string for naming the server"), - make_option("--user-id", dest="user_id", + make_option("--user", dest="user_id", help="Unique identifier of the owner of the server"), - make_option("--image-id", dest="image_id", + make_option("--image", dest="image_id", default=None, help="Unique identifier of the image." " Use snf-manage image-list to find out" " available images."), - make_option("--flavor-id", dest="flavor_id", + make_option("--flavor", dest="flavor_id", help="Unique identifier of the flavor" " Use snf-manage flavor-list to find out" " available flavors."), @@ -73,6 +58,11 @@ class Command(BaseCommand): help="--port network:<network_id>(,address=<ip_address>)," " --port id:<port_id>" " --port floatingip:<floatingip_id>."), + make_option("--volume", dest="volumes", action="append", + help="--volume size=<size>, --volume id=<volume_id>" + ", --volume size=<size>,image=<image_id>" + ", --volume size=<size>,snapshot=<snapshot_id>", + default=[]), make_option("--floating-ips", dest="floating_ip_ids", help="Comma separated list of port IDs to connect"), make_option( @@ -81,7 +71,7 @@ class Command(BaseCommand): default="False", choices=["True", "False"], metavar="True|False", - help="Wait for Ganeti job to complete."), + help="Wait for Ganeti job to complete. [Default: False]"), ) @@ -96,28 +86,33 @@ class Command(BaseCommand): image_id = options['image_id'] flavor_id = options['flavor_id'] password = options['password'] + volumes = options['volumes'] if not name: raise CommandError("name is mandatory") if not user_id: - raise CommandError("user-id is mandatory") + raise CommandError("user is mandatory") if not password: raise CommandError("password is mandatory") if not flavor_id: - raise CommandError("flavor-id is mandatory") - if not image_id: - raise CommandError("image-id is mandatory") + raise CommandError("flavor is mandatory") + if not image_id and not volumes: + raise CommandError("image is mandatory") + + flavor = common.get_resource("flavor", flavor_id) + if image_id is not None: + common.get_image(image_id, user_id) - flavor = common.get_flavor(flavor_id) - image = common.get_image(image_id, user_id) if backend_id: - backend = common.get_backend(backend_id) + backend = common.get_resource("backend", backend_id) else: backend = None connection_list = parse_connections(options["connections"]) - server = servers.create(user_id, name, password, flavor, image, + volumes_list = parse_volumes(volumes) + server = servers.create(user_id, name, password, flavor, image_id, networks=connection_list, + volumes=volumes_list, use_backend=backend) pprint.pprint_server(server, stdout=self.stdout) @@ -125,6 +120,46 @@ class Command(BaseCommand): common.wait_server_task(server, wait, self.stdout) +def parse_volumes(vol_list): + volumes = [] + for vol in vol_list: + vol_dict = {} + kv_list = vol.split(",") + for kv_item in kv_list: + try: + k, v = kv_item.split("=") + except (TypeError, ValueError): + raise CommandError("Invalid syntax for volume: %s" % vol) + vol_dict[k] = v + size = vol_dict.get("size") + if size is not None: + try: + size = int(size) + except (TypeError, ValueError): + raise CommandError("Invalid size: %s" % size) + if len(vol_dict) == 1: + if size is not None: + volumes.append({"size": size, "source_type": "blank", + "source_uuid": None}) + continue + vol_id = vol_dict.get("id") + if vol_id is not None: + volumes.append({"source_uuid": vol_id, + "source_type": "volume"}) + continue + raise CommandError("Invalid syntax for volume %s" % vol) + image = vol_dict.get("image") + snapshot = vol_dict.get("snapshot") + if image and snapshot or not(image or snapshot) or size is None: + raise CommandError("Invalid syntax for volume %s" % vol) + source = image if image else snapshot + source_type = "image" if image else "snapshot" + volumes.append({"source_uuid": source, + "source_type": source_type, + "size": size}) + return volumes + + def parse_connections(con_list): connections = [] if con_list: @@ -147,7 +182,8 @@ def parse_connections(con_list): val = {"port": port_id} elif con_kind == "floatingip": fip_id = opt.split(":")[1] - fip = common.get_floating_ip_by_id(fip_id, for_update=True) + fip = common.get_resource("floating-ip", fip_id, + for_update=True) val = {"uuid": fip.network_id, "fixed_ip": fip.address} else: raise CommandError("Unknown argument for option --port") diff --git a/snf-cyclades-app/synnefo/logic/management/commands/server-import.py b/snf-cyclades-app/synnefo/logic/management/commands/server-import.py index beea38b51f4ddf6297d6d861f65d2dc8876fb0f8..b8e1b67ce3edadf3659d41f1bd35d01318392459 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/server-import.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/server-import.py @@ -1,50 +1,31 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import CommandError from synnefo.management import common -from synnefo.db.models import VirtualMachine, Network, Flavor +from synnefo.db.models import VirtualMachine, Network, Flavor, VolumeType from synnefo.logic.utils import id_from_network_name, id_from_instance_name from synnefo.logic.backend import wait_for_job, connect_to_network +from snf_django.management.commands import SynnefoCommand from synnefo.logic.rapi import GanetiApiError from synnefo.logic import servers from synnefo import quotas -import sys - HELP_MSG = """ @@ -60,12 +41,12 @@ connected to a public network of Synnefo. """ -class Command(BaseCommand): +class Command(SynnefoCommand): help = "Import an existing Ganeti VM into Synnefo." + HELP_MSG args = "<ganeti_instance_name>" output_transaction = True - option_list = BaseCommand.option_list + ( + option_list = SynnefoCommand.option_list + ( make_option( "--backend-id", dest="backend_id", @@ -73,18 +54,18 @@ class Command(BaseCommand): " hosts the VM. Use snf-manage backend-list to" " find out available backends."), make_option( - "--user-id", + "--user", dest="user_id", help="Unique identifier of the owner of the server"), make_option( - "--image-id", + "--image", dest="image_id", default=None, help="Unique identifier of the image." " Use snf-manage image-list to find out" " available images."), make_option( - "--flavor-id", + "--flavor", dest="flavor_id", help="Unique identifier of the flavor" " Use snf-manage flavor-list to find out" @@ -99,7 +80,7 @@ class Command(BaseCommand): " Synnefo.") ) - REQUIRED = ("user-id", "backend-id", "image-id", "flavor-id") + REQUIRED = ("user", "backend-id", "image", "flavor") def handle(self, *args, **options): if len(args) < 1: @@ -124,13 +105,13 @@ class Command(BaseCommand): raise CommandError(field + " is mandatory") import_server(instance_name, backend_id, flavor_id, image_id, user_id, - new_public_nic, self.stdout) + new_public_nic, self.stderr) def import_server(instance_name, backend_id, flavor_id, image_id, user_id, - new_public_nic, stream=sys.stdout): - flavor = common.get_flavor(flavor_id) - backend = common.get_backend(backend_id) + new_public_nic, stream): + flavor = common.get_resource("flavor", flavor_id) + backend = common.get_resource("backend", backend_id) backend_client = backend.get_client() @@ -141,10 +122,10 @@ def import_server(instance_name, backend_id, flavor_id, image_id, user_id, raise CommandError("Instance %s does not exist in backend %s" % (instance_name, backend)) else: - raise CommandError("Unexpected error" + str(e)) + raise CommandError("Unexpected error: %s" % e) if not new_public_nic: - check_instance_nics(instance) + check_instance_nics(instance, stream) shutdown_instance(instance, backend_client, stream=stream) @@ -180,7 +161,7 @@ def import_server(instance_name, backend_id, flavor_id, image_id, user_id, return -def flavor_from_instance(instance, flavor, stream=sys.stdout): +def flavor_from_instance(instance, flavor, stream): beparams = instance['beparams'] disk_sizes = instance['disk.sizes'] if len(disk_sizes) != 1: @@ -191,14 +172,20 @@ def flavor_from_instance(instance, flavor, stream=sys.stdout): cpu = beparams['vcpus'] ram = beparams['memory'] - return Flavor.objects.get_or_create(disk=disk, disk_template=disk_template, + try: + volume_type = VolumeType.objects.get(disk_template=disk_template) + except VolumeType.DoesNotExist: + raise CommandError("Cannot find volume type with '%s' disk template." + % disk_template) + return Flavor.objects.get_or_create(disk=disk, + volume_type=volume_type, cpu=cpu, ram=ram) -def check_instance_nics(instance): +def check_instance_nics(instance, stream): instance_name = instance['name'] networks = instance['nic.networks.names'] - print networks + stream.write(str(networks) + "\n") try: networks = map(id_from_network_name, networks) except Network.InvalidBackendIdError: @@ -209,7 +196,7 @@ def check_instance_nics(instance): " a public network of synnefo." % instance_name) -def remove_instance_nics(instance, backend_client, stream=sys.stdout): +def remove_instance_nics(instance, backend_client, stream): instance_name = instance['name'] ips = instance['nic.ips'] nic_indexes = xrange(0, len(ips)) @@ -222,7 +209,7 @@ def remove_instance_nics(instance, backend_client, stream=sys.stdout): raise CommandError("Cannot remove instance NICs: %s" % error) -def add_public_nic(instance_name, nic, backend_client, stream=sys.stdout): +def add_public_nic(instance_name, nic, backend_client, stream): stream.write("Adding public NIC %s\n" % nic) jobid = backend_client.ModifyInstance(instance_name, nics=[('add', nic)]) (status, error) = wait_for_job(backend_client, jobid) @@ -230,7 +217,7 @@ def add_public_nic(instance_name, nic, backend_client, stream=sys.stdout): raise CommandError("Cannot rename instance: %s" % error) -def shutdown_instance(instance, backend_client, stream=sys.stdout): +def shutdown_instance(instance, backend_client, stream): instance_name = instance['name'] if instance['status'] != 'ADMIN_down': stream.write("Instance is not down. Shutting down instance...\n") @@ -240,7 +227,7 @@ def shutdown_instance(instance, backend_client, stream=sys.stdout): raise CommandError("Cannot shutdown instance: %s" % error) -def rename_instance(old_name, new_name, backend_client, stream=sys.stdout): +def rename_instance(old_name, new_name, backend_client, stream): stream.write("Renaming instance to %s\n" % new_name) jobid = backend_client.RenameInstance(old_name, new_name, @@ -250,7 +237,7 @@ def rename_instance(old_name, new_name, backend_client, stream=sys.stdout): raise CommandError("Cannot rename instance: %s" % error) -def startup_instance(name, backend_client, stream=sys.stdout): +def startup_instance(name, backend_client, stream): stream.write("Starting instance %s\n" % name) jobid = backend_client.StartupInstance(name) (status, error) = wait_for_job(backend_client, jobid) diff --git a/snf-cyclades-app/synnefo/logic/management/commands/server-inspect.py b/snf-cyclades-app/synnefo/logic/management/commands/server-inspect.py index c581728242d617807311cb8c6c4f2189144e5212..c2bb24fc10d8cffb344d315104273d5b45c38601 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/server-inspect.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/server-inspect.py @@ -1,48 +1,31 @@ -# Copyright 2012-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import CommandError +from snf_django.management.commands import SynnefoCommand from synnefo.management import common from synnefo.management import pprint -class Command(BaseCommand): +class Command(SynnefoCommand): help = "Inspect a server on DB and Ganeti" - args = "<server ID>" + args = "<server_id>" - option_list = BaseCommand.option_list + ( + option_list = SynnefoCommand.option_list + ( make_option( '--jobs', action='store_true', @@ -50,25 +33,27 @@ class Command(BaseCommand): default=False, help="Show non-archived jobs concerning server."), make_option( - '--displayname', + '--display-mails', action='store_true', - dest='displayname', + dest='displaymails', default=False, - help="Display both uuid and display name"), + help="Display both uuid and email"), ) def handle(self, *args, **options): if len(args) != 1: raise CommandError("Please provide a server ID") - vm = common.get_vm(args[0], for_update=True) + vm = common.get_resource("server", args[0], for_update=True) - displayname = options['displayname'] + display_mails = options['displaymails'] - pprint.pprint_server(vm, display_mails=displayname, stdout=self.stdout) + pprint.pprint_server(vm, display_mails=display_mails, stdout=self.stdout) self.stdout.write("\n") pprint.pprint_server_nics(vm, stdout=self.stdout) self.stdout.write("\n") + pprint.pprint_server_volumes(vm, stdout=self.stdout) + self.stdout.write("\n") pprint.pprint_server_in_ganeti(vm, print_jobs=options["jobs"], stdout=self.stdout) self.stdout.write("\n") diff --git a/snf-cyclades-app/synnefo/logic/management/commands/server-list.py b/snf-cyclades-app/synnefo/logic/management/commands/server-list.py index 886b629967227d4bb648dae48586b18bb57d3597..6b8e8730fa0852f0f620580cdbeaafd267c9f7e4 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/server-list.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/server-list.py @@ -1,42 +1,24 @@ -# Copyright 2012-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option from functools import partial from snf_django.management.commands import ListCommand from synnefo.db.models import VirtualMachine -from synnefo.management.common import get_backend +from synnefo.management.common import get_resource from synnefo.api.util import get_image from synnefo.settings import (CYCLADES_SERVICE_TOKEN as ASTAKOS_TOKEN, ASTAKOS_AUTH_URL) @@ -77,6 +59,7 @@ class Command(ListCommand): user_uuid_field = "userid" astakos_auth_url = ASTAKOS_AUTH_URL astakos_token = ASTAKOS_TOKEN + select_related = ["flavor.volume_type"] def get_ips(version, vm): ips = [] @@ -109,6 +92,7 @@ class Command(ListCommand): "deleted": ("deleted", "Whether the server is deleted or not"), "suspended": ("suspended", "Whether the server is administratively" " suspended"), + "project": ("project", "The project UUID"), } fields = ["id", "name", "user.uuid", "state", "flavor", "image.id", @@ -119,7 +103,7 @@ class Command(ListCommand): self.filters["suspended"] = True if options["backend_id"]: - backend = get_backend(options["backend_id"]) + backend = get_resource("backend", options["backend_id"]) self.filters["backend"] = backend.id if options["build"]: diff --git a/snf-cyclades-app/synnefo/logic/management/commands/server-modify.py b/snf-cyclades-app/synnefo/logic/management/commands/server-modify.py index b7fe48169c6adb61e1d12f7b46cfaf1a90381fb7..deb87cc1f79fdaf8e8c1c8d1154fb55fa32036b8 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/server-modify.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/server-modify.py @@ -1,42 +1,26 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option -from django.db import transaction -from django.core.management.base import BaseCommand, CommandError -from synnefo.management.common import (get_vm, get_flavor, convert_api_faults, +from synnefo.db import transaction +from django.core.management.base import CommandError + +from synnefo.management.common import (get_resource, convert_api_faults, wait_server_task) +from snf_django.management.commands import SynnefoCommand from snf_django.management.utils import parse_bool from synnefo.logic import servers @@ -44,19 +28,19 @@ from synnefo.logic import servers ACTIONS = ["start", "stop", "reboot_hard", "reboot_soft"] -class Command(BaseCommand): - args = "<server ID>" +class Command(SynnefoCommand): + args = "<server_id>" help = "Modify a server." - option_list = BaseCommand.option_list + ( + option_list = SynnefoCommand.option_list + ( make_option( '--name', dest='name', metavar='NAME', help="Rename server."), make_option( - '--owner', - dest='owner', + '--user', + dest='user', metavar='USER_UUID', help="Change ownership of server. Value must be a user UUID"), make_option( @@ -84,7 +68,7 @@ class Command(BaseCommand): default="True", choices=["True", "False"], metavar="True|False", - help="Wait for Ganeti jobs to complete."), + help="Wait for Ganeti jobs to complete. [Default: True]"), ) @transaction.commit_on_success @@ -93,7 +77,7 @@ class Command(BaseCommand): if len(args) != 1: raise CommandError("Please provide a server ID") - server = get_vm(args[0], for_update=True) + server = get_resource("server", args[0], for_update=True) new_name = options.get("name", None) if new_name is not None: @@ -110,10 +94,10 @@ class Command(BaseCommand): self.stdout.write("Set server '%s' as suspended=%s\n" % (server, suspended)) - new_owner = options.get('owner') + new_owner = options.get('user') if new_owner is not None: if "@" in new_owner: - raise CommandError("Invalid owner UUID.") + raise CommandError("Invalid user UUID.") old_owner = server.userid server.userid = new_owner server.save() @@ -123,7 +107,7 @@ class Command(BaseCommand): wait = parse_bool(options["wait"]) new_flavor_id = options.get("flavor") if new_flavor_id is not None: - new_flavor = get_flavor(new_flavor_id) + new_flavor = get_resource("flavor", new_flavor_id) old_flavor = server.flavor msg = "Resizing server '%s' from flavor '%s' to '%s'.\n" self.stdout.write(msg % (server, old_flavor, new_flavor)) diff --git a/snf-cyclades-app/synnefo/logic/management/commands/server-remove.py b/snf-cyclades-app/synnefo/logic/management/commands/server-remove.py index 86a6b9d6660536f13183dcc99896daf2c1d16780..a1d8ba03cfc32d8991a2f1f0caef4b28f9c26994 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/server-remove.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/server-remove.py @@ -1,40 +1,22 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option from django.core.management.base import CommandError -from synnefo.management.common import (get_vm, convert_api_faults, +from synnefo.management.common import (get_resource, convert_api_faults, wait_server_task) from synnefo.logic import servers from snf_django.management.commands import RemoveCommand @@ -43,17 +25,17 @@ from snf_django.lib.api import faults class Command(RemoveCommand): - args = "<Server ID> [<Server ID> ...]" + args = "<server_id> [<server_id> ...]" help = "Remove a server by deleting the instance from the Ganeti backend." - option_list = RemoveCommand.option_list + ( + command_option_list = RemoveCommand.command_option_list + ( make_option( '--wait', dest='wait', default="True", choices=["True", "False"], metavar="True|False", - help="Wait for Ganeti job to complete."), + help="Wait for Ganeti job to complete. [Default: True]"), ) @convert_api_faults @@ -68,7 +50,7 @@ class Command(RemoveCommand): for server_id in args: self.stdout.write("\n") try: - server = get_vm(server_id) + server = get_resource("server", server_id, for_update=True) self.stdout.write("Trying to remove server '%s' from backend " "'%s' \n" % (server.backend_vm_id, diff --git a/snf-cyclades-app/synnefo/logic/management/commands/server-show.py b/snf-cyclades-app/synnefo/logic/management/commands/server-show.py index ef3c16600eb194caf553d74ebfe9f3e39103ce41..390b71dd402384c98fac16e31d13324a3fbeff4f 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/server-show.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/server-show.py @@ -1,39 +1,21 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.core.management.base import CommandError from snf_django.management.commands import SynnefoCommand -from synnefo.management.common import (format_vm_state, get_vm, +from synnefo.management.common import (format_vm_state, get_resource, get_image) from snf_django.lib.astakos import UserCache from synnefo.settings import (CYCLADES_SERVICE_TOKEN as ASTAKOS_TOKEN, @@ -42,14 +24,14 @@ from snf_django.management import utils class Command(SynnefoCommand): - args = "<server ID>" + args = "<server_id>" help = "Show server info" def handle(self, *args, **options): if len(args) != 1: raise CommandError("Please provide a server ID") - server = get_vm(args[0]) + server = get_resource("server", args[0]) flavor = '%s (%s)' % (server.flavor.id, server.flavor.name) userid = server.userid @@ -67,6 +49,7 @@ class Command(SynnefoCommand): 'name': server.name, 'owner_uuid': userid, 'owner_name': usercache.get_name(userid), + 'project': server.project, 'created': utils.format_date(server.created), 'updated': utils.format_date(server.updated), 'image': image, @@ -79,5 +62,5 @@ class Command(SynnefoCommand): 'task_job_id': server.task_job_id, } - utils.pprint_table(self.stdout, [kv.values()], kv.keys(), - options["output_format"], vertical=True) + self.pprint_table([kv.values()], kv.keys(), options["output_format"], + vertical=True) diff --git a/snf-cyclades-app/synnefo/logic/management/commands/service-export-cyclades.py b/snf-cyclades-app/synnefo/logic/management/commands/service-export-cyclades.py index 146f1891c25311b7f98e35a0f11cc4b72e574b94..6d5acf6519fbac3cad60b15347570ecbad8ea774 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/service-export-cyclades.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/service-export-cyclades.py @@ -1,43 +1,26 @@ -# Copyright 2012-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.utils import simplejson as json -from django.core.management.base import NoArgsCommand + +from snf_django.management.commands import SynnefoCommand from synnefo.cyclades_settings import cyclades_services from synnefo.lib.services import filter_public -class Command(NoArgsCommand): +class Command(SynnefoCommand): help = "Export Cyclades services in JSON format." def handle(self, *args, **options): diff --git a/snf-cyclades-app/synnefo/logic/management/commands/stats-cyclades.py b/snf-cyclades-app/synnefo/logic/management/commands/stats-cyclades.py index 0e458a46de4bcfcaa078efe25274db3681f50b85..31b5e04366d08e64d94459088b7889026ea6ae4a 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/stats-cyclades.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/stats-cyclades.py @@ -1,36 +1,17 @@ -# Copyright 2013-2014 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. - +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from __future__ import division import sys @@ -43,7 +24,7 @@ from collections import defaultdict from snf_django.management.utils import pprint_table, parse_bool from snf_django.management.commands import SynnefoCommand, CommandError -from synnefo.management.common import get_backend +from synnefo.management.common import get_resource from synnefo.admin import stats as statistics from synnefo.util import units @@ -52,7 +33,7 @@ class Command(SynnefoCommand): help = "Get available statistics about the Cyclades service" can_import_settings = True - option_list = SynnefoCommand.option_list + ( + command_option_list = ( make_option("--backend", dest="backend", help="Include statistics only for this backend"), @@ -101,7 +82,7 @@ class Command(SynnefoCommand): def handle(self, *args, **options): if options["backend"] is not None: - backend = get_backend(options["backend"]) + backend = get_resource("backend", options["backend"]) else: backend = None @@ -239,7 +220,7 @@ def pprint_clusters(clusters, stdout, detail=True): "%s/%s %s%%" % (units.show(c_dused, "bytes"), units.show(c_dtotal, "bytes"), ("%.2f%%" % (100 * c_dused/c_dtotal) - if c_dtotal != 0 else "-"))), + if c_dtotal != 0 else "-"))), ("V/P used disk", ("%.2f%%" % (100 * virtual_disk / c_dused) if c_dused != 0 else "-")), ("V/P total disk", ("%.2f%%" % (100 * virtual_disk / c_dtotal) diff --git a/snf-cyclades-app/synnefo/logic/management/commands/subnet-create.py b/snf-cyclades-app/synnefo/logic/management/commands/subnet-create.py index ce437979daefa889e164c8c9b64f15fde9174446..fbf72ac0334269eed51162f6b923622b64ece7c5 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/subnet-create.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/subnet-create.py @@ -1,46 +1,28 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import CommandError + from synnefo.management import common +from snf_django.management.commands import SynnefoCommand from snf_django.management.utils import parse_bool from synnefo.management import pprint from synnefo.logic import subnets -import ipaddr - HELP_MSG = """ Create a new subnet without authenticating the user. The limit of one @@ -49,11 +31,11 @@ Network ID. """ -class Command(BaseCommand): +class Command(SynnefoCommand): help = "Create a new Subnet." + HELP_MSG - option_list = BaseCommand.option_list + ( - make_option("--network-id", dest="network_id", + option_list = SynnefoCommand.option_list + ( + make_option("--network", dest="network_id", help="Specify the Network to attach the subnet. To get the" " networks of a user, use snf-manage network-list"), make_option("--cidr", dest="cidr", @@ -99,11 +81,11 @@ class Command(BaseCommand): cidr = options["cidr"] if not network_id: - raise CommandError("network-id is mandatory") + raise CommandError("network is mandatory") if not cidr: raise CommandError("cidr is mandatory") - user_id = common.get_network(network_id).userid + user_id = common.get_resource("network", network_id).userid name = options["name"] or "" allocation_pools = options["allocation_pools"] ipversion = options["ipversion"] or 4 diff --git a/snf-cyclades-app/synnefo/logic/management/commands/subnet-inspect.py b/snf-cyclades-app/synnefo/logic/management/commands/subnet-inspect.py index 922fa7a00c5f66387117b994033d39063c623fa6..fa2530981c794e516f51e0df457e0c8458d24655 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/subnet-inspect.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/subnet-inspect.py @@ -1,52 +1,36 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +#from optparse import make_option -from optparse import make_option +from django.core.management.base import CommandError -from django.core.management.base import BaseCommand, CommandError +from snf_django.management.commands import SynnefoCommand from synnefo.management import pprint, common -class Command(BaseCommand): +class Command(SynnefoCommand): help = "Inspect a subnet on DB and Ganeti." - args = "<Subnet ID>" - option_list = BaseCommand.option_list + args = "<subnet_id>" + option_list = SynnefoCommand.option_list def handle(self, *args, **options): if len(args) != 1: raise CommandError("Please provide a subnet ID.") - subnet = common.get_subnet(args[0]) + subnet = common.get_resource("subnet", args[0]) pprint.pprint_subnet_in_db(subnet, stdout=self.stdout) self.stdout.write("\n\n") diff --git a/snf-cyclades-app/synnefo/logic/management/commands/subnet-list.py b/snf-cyclades-app/synnefo/logic/management/commands/subnet-list.py index 1d1cac7b20a9153ccd7dbf8d4264e46a43519cf5..645e63e921a74d6487eed3e4d7399f133b9d7817 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/subnet-list.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/subnet-list.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS"" AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option diff --git a/snf-cyclades-app/synnefo/logic/management/commands/subnet-modify.py b/snf-cyclades-app/synnefo/logic/management/commands/subnet-modify.py index 81412f9b62c0a7dc009b15c47961b765692622ed..dea5ba0354c3398805a8af437d6a76269ecb7ad6 100644 --- a/snf-cyclades-app/synnefo/logic/management/commands/subnet-modify.py +++ b/snf-cyclades-app/synnefo/logic/management/commands/subnet-modify.py @@ -1,39 +1,23 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from optparse import make_option -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import CommandError + +from snf_django.management.commands import SynnefoCommand from synnefo.management import common from synnefo.logic import subnets @@ -45,10 +29,10 @@ be updated. """ -class Command(BaseCommand): +class Command(SynnefoCommand): help = "Update a Subnet." + HELP_MSG - args = "<Subnet ID>" - option_list = BaseCommand.option_list + ( + args = "<subnet_id>" + option_list = SynnefoCommand.option_list + ( make_option("--name", dest="name", help="The new subnet name."), ) @@ -66,8 +50,8 @@ class Command(BaseCommand): if not name: raise CommandError("--name is mandatory") - subnet = common.get_subnet(subnet_id) - user_id = common.get_network(subnet.network.id).userid + subnet = common.get_resource("subnet", subnet_id, for_update=True) + user_id = common.get_resource("network", subnet.network.id).userid subnets.update_subnet(sub_id=subnet_id, name=name, diff --git a/snf-cyclades-app/synnefo/logic/networks.py b/snf-cyclades-app/synnefo/logic/networks.py index fc74457f1ddb20896d7698eba01ced8113326c8e..333d5b094a066142a368f57fa0ff211cfbe72628 100644 --- a/snf-cyclades-app/synnefo/logic/networks.py +++ b/snf-cyclades-app/synnefo/logic/networks.py @@ -1,38 +1,20 @@ -# Copyright 2011-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from functools import wraps -from django.db import transaction +from synnefo.db import transaction from django.conf import settings from snf_django.lib.api import faults @@ -66,7 +48,8 @@ def network_command(action): @transaction.commit_on_success def create(userid, name, flavor, link=None, mac_prefix=None, mode=None, - floating_ip_pool=False, tags=None, public=False, drained=False): + floating_ip_pool=False, tags=None, public=False, drained=False, + project=None): if flavor is None: raise faults.BadRequest("Missing request parameter 'type'") elif flavor not in Network.FLAVORS.keys(): @@ -101,9 +84,13 @@ def create(userid, name, flavor, link=None, mac_prefix=None, mode=None, msg = "Link '%s' is already used." % link raise faults.BadRequest(msg) + if project is None: + project = userid + network = Network.objects.create( name=name, userid=userid, + project=project, flavor=flavor, mode=mode, link=link, @@ -171,3 +158,15 @@ def delete(network): # If network does not exist in any backend, update the network state backend_mod.update_network_state(network) return network + + +@network_command("REASSIGN") +def reassign(network, project): + action_fields = {"to_project": project, "from_project": network.project} + log.info("Reassigning network %s from project %s to %s", + network, network.project, project) + network.project = project + network.save() + quotas.issue_and_accept_commission(network, action="REASSIGN", + action_fields=action_fields) + return network diff --git a/snf-cyclades-app/synnefo/logic/queues.py b/snf-cyclades-app/synnefo/logic/queues.py index b02d50e56a0d81f78b9726c9bc1d7327c76a6f36..fbbc9a957ffd715431e65c1b83ed7bdd24c44bc1 100644 --- a/snf-cyclades-app/synnefo/logic/queues.py +++ b/snf-cyclades-app/synnefo/logic/queues.py @@ -1,31 +1,17 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from synnefo.settings import BACKEND_PREFIX_ID, DEBUG, EXCHANGE_GANETI @@ -86,3 +72,14 @@ def convert_queue_to_dead(queue): def convert_exchange_to_dead(exchange): """Convert the name of an exchange to the corresponding dead-letter one""" return exchange + "-dl" + + +EVENTD_HEARTBEAT_ROUTING_KEY = "eventd.heartbeat" + + +def get_dispatcher_request_queue(hostname, pid): + return "snf:dispatcher:%s:%s" % (hostname, pid) + + +def get_dispatcher_heartbeat_queue(hostname, pid): + return "snf:dispatcher:heartbeat:%s:%s" % (hostname, pid) diff --git a/snf-cyclades-app/synnefo/logic/rapi.py b/snf-cyclades-app/synnefo/logic/rapi.py index ba83f508feaabc103c418e135b4d96b4c1ee7349..64468e751c945c15ac513d5a113330aa8a5a142d 100644 --- a/snf-cyclades-app/synnefo/logic/rapi.py +++ b/snf-cyclades-app/synnefo/logic/rapi.py @@ -546,6 +546,29 @@ class GanetiRapiClient(object): # pylint: disable=R0904 ("/%s/instances/%s/deactivate-disks" % (GANETI_RAPI_VERSION, instance)), None, None) + def SnapshotInstance(self, instance, disks, dry_run=False, reason=None): + """Takes snapshot of instance's disks. + + More details for parameters can be found in the RAPI documentation. + + @type instance: string + @param instance: Instance name + @type disks: list of tuples + @param disks: The disks to snapshot + @rtype: string + @return: job id + + """ + body = {"disks": disks} + + query = [] + _AppendIf(query, reason, ("reason", reason)) + _AppendDryRunIf(query, dry_run) + + return self._SendRequest(HTTP_PUT, + ("/%s/instances/%s/snapshot" % + (GANETI_RAPI_VERSION, instance)), query, body) + def RecreateInstanceDisks(self, instance, disks=None, nodes=None): """Recreate an instance's disks. @@ -1462,7 +1485,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904 return self._SendRequest(HTTP_POST, "/%s/networks" % GANETI_RAPI_VERSION, query, body) - def ConnectNetwork(self, network_name, group_name, mode, link, + def ConnectNetwork(self, network_name, group_name, mode, link, vlan="", conflicts_check=False, depends=None, dry_run=False): """Connects a Network to a NodeGroup with the given netparams @@ -1471,6 +1494,10 @@ class GanetiRapiClient(object): # pylint: disable=R0904 "group_name": group_name, "network_mode": mode, "network_link": link, + # This will be needed only if synnefo supports networks based on ovs. + # Note that adding this here will break + # compatibility with Ganeti versions older than 2.10 + # "network_vlan": vlan, "conflicts_check": conflicts_check, } diff --git a/snf-cyclades-app/synnefo/logic/rapi_pool.py b/snf-cyclades-app/synnefo/logic/rapi_pool.py index 4fe90b75fbd0649cb45bd01f8cd4418e73d90ef7..2d407f0c4e82bbb4b314bb07c19ce064173e1b7a 100644 --- a/snf-cyclades-app/synnefo/logic/rapi_pool.py +++ b/snf-cyclades-app/synnefo/logic/rapi_pool.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from objpool import ObjectPool from synnefo.logic.rapi import GanetiRapiClient diff --git a/snf-cyclades-app/synnefo/logic/reconciliation.py b/snf-cyclades-app/synnefo/logic/reconciliation.py index 597bef7480f3da1c4fb6f9f2b4a58ae2268fa5e6..48d2599cfef7f3acb6a42402b5b6c0014840de43 100644 --- a/snf-cyclades-app/synnefo/logic/reconciliation.py +++ b/snf-cyclades-app/synnefo/logic/reconciliation.py @@ -1,37 +1,19 @@ # -*- coding: utf-8 -*- # -# Copyright 2011-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # """Business logic for reconciliation @@ -61,9 +43,10 @@ from django.conf import settings import logging import itertools import bitarray +import simplejson as json from datetime import datetime, timedelta -from django.db import transaction +from synnefo.db import transaction from synnefo.db.models import (Backend, VirtualMachine, Flavor, pooled_rapi_client, Network, BackendNetwork, BridgePoolTable, @@ -71,6 +54,8 @@ from synnefo.db.models import (Backend, VirtualMachine, Flavor, from synnefo.db import pools from synnefo.logic import utils, rapi, backend as backend_mod from synnefo.lib.utils import merge_time +from synnefo.plankton.backend import (PlanktonBackend, OBJECT_UNAVAILABLE, + OBJECT_ERROR) logger = logging.getLogger() logging.basicConfig() @@ -113,6 +98,7 @@ class BackendReconciler(object): self.stale_servers = self.reconcile_stale_servers() self.orphan_servers = self.reconcile_orphan_servers() self.unsynced_servers = self.reconcile_unsynced_servers() + self.unsynced_snapshots = self.reconcile_unsynced_snapshots() self.close() def get_build_status(self, db_server): @@ -193,7 +179,6 @@ class BackendReconciler(object): self.log.debug("Issued OP_INSTANCE_REMOVE for orphan servers.") def reconcile_unsynced_servers(self): - #log = self.log for server_id in self.db_servers_keys & self.gnt_servers_keys: db_server = self.db_servers[server_id] gnt_server = self.gnt_servers[server_id] @@ -269,7 +254,7 @@ class BackendReconciler(object): ram=gnt_flavor["ram"], cpu=gnt_flavor["vcpus"], disk=db_flavor.disk, - disk_template=db_flavor.disk_template) + volume_type_id=db_flavor.volume_type_id) except Flavor.DoesNotExist: self.log.warning("Server '%s' has unknown flavor.", server_id) return @@ -302,7 +287,12 @@ class BackendReconciler(object): created__lte=building_time) \ .order_by("id") gnt_nics = gnt_server["nics"] - gnt_nics_parsed = backend_mod.process_ganeti_nics(gnt_nics) + try: + gnt_nics_parsed = backend_mod.parse_instance_nics(gnt_nics) + except Network.InvalidBackendIdError as e: + self.log.warning("Server %s is connected to unknown network %s" + " Cannot reconcile server." % (server_id, str(e))) + return nics_changed = len(db_nics) != len(gnt_nics) for db_nic, gnt_nic in zip(db_nics, sorted(gnt_nics_parsed.items())): gnt_nic_id, gnt_nic = gnt_nic @@ -321,12 +311,44 @@ class BackendReconciler(object): self.log.info(msg, server_id, db_nics_str, gnt_nics_str) if self.options["fix_unsynced_nics"]: vm = get_locked_server(server_id) - backend_mod.process_net_status(vm=vm, - etime=self.event_time, - nics=gnt_nics) + backend_mod.process_op_status( + vm=vm, etime=self.event_time, jobid=-0, + opcode="OP_INSTANCE_SET_PARAMS", status='success', + logmsg="Reconciliation: simulated Ganeti event", + nics=gnt_nics) def reconcile_unsynced_disks(self, server_id, db_server, gnt_server): - pass + building_time = self.event_time - BUILDING_NIC_TIMEOUT + db_disks = db_server.volumes.exclude(status="CREATING", + created__lte=building_time) \ + .filter(deleted=False)\ + .order_by("id") + gnt_disks = gnt_server["disks"] + gnt_disks_parsed = backend_mod.parse_instance_disks(gnt_disks) + disks_changed = len(db_disks) != len(gnt_disks) + for db_disk, gnt_disk in zip(db_disks, + sorted(gnt_disks_parsed.items())): + gnt_disk_id, gnt_disk = gnt_disk + if (db_disk.id == gnt_disk_id) and\ + backend_mod.disks_are_equal(db_disk, gnt_disk): + continue + else: + disks_changed = True + break + if disks_changed: + msg = "Found unsynced disks for server '%s'.\n"\ + "\tDB:\n\t\t%s\n\tGaneti:\n\t\t%s" + db_disks_str = "\n\t\t".join(map(format_db_disk, db_disks)) + gnt_disks_str = "\n\t\t".join(map(format_gnt_disk, + sorted(gnt_disks_parsed.items()))) + self.log.info(msg, server_id, db_disks_str, gnt_disks_str) + if self.options["fix_unsynced_disks"]: + vm = get_locked_server(server_id) + backend_mod.process_op_status( + vm=vm, etime=self.event_time, jobid=-0, + opcode="OP_INSTANCE_SET_PARAMS", status='success', + logmsg="Reconciliation: simulated Ganeti event", + disks=gnt_disks) def reconcile_pending_task(self, server_id, db_server): job_id = db_server.task_job_id @@ -351,6 +373,48 @@ class BackendReconciler(object): db_server.save() self.log.info("Cleared pending task for server '%s", server_id) + def reconcile_unsynced_snapshots(self): + # Find the biggest ID of the retrieved Ganeti jobs. Reconciliation + # will be performed for IDs that are smaller from this. + max_job_id = max(self.gnt_jobs.keys()) if self.gnt_jobs.keys() else 0 + + with PlanktonBackend(None) as b: + snapshots = b.list_snapshots(check_permissions=False) + unavail_snapshots = [s for s in snapshots + if s["status"] == OBJECT_UNAVAILABLE] + + for snapshot in unavail_snapshots: + uuid = snapshot["id"] + backend_info = snapshot["backend_info"] + if backend_info is None: + self.log.warning("Cannot perform reconciliation for" + " snapshot '%s'. Not enough information.", + uuid) + continue + job_info = json.loads(backend_info) + backend_id = job_info["ganeti_backend_id"] + job_id = job_info["ganeti_job_id"] + + if backend_id == self.backend.id and job_id <= max_job_id: + if job_id in self.gnt_jobs: + job_status = self.gnt_jobs[job_id]["status"] + state = \ + backend_mod.snapshot_state_from_job_status(job_status) + if state == OBJECT_UNAVAILABLE: + continue + else: + # Snapshot in unavailable but no job exists + state = OBJECT_ERROR + + self.log.info("Snapshot '%s' is '%s' in Pithos DB but should" + " be '%s'", uuid, snapshot["status"], state) + if self.options["fix_unsynced_snapshots"]: + backend_mod.update_snapshot(uuid, snapshot["owner"], + job_id=-1, + job_status=job_status, + etime=self.event_time) + self.log.info("Fixed state of snapshot '%s'.", uuid) + NIC_MSG = ": %s\t".join(["ID", "State", "IP", "Network", "MAC", "Index", "Firewall"]) + ": %s" @@ -367,6 +431,17 @@ def format_gnt_nic(nic): nic["network"].id, nic["mac"], nic["index"], nic["firewall_profile"]) +DISK_MSG = ": %s\t".join(["ID", "State", "Size", "Index"]) + ": %s" + + +def format_db_disk(disk): + return DISK_MSG % (disk.id, disk.status, disk.size, disk.index) + + +def format_gnt_disk(disk): + disk_name, disk = disk + return DISK_MSG % (disk_name, disk["status"], disk["size"], disk["index"]) + # # Networks @@ -392,8 +467,10 @@ def hanging_networks(backend, GNets): """ def get_network_groups(group_list): groups = set() - for (name, mode, link) in group_list: - groups.add(name) + # Since ganeti 2.10 networks are connected to nodegroups + # with mode and link AND vlan (ovs extra nicparam) + for group_info in group_list: + groups.add(group_info[0]) return groups with pooled_rapi_client(backend) as c: @@ -459,12 +536,12 @@ def nics_from_instance(i): names = zip(itertools.repeat('name'), i['nic.names']) macs = zip(itertools.repeat('mac'), i['nic.macs']) networks = zip(itertools.repeat('network'), i['nic.networks.names']) + indexes = zip(itertools.repeat('index'), range(0, len(ips))) # modes = zip(itertools.repeat('mode'), i['nic.modes']) # links = zip(itertools.repeat('link'), i['nic.links']) # nics = zip(ips,macs,modes,networks,links) - nics = zip(ips, names, macs, networks) + nics = zip(ips, names, macs, networks, indexes) nics = map(lambda x: dict(x), nics) - #nics = dict(enumerate(nics)) tags = i["tags"] for tag in tags: t = tag.split(":") @@ -479,16 +556,21 @@ def nics_from_instance(i): return nics +def disks_from_instance(i): + sizes = zip(itertools.repeat('size'), i['disk.sizes']) + names = zip(itertools.repeat('name'), i['disk.names']) + uuids = zip(itertools.repeat('uuid'), i['disk.uuids']) + indexes = zip(itertools.repeat('index'), range(0, len(sizes))) + disks = zip(sizes, names, uuids, indexes) + disks = map(lambda x: dict(x), disks) + return disks + + def get_ganeti_jobs(backend): gnt_jobs = backend_mod.get_jobs(backend) return dict([(int(j["id"]), j) for j in gnt_jobs]) -def disks_from_instance(i): - return dict([(index, {"size": size}) - for index, size in enumerate(i["disk.sizes"])]) - - class NetworkReconciler(object): def __init__(self, logger, fix=False): self.log = logger diff --git a/snf-cyclades-app/synnefo/logic/server_attachments.py b/snf-cyclades-app/synnefo/logic/server_attachments.py new file mode 100644 index 0000000000000000000000000000000000000000..a3bd2a34e3254a8da0977ff98e0baf0131855c1c --- /dev/null +++ b/snf-cyclades-app/synnefo/logic/server_attachments.py @@ -0,0 +1,117 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import logging + +from snf_django.lib.api import faults +from django.conf import settings +from synnefo.logic import backend, commands + +log = logging.getLogger(__name__) + + +def attach_volume(vm, volume): + """Attach a volume to a server. + + The volume must be in 'AVAILABLE' status in order to be attached. Also, + number of the volumes that are attached to the server must remain less + than 'GANETI_MAX_DISKS_PER_INSTANCE' setting. This function will send + the corresponding job to Ganeti backend and update the status of the + volume to 'ATTACHING'. + + """ + # Check volume state + if volume.status not in ["AVAILABLE", "CREATING"]: + raise faults.BadRequest("Cannot attach volume while volume is in" + " '%s' status." % volume.status) + + # Check that disk templates are the same + if volume.volume_type_id != vm.flavor.volume_type_id: + msg = ("Volume and server must have the same volume type. Volume has" + " volume type '%s' while server has '%s'" + % (volume.volume_type_id, vm.flavor.volume_type_id)) + raise faults.BadRequest(msg) + + # Check maximum disk per instance hard limit + vm_volumes_num = vm.volumes.filter(deleted=False).count() + if vm_volumes_num == settings.GANETI_MAX_DISKS_PER_INSTANCE: + raise faults.BadRequest("Maximum volumes per server limit reached") + + if volume.status == "CREATING": + action_fields = {"disks": [("add", volume, {})]} + else: + action_fields = {} + comm = commands.server_command("ATTACH_VOLUME", + action_fields=action_fields) + return comm(_attach_volume)(vm, volume) + + +def _attach_volume(vm, volume): + """Attach a Volume to a VM and update the Volume's status.""" + jobid = backend.attach_volume(vm, volume) + log.info("Attached volume '%s' to server '%s'. JobID: '%s'", volume.id, + volume.machine_id, jobid) + volume.backendjobid = jobid + volume.machine = vm + if volume.status == "AVAILABLE": + volume.status = "ATTACHING" + else: + volume.status = "CREATING" + volume.save() + return jobid + + +def detach_volume(vm, volume): + """Detach a Volume from a VM + + The volume must be in 'IN_USE' status in order to be detached. Also, + the root volume of the instance (index=0) can not be detached. This + function will send the corresponding job to Ganeti backend and update the + status of the volume to 'DETACHING'. + + """ + + _check_attachment(vm, volume) + if volume.status not in ["IN_USE", "ERROR"]: + raise faults.BadRequest("Cannot detach volume while volume is in" + " '%s' status." % volume.status) + if volume.index == 0: + raise faults.BadRequest("Cannot detach the root volume of a server") + + action_fields = {"disks": [("remove", volume, {})]} + comm = commands.server_command("DETACH_VOLUME", + action_fields=action_fields) + return comm(_detach_volume)(vm, volume) + + +def _detach_volume(vm, volume): + """Detach a Volume from a VM and update the Volume's status""" + jobid = backend.detach_volume(vm, volume) + log.info("Detached volume '%s' from server '%s'. JobID: '%s'", volume.id, + volume.machine_id, jobid) + volume.backendjobid = jobid + if volume.delete_on_termination: + volume.status = "DELETING" + else: + volume.status = "DETACHING" + volume.save() + return jobid + + +def _check_attachment(vm, volume): + """Check that the Volume is attached to the VM""" + if volume.machine_id != vm.id: + raise faults.BadRequest("Volume '%s' is not attached to server '%s'" + % volume.id, vm.id) diff --git a/snf-cyclades-app/synnefo/logic/servers.py b/snf-cyclades-app/synnefo/logic/servers.py index 5c865a6a2ba3977259b4cce1cf23ad61fa08f3d7..42d1a78c65cf5ea1edee5907f5001bd931647280 100644 --- a/snf-cyclades-app/synnefo/logic/servers.py +++ b/snf-cyclades-app/synnefo/logic/servers.py @@ -1,50 +1,41 @@ -# Copyright 2011-2014 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import logging +from datetime import datetime from socket import getfqdn -from functools import wraps +from random import choice from django import dispatch -from django.db import transaction +from synnefo.db import transaction from django.utils import simplejson as json from snf_django.lib.api import faults from django.conf import settings -from synnefo import quotas from synnefo.api import util from synnefo.logic import backend, ips, utils from synnefo.logic.backend_allocator import BackendAllocator from synnefo.db.models import (NetworkInterface, VirtualMachine, - VirtualMachineMetadata, IPAddressLog, Network) + VirtualMachineMetadata, IPAddressLog, Network, + Image, pooled_rapi_client) from vncauthproxy.client import request_forwarding as request_vnc_forwarding from synnefo.logic import rapi +from synnefo.volume.volumes import _create_volume +from synnefo.volume.util import get_volume +from synnefo.logic import commands +from synnefo import quotas log = logging.getLogger(__name__) @@ -52,148 +43,87 @@ log = logging.getLogger(__name__) server_created = dispatch.Signal(providing_args=["created_vm_params"]) -def validate_server_action(vm, action): - if vm.deleted: - raise faults.BadRequest("Server '%s' has been deleted." % vm.id) - - # Destroyin a server should always be permitted - if action == "DESTROY": - return - - # Check that there is no pending action - pending_action = vm.task - if pending_action: - if pending_action == "BUILD": - raise faults.BuildInProgress("Server '%s' is being build." % vm.id) - raise faults.BadRequest("Cannot perform '%s' action while there is a" - " pending '%s'." % (action, pending_action)) - - # Check if action can be performed to VM's operstate - operstate = vm.operstate - if operstate == "ERROR": - raise faults.BadRequest("Cannot perform '%s' action while server is" - " in 'ERROR' state." % action) - elif operstate == "BUILD" and action != "BUILD": - raise faults.BuildInProgress("Server '%s' is being build." % vm.id) - elif (action == "START" and operstate != "STOPPED") or\ - (action == "STOP" and operstate != "STARTED") or\ - (action == "RESIZE" and operstate != "STOPPED") or\ - (action in ["CONNECT", "DISCONNECT"] and operstate != "STOPPED" - and not settings.GANETI_USE_HOTPLUG): - raise faults.BadRequest("Cannot perform '%s' action while server is" - " in '%s' state." % (action, operstate)) - return - - -def server_command(action, action_fields=None): - """Handle execution of a server action. - - Helper function to validate and execute a server action, handle quota - commission and update the 'task' of the VM in the DB. - - 1) Check if action can be performed. If it can, then there must be no - pending task (with the exception of DESTROY). - 2) Handle previous commission if unresolved: - * If it is not pending and it to accept, then accept - * If it is not pending and to reject or is pending then reject it. Since - the action can be performed only if there is no pending task, then there - can be no pending commission. The exception is DESTROY, but in this case - the commission can safely be rejected, and the dispatcher will generate - the correct ones! - 3) Issue new commission and associate it with the VM. Also clear the task. - 4) Send job to ganeti - 5) Update task and commit - """ - def decorator(func): - @wraps(func) - @transaction.commit_on_success - def wrapper(vm, *args, **kwargs): - user_id = vm.userid - validate_server_action(vm, action) - vm.action = action - - commission_name = "client: api, resource: %s" % vm - quotas.handle_resource_commission(vm, action=action, - action_fields=action_fields, - commission_name=commission_name) - vm.save() - - # XXX: Special case for server creation! - if action == "BUILD": - # Perform a commit, because the VirtualMachine must be saved to - # DB before the OP_INSTANCE_CREATE job in enqueued in Ganeti. - # Otherwise, messages will arrive from snf-dispatcher about - # this instance, before the VM is stored in DB. - transaction.commit() - # After committing the locks are released. Refetch the instance - # to guarantee x-lock. - vm = VirtualMachine.objects.select_for_update().get(id=vm.id) - - # Send the job to Ganeti and get the associated jobID - try: - job_id = func(vm, *args, **kwargs) - except Exception as e: - if vm.serial is not None: - # Since the job never reached Ganeti, reject the commission - log.debug("Rejecting commission: '%s', could not perform" - " action '%s': %s" % (vm.serial, action, e)) - transaction.rollback() - quotas.reject_resource_serial(vm) - transaction.commit() - raise - - if action == "BUILD" and vm.serial is not None: - # XXX: Special case for server creation: we must accept the - # commission because the VM has been stored in DB. Also, if - # communication with Ganeti fails, the job will never reach - # Ganeti, and the commission will never be resolved. - quotas.accept_resource_serial(vm) - - log.info("user: %s, vm: %s, action: %s, job_id: %s, serial: %s", - user_id, vm.id, action, job_id, vm.serial) - - # store the new task in the VM - if job_id is not None: - vm.task = action - vm.task_job_id = job_id - vm.save() - - return vm - return wrapper - return decorator +@transaction.commit_on_success +def create(userid, name, password, flavor, image_id, metadata={}, + personality=[], networks=None, use_backend=None, project=None, + volumes=None): + + utils.check_name_length(name, VirtualMachine.VIRTUAL_MACHINE_NAME_LENGTH, + "Server name is too long") + # Get the image, if any, that is used for the first volume + vol_image_id = None + if volumes: + vol = volumes[0] + if vol["source_type"] in ["image", "snapshot"]: + vol_image_id = vol["source_uuid"] + + # Check conflict between server's and volume's image + if image_id and vol_image_id and image_id != vol_image_id: + raise faults.BadRequest("The specified server's image is different" + " from the the source of the first volume.") + elif vol_image_id and not image_id: + image_id = vol_image_id + elif not image_id: + raise faults.BadRequest("You need to specify either an image or a" + " block device mapping.") + + if len(metadata) > settings.CYCLADES_VM_MAX_METADATA: + raise faults.BadRequest("Virtual Machines cannot have more than %s " + "metadata items" % + settings.CYCLADES_VM_MAX_METADATA) + # Get image info + image = util.get_image_dict(image_id, userid) + + if not volumes: + # If no volumes are specified, we automatically create a volume with + # the size of the flavor and filled with the specified image. + volumes = [{"source_type": "image", + "source_uuid": image_id, + "size": flavor.disk, + "delete_on_termination": True}] + assert(len(volumes) > 0), "Cannot create server without volumes" + + if volumes[0]["source_type"] == "blank": + raise faults.BadRequest("Root volume cannot be blank") + + try: + is_system = (image["owner"] == settings.SYSTEM_IMAGES_OWNER) + img, created = Image.objects.get_or_create(uuid=image["id"], + version=image["version"]) + if created: + img.owner = image["owner"] + img.name = image["name"] + img.location = image["location"] + img.mapfile = image["mapfile"] + img.is_public = image["is_public"] + img.is_snapshot = image["is_snapshot"] + img.is_system = is_system + img.os = image["metadata"].get("OS", "unknown") + img.osfamily = image["metadata"].get("OSFAMILY", "unknown") + img.save() + except Exception as e: + # Image info is not critical. Continue if it fails for any reason + log.warning("Failed to store image info: %s", e) -@transaction.commit_on_success -def create(userid, name, password, flavor, image, metadata={}, - personality=[], networks=None, use_backend=None): if use_backend is None: # Allocate server to a Ganeti backend use_backend = allocate_new_server(userid, flavor) - utils.check_name_length(name, VirtualMachine.VIRTUAL_MACHINE_NAME_LENGTH, - "Server name is too long") - # Create the ports for the server ports = create_instance_ports(userid, networks) - # Fix flavor for archipelago - disk_template, provider = util.get_flavor_provider(flavor) - if provider: - flavor.disk_template = disk_template - flavor.disk_provider = provider - flavor.disk_origin = None - if provider in settings.GANETI_CLONE_PROVIDERS: - flavor.disk_origin = image['checksum'] - image['backend_id'] = 'null' - else: - flavor.disk_provider = None + if project is None: + project = userid # We must save the VM instance now, so that it gets a valid # vm.backend_vm_id. vm = VirtualMachine.objects.create(name=name, backend=use_backend, userid=userid, + project=project, imageid=image["id"], + image_version=image["version"], flavor=flavor, operstate="BUILD") log.info("Created entry in DB for VM '%s'", vm) @@ -204,6 +134,32 @@ def create(userid, name, password, flavor, image, metadata={}, port.index = index port.save() + # Create instance volumes + server_vtype = flavor.volume_type + server_volumes = [] + for index, vol_info in enumerate(volumes): + if vol_info["source_type"] == "volume": + uuid = vol_info["source_uuid"] + v = get_volume(userid, uuid, for_update=True, non_deleted=True, + exception=faults.BadRequest) + if v.volume_type_id != server_vtype.id: + msg = ("Volume '%s' has type '%s' while flavor's volume type" + " is '%s'" % (v.id, v.volume_type_id, server_vtype.id)) + raise faults.BadRequest(msg) + if v.status != "AVAILABLE": + raise faults.BadRequest("Cannot use volume while it is in %s" + " status" % v.status) + v.delete_on_termination = vol_info["delete_on_termination"] + v.machine = vm + v.index = index + v.save() + else: + v = _create_volume(server=vm, user_id=userid, + volume_type=server_vtype, project=project, + index=index, **vol_info) + server_volumes.append(v) + + # Create instance metadata for key, val in metadata.items(): utils.check_name_length(key, VirtualMachineMetadata.KEY_LENGTH, "Metadata key is too long") @@ -215,7 +171,8 @@ def create(userid, name, password, flavor, image, metadata={}, vm=vm) # Create the server in Ganeti. - vm = create_server(vm, ports, flavor, image, personality, password) + vm = create_server(vm, ports, server_volumes, flavor, image, personality, + password) return vm @@ -240,21 +197,30 @@ def allocate_new_server(userid, flavor): return use_backend -@server_command("BUILD") -def create_server(vm, nics, flavor, image, personality, password): +@commands.server_command("BUILD") +def create_server(vm, nics, volumes, flavor, image, personality, password): # dispatch server created signal needed to trigger the 'vmapi', which # enriches the vm object with the 'config_url' attribute which must be # passed to the Ganeti job. + + # If the root volume has a provider, then inform snf-image to not fill + # the volume with data + image_id = image["pithosmap"] + root_volume = volumes[0] + if root_volume.volume_type.provider in settings.GANETI_CLONE_PROVIDERS: + image_id = "null" + server_created.send(sender=vm, created_vm_params={ - 'img_id': image['backend_id'], + 'img_id': image_id, 'img_passwd': password, 'img_format': str(image['format']), 'img_personality': json.dumps(personality), 'img_properties': json.dumps(image['metadata']), }) + # send job to Ganeti try: - jobID = backend.create_instance(vm, nics, flavor, image) + jobID = backend.create_instance(vm, nics, volumes, flavor, image) except: log.exception("Failed create instance '%s'", vm) jobID = None @@ -262,6 +228,7 @@ def create_server(vm, nics, flavor, image, personality, password): vm.backendlogmsg = "Failed to send job to Ganeti." vm.save() vm.nics.all().update(state="ERROR") + vm.volumes.all().update(status="ERROR") # At this point the job is enqueued in the Ganeti backend vm.backendopcode = "OP_INSTANCE_CREATE" @@ -273,7 +240,7 @@ def create_server(vm, nics, flavor, image, personality, password): return jobID -@server_command("DESTROY") +@commands.server_command("DESTROY") def destroy(vm, shutdown_timeout=None): # XXX: Workaround for race where OP_INSTANCE_REMOVE starts executing on # Ganeti before OP_INSTANCE_CREATE. This will be fixed when @@ -287,19 +254,19 @@ def destroy(vm, shutdown_timeout=None): return backend.delete_instance(vm, shutdown_timeout=shutdown_timeout) -@server_command("START") +@commands.server_command("START") def start(vm): log.info("Starting VM %s", vm) return backend.startup_instance(vm) -@server_command("STOP") +@commands.server_command("STOP") def stop(vm, shutdown_timeout=None): log.info("Stopping VM %s", vm) return backend.shutdown_instance(vm, shutdown_timeout=shutdown_timeout) -@server_command("REBOOT") +@commands.server_command("REBOOT") def reboot(vm, reboot_type, shutdown_timeout=None): if reboot_type not in ("SOFT", "HARD"): raise faults.BadRequest("Malformed request. Invalid reboot" @@ -313,7 +280,7 @@ def reboot(vm, reboot_type, shutdown_timeout=None): def resize(vm, flavor): action_fields = {"beparams": {"vcpus": flavor.cpu, "maxmem": flavor.ram}} - comm = server_command("RESIZE", action_fields=action_fields) + comm = commands.server_command("RESIZE", action_fields=action_fields) return comm(_resize)(vm, flavor) @@ -325,15 +292,28 @@ def _resize(vm, flavor): % (vm, flavor)) # Check that resize can be performed if old_flavor.disk != flavor.disk: - raise faults.BadRequest("Cannot resize instance disk.") - if old_flavor.disk_template != flavor.disk_template: - raise faults.BadRequest("Cannot change instance disk template.") + raise faults.BadRequest("Cannot change instance's disk size.") + if old_flavor.volume_type_id != flavor.volume_type_id: + raise faults.BadRequest("Cannot change instance's volume type.") log.info("Resizing VM from flavor '%s' to '%s", old_flavor, flavor) return backend.resize_instance(vm, vcpus=flavor.cpu, memory=flavor.ram) -@server_command("SET_FIREWALL_PROFILE") +@transaction.commit_on_success +def reassign(vm, project): + commands.validate_server_action(vm, "REASSIGN") + action_fields = {"to_project": project, "from_project": vm.project} + log.info("Reassigning VM %s from project %s to %s", + vm, vm.project, project) + vm.project = project + vm.save() + vm.volumes.filter(index=0, deleted=False).update(project=project) + quotas.issue_and_accept_commission(vm, action="REASSIGN", + action_fields=action_fields) + + +@commands.server_command("SET_FIREWALL_PROFILE") def set_firewall_profile(vm, profile, nic): log.info("Setting VM %s, NIC %s, firewall %s", vm, nic, profile) @@ -343,7 +323,7 @@ def set_firewall_profile(vm, profile, nic): return None -@server_command("CONNECT") +@commands.server_command("CONNECT") def connect(vm, network, port=None): if port is None: port = _create_port(vm.userid, network) @@ -354,7 +334,7 @@ def connect(vm, network, port=None): return backend.connect_to_network(vm, port) -@server_command("DISCONNECT") +@commands.server_command("DISCONNECT") def disconnect(vm, nic): log.info("Removing NIC %s from VM %s", nic, vm) return backend.disconnect_from_network(vm, nic) @@ -373,18 +353,41 @@ def console(vm, console_type): """ log.info("Get console VM %s, type %s", vm, console_type) - # Use RAPI to get VNC console information for this instance if vm.operstate != "STARTED": raise faults.BadRequest('Server not in ACTIVE state.') - if settings.TEST: - console_data = {'kind': 'vnc', 'host': 'ganeti_node', 'port': 1000} - else: - console_data = backend.get_instance_console(vm) - - if console_data['kind'] != 'vnc': - message = 'got console of kind %s, not "vnc"' % console_data['kind'] - raise faults.ServiceUnavailable(message) + # Use RAPI to get VNC console information for this instance + # RAPI GetInstanceConsole() returns endpoints to the vnc_bind_address, + # which is a cluster-wide setting, either 0.0.0.0 or 127.0.0.1, and pretty + # useless (see #783). + # + # Until this is fixed on the Ganeti side, construct a console info reply + # directly. + # + # WARNING: This assumes that VNC runs on port network_port on + # the instance's primary node, and is probably + # hypervisor-specific. + def get_console_data(i): + return {"kind": "vnc", + "host": i["pnode"], + "port": i["network_port"]} + with pooled_rapi_client(vm) as c: + i = c.GetInstance(vm.backend_vm_id) + console_data = get_console_data(i) + + if vm.backend.hypervisor == "kvm" and i['hvparams']['serial_console']: + raise Exception("hv parameter serial_console cannot be true") + + # Check that the instance is really running + if not i["oper_state"]: + log.warning("VM '%s' is marked as '%s' in DB while DOWN in Ganeti", + vm.id, vm.operstate) + # Instance is not running. Mock a shutdown job to sync DB + backend.process_op_status(vm, etime=datetime.now(), jobid=0, + opcode="OP_INSTANCE_SHUTDOWN", + status="success", + logmsg="Reconciliation simulated event") + raise faults.BadRequest('Server not in ACTIVE state.') # Let vncauthproxy decide on the source port. # The alternative: static allocation, e.g. @@ -394,24 +397,33 @@ def console(vm, console_type): dport = console_data['port'] password = util.random_password() - if settings.TEST: - fwd = {'source_port': 1234, 'status': 'OK'} - else: - vnc_extra_opts = settings.CYCLADES_VNCAUTHPROXY_OPTS - fwd = request_vnc_forwarding(sport, daddr, dport, password, - **vnc_extra_opts) + vnc_extra_opts = settings.CYCLADES_VNCAUTHPROXY_OPTS + + # Maintain backwards compatibility with the dict setting + if isinstance(vnc_extra_opts, list): + vnc_extra_opts = choice(vnc_extra_opts) + + fwd = request_vnc_forwarding(sport, daddr, dport, password, + console_type=console_type, **vnc_extra_opts) if fwd['status'] != "OK": + log.error("vncauthproxy returned error status: '%s'" % fwd) raise faults.ServiceUnavailable('vncauthproxy returned error status') # Verify that the VNC server settings haven't changed - if not settings.TEST: - if console_data != backend.get_instance_console(vm): - raise faults.ServiceUnavailable('VNC Server settings changed.') + with pooled_rapi_client(vm) as c: + i = c.GetInstance(vm.backend_vm_id) + if get_console_data(i) != console_data: + raise faults.ServiceUnavailable('VNC Server settings changed.') + + try: + host = fwd['proxy_address'] + except KeyError: + host = getfqdn() console = { - 'type': 'vnc', - 'host': getfqdn(), + 'type': console_type, + 'host': host, 'port': fwd['source_port'], 'password': password} @@ -493,6 +505,7 @@ def _create_port(userid, network, machine=None, use_ipaddress=None, state="DOWN", userid=userid, device_owner=None, + public=network.public, name=name) # add the security groups if any @@ -724,7 +737,7 @@ def _port_for_request(user_id, network_dict): network = util.get_network(network_id, user_id, non_deleted=True) if network.public: if network.subnet4 is not None: - if not "fixed_ip" in network_dict: + if "fixed_ip" not in network_dict: return create_public_ipv4_port(user_id, network) elif address is None: msg = "Cannot connect to public network" diff --git a/snf-cyclades-app/synnefo/logic/subnets.py b/snf-cyclades-app/synnefo/logic/subnets.py index ff4d1f6f92a2c9f89c5f43e14f2d2edd68b95770..265ba2ff9c2e13b3221a1c717f0249968bf30a77 100644 --- a/snf-cyclades-app/synnefo/logic/subnets.py +++ b/snf-cyclades-app/synnefo/logic/subnets.py @@ -1,42 +1,24 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import ipaddr from logging import getLogger from functools import wraps from django.conf import settings -from django.db import transaction +from synnefo.db import transaction from django.db.models import Q from snf_django.lib import api @@ -85,7 +67,7 @@ def _create_subnet(network_id, user_id, cidr, name, ipversion=4, gateway=None, try: network_id = int(network_id) - network = Network.objects.get(id=network_id) + network = Network.objects.select_for_update().get(id=network_id) except (ValueError, TypeError): raise api.faults.BadRequest("Malformed network ID") except Network.DoesNotExist: @@ -95,7 +77,7 @@ def _create_subnet(network_id, user_id, cidr, name, ipversion=4, gateway=None, raise api.faults.BadRequest("Network has been deleted") if user_id != network.userid: - raise api.faults.Unauthorized("Unauthorized operation") + raise api.faults.Forbidden("Forbidden operation") if ipversion not in [4, 6]: raise api.faults.BadRequest("Malformed IP version type") @@ -125,6 +107,9 @@ def _create_subnet(network_id, user_id, cidr, name, ipversion=4, gateway=None, dhcp=dhcp, host_routes=host_routes, dns_nameservers=dns_nameservers) + network.subnet_ids.append(sub.id) + network.save() + gateway_ip = ipaddr.IPAddress(gateway) if gateway else None if allocation_pools is not None: @@ -190,7 +175,7 @@ def update_subnet(sub_id, name, user_id): raise api.faults.ItemNotFound("Subnet not found") if user_id != subnet.network.userid: - raise api.faults.Unauthorized("Unauthorized operation") + raise api.faults.Forbidden("Forbidden operation") utils.check_name_length(name, Subnet.SUBNET_NAME_LENGTH, "Subnet name is " " too long") @@ -266,17 +251,17 @@ def validate_subnet_params(subnet=None, gateway=None, subnet6=None, # Check that network size is allowed! prefixlen = network.prefixlen - if prefixlen > 29 or prefixlen <= settings.MAX_CIDR_BLOCK: + if prefixlen > 29 or prefixlen < settings.MAX_CIDR_BLOCK: raise faults.OverLimit( message="Unsupported network size", - details="Netmask must be in range: (%s, 29]" % + details="Netmask must be in range: [%s, 29]" % settings.MAX_CIDR_BLOCK) if gateway: # Check that gateway belongs to network try: gateway = ipaddr.IPv4Address(gateway) except ValueError: raise faults.BadRequest("Invalid network IPv4 gateway") - if not gateway in network: + if gateway not in network: raise faults.BadRequest("Invalid network IPv4 gateway") if subnet6: diff --git a/snf-cyclades-app/synnefo/logic/tests/__init__.py b/snf-cyclades-app/synnefo/logic/tests/__init__.py index 3e97470e2e8663c3645b2ffbdb012c015b9acf6f..3f6fc85b2fda87b1e87a2e8fdd20636c6f2c5c54 100644 --- a/snf-cyclades-app/synnefo/logic/tests/__init__.py +++ b/snf-cyclades-app/synnefo/logic/tests/__init__.py @@ -1,4 +1,6 @@ +# flake8: noqa from .networks import * +from .ips import * from .servers import * from .utils_tests import * from .rapi_pool_tests import * diff --git a/snf-cyclades-app/synnefo/logic/tests/callbacks.py b/snf-cyclades-app/synnefo/logic/tests/callbacks.py index 2c2d13e2d412261bc8473c70d3c27e3b0bd9c9a9..18333a20df744104a683bb1a0918a145dd8b1808 100644 --- a/snf-cyclades-app/synnefo/logic/tests/callbacks.py +++ b/snf-cyclades-app/synnefo/logic/tests/callbacks.py @@ -1,32 +1,18 @@ # vim: set fileencoding=utf-8 : -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # Provides automated tests for logic module @@ -51,7 +37,7 @@ from time import time import json -## Test Callbacks +# Test Callbacks @patch('synnefo.lib.amqp.AMQPClient') class UpdateDBTest(TestCase): def create_msg(self, **kwargs): @@ -133,14 +119,16 @@ class UpdateDBTest(TestCase): self.assertEqual(db_vm.operstate, 'STARTED') def test_remove(self, client): - vm = mfactory.VirtualMachineFactory() + vm = mfactory.VirtualMachineFactory(flavor__cpu=1, flavor__ram=128) + mfactory.VolumeFactory(userid=vm.userid, machine=vm, size=1) + mfactory.VolumeFactory(userid=vm.userid, machine=vm, size=3) # Also create a NIC ip = mfactory.IPv4AddressFactory(nic__machine=vm) nic = ip.nic nic.network.get_ip_pools()[0].reserve(nic.ipv4_address) msg = self.create_msg(operation='OP_INSTANCE_REMOVE', instance=vm.backend_vm_id) - with mocked_quotaholder(): + with mocked_quotaholder() as m: update_db(client, msg) self.assertTrue(client.basic_ack.called) db_vm = VirtualMachine.objects.get(id=vm.id) @@ -149,6 +137,17 @@ class UpdateDBTest(TestCase): # Check that nics are deleted self.assertFalse(db_vm.nics.all()) self.assertTrue(nic.network.get_ip_pools()[0].is_available(ip.address)) + # Check that volumes are deleted + self.assertFalse(db_vm.volumes.filter(deleted=False)) + # Check quotas + name, args, kwargs = m.mock_calls[0] + for (userid, res), value in args[1].items(): + if res == 'cyclades.disk': + self.assertEqual(value, -4 << 30) + elif res == 'cyclades.cpu': + self.assertEqual(value, -1) + elif res == 'cyclades.ram': + self.assertEqual(value, -128 << 20) vm2 = mfactory.VirtualMachineFactory() fp1 = mfactory.IPv4AddressFactory(nic__machine=vm2, floating_ip=True, network__floating_ip_pool=True) @@ -285,11 +284,13 @@ class UpdateDBTest(TestCase): db_vm = VirtualMachine.objects.get(id=vm.id) self.assertEqual(db_vm.operstate, "STOPPED") # Test success - f1 = mfactory.FlavorFactory(cpu=4, ram=1024, disk_template="drbd", + f1 = mfactory.FlavorFactory(cpu=4, ram=1024, + volume_type__disk_template="drbd", disk=1024) vm.flavor = f1 vm.save() - f2 = mfactory.FlavorFactory(cpu=8, ram=2048, disk_template="drbd", + f2 = mfactory.FlavorFactory(cpu=8, ram=2048, + volume_type__disk_template="drbd", disk=1024) beparams = {"vcpus": 8, "minmem": 2048, "maxmem": 2048} msg = self.create_msg(operation='OP_INSTANCE_SET_PARAMS', @@ -313,6 +314,40 @@ class UpdateDBTest(TestCase): update_db(client, msg) self.assertTrue(client.basic_reject.called) + @patch("synnefo.plankton.backend.get_pithos_backend") + def test_error_snapshot(self, pithos_backend, client): + vm = mfactory.VirtualMachineFactory() + disks = [ + (0, {"snapshot_info": json.dumps({"snapshot_id": + "test_snapshot_id"})}) + ] + msg = self.create_msg(operation='OP_INSTANCE_SNAPSHOT', + instance=vm.backend_vm_id, + job_fields={'disks': disks}, + status="running") + update_db(client, msg) + self.assertEqual(pithos_backend().update_object_status.mock_calls, []) + + msg = self.create_msg(operation='OP_INSTANCE_SNAPSHOT', + instance=vm.backend_vm_id, + job_fields={'disks': disks}, + event_time=split_time(time()), + status="error") + update_db(client, msg) + + pithos_backend().update_object_status\ + .assert_called_once_with("test_snapshot_id", state=-1) + + pithos_backend.reset_mock() + msg = self.create_msg(operation='OP_INSTANCE_SNAPSHOT', + instance=vm.backend_vm_id, + job_fields={'disks': disks}, + event_time=split_time(time()), + status="success") + update_db(client, msg) + pithos_backend().update_object_status\ + .assert_called_once_with("test_snapshot_id", state=1) + @patch('synnefo.lib.amqp.AMQPClient') class UpdateNetTest(TestCase): @@ -618,7 +653,6 @@ class UpdateNetworkTest(TestCase): "remove_reserved_ips": ["10.0.0.10", "10.0.0.20"]}) update_network(client, msg) - #self.assertTrue(client.basic_ack.called) pool = network.get_ip_pools()[0] self.assertTrue(pool.is_reserved('10.0.0.10')) self.assertTrue(pool.is_reserved('10.0.0.20')) diff --git a/snf-cyclades-app/synnefo/logic/tests/ips.py b/snf-cyclades-app/synnefo/logic/tests/ips.py new file mode 100644 index 0000000000000000000000000000000000000000..2994ea3b13c94c70f170ebf4e08ceb46b7952029 --- /dev/null +++ b/snf-cyclades-app/synnefo/logic/tests/ips.py @@ -0,0 +1,92 @@ +# vim: set fileencoding=utf-8 : +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# Provides automated tests for logic module +from django.test import TestCase +from django.core.exceptions import ObjectDoesNotExist +from snf_django.lib.api import faults +from snf_django.utils.testing import mocked_quotaholder +from synnefo.logic import ips +from synnefo.db import models_factory as mfactory +from synnefo.db.models import IPAddress + + +class IPTest(TestCase): + + """Test suite for actions on IP addresses.""" + + def setUp(self): + """Common setup method for this suite. + + This setUp method creates a simple IP Pool from which IPs can be + created. + """ + self.subnet = mfactory.IPv4SubnetFactory(network__floating_ip_pool=True) + self.network = self.subnet.network + + def test_create(self): + """Test if a floating IP is created properly.""" + with mocked_quotaholder(): + ip = ips.create_floating_ip("1134", network=self.network) + self.assertEqual(len(self.network.ips.all()), 1) + self.assertEqual(self.network.ips.all()[0], ip) + + def test_delete(self): + """Test if the delete action succeeds/fails properly.""" + # Create a floating IP and force-attach it to a NIC instance. + vm = mfactory.VirtualMachineFactory() + nic = mfactory.NetworkInterfaceFactory(network=self.network, + machine=vm) + with mocked_quotaholder(): + ip = ips.create_floating_ip("1134", network=self.network) + ip.nic = nic + + # Test 1 - Check if we can delete an IP attached to a VM. + # + # The validate function and the action should both fail with the + # following message. + expected_msg = "IP '{}' is used by server '{}'".format(ip.id, vm.id) + + # Verify that the validate function fails in silent mode. + res, msg = ips.validate_ip_action(ip, "DELETE", silent=True) + self.assertFalse(res) + self.assertEqual(msg, expected_msg) + + # Verify that the validate function fails in non-silent mode. + with self.assertRaises(faults.Conflict) as cm: + ips.validate_ip_action(ip, "DELETE", silent=False) + self.assertEqual(cm.exception.message, expected_msg) + + # Verify that the delete action fails with exception. + with mocked_quotaholder(): + with self.assertRaises(faults.Conflict) as cm: + ips.delete_floating_ip(ip) + self.assertEqual(cm.exception.message, expected_msg) + + # Test 2 - Check if we can delete a free IP. + # + # Force-detach IP from NIC. + ip.nic = None + + # Verify that the validate function passes in silent mode. + res, _ = ips.validate_ip_action(ip, "DELETE", silent=True) + self.assertTrue(res) + + # Verify that the delete action succeeds. + with mocked_quotaholder(): + ips.delete_floating_ip(ip) + with self.assertRaises(ObjectDoesNotExist): + IPAddress.objects.get(id=ip.id) diff --git a/snf-cyclades-app/synnefo/logic/tests/networks.py b/snf-cyclades-app/synnefo/logic/tests/networks.py index 597899aa725363d0ada338d089efeca7637aa622..46dd2942581d37e4c2be110b05169aa2d441e6a9 100644 --- a/snf-cyclades-app/synnefo/logic/tests/networks.py +++ b/snf-cyclades-app/synnefo/logic/tests/networks.py @@ -1,32 +1,18 @@ # vim: set fileencoding=utf-8 : -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # Provides automated tests for logic module from django.test import TestCase diff --git a/snf-cyclades-app/synnefo/logic/tests/rapi_pool_tests.py b/snf-cyclades-app/synnefo/logic/tests/rapi_pool_tests.py index 915996477162c572d26e00159e629cc0459b998e..0d9a1437bfc42ff4e1031bffe5963b93aab97ec4 100644 --- a/snf-cyclades-app/synnefo/logic/tests/rapi_pool_tests.py +++ b/snf-cyclades-app/synnefo/logic/tests/rapi_pool_tests.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.test import TestCase diff --git a/snf-cyclades-app/synnefo/logic/tests/reconciliation.py b/snf-cyclades-app/synnefo/logic/tests/reconciliation.py index 92937366e11ae656a75374839f197bb2efa37493..1ec64bca41f0c4b2e410ec5a8e42b607cd10df23 100644 --- a/snf-cyclades-app/synnefo/logic/tests/reconciliation.py +++ b/snf-cyclades-app/synnefo/logic/tests/reconciliation.py @@ -1,32 +1,18 @@ # vim: set fileencoding=utf-8 : -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import logging from django.test import TestCase @@ -124,6 +110,8 @@ class ServerReconciliationTest(TestCase): "oper_state": True, "mtime": time(), "disk.sizes": [], + "disk.names": [], + "disk.uuids": [], "nic.ips": [], "nic.names": [], "nic.macs": [], @@ -145,6 +133,8 @@ class ServerReconciliationTest(TestCase): "oper_state": True, "mtime": time(), "disk.sizes": [], + "disk.names": [], + "disk.uuids": [], "nic.ips": [], "nic.names": [], "nic.macs": [], @@ -157,9 +147,9 @@ class ServerReconciliationTest(TestCase): def test_unsynced_flavor(self, mrapi): flavor1 = mfactory.FlavorFactory(cpu=2, ram=1024, disk=1, - disk_template="drbd") + volume_type__disk_template="drbd") flavor2 = mfactory.FlavorFactory(cpu=4, ram=2048, disk=1, - disk_template="drbd") + volume_type__disk_template="drbd") vm1 = mfactory.VirtualMachineFactory(backend=self.backend, deleted=False, flavor=flavor1, @@ -172,6 +162,8 @@ class ServerReconciliationTest(TestCase): "oper_state": True, "mtime": time(), "disk.sizes": [], + "disk.names": [], + "disk.uuids": [], "nic.ips": [], "nic.names": [], "nic.macs": [], @@ -204,6 +196,8 @@ class ServerReconciliationTest(TestCase): "oper_state": True, "mtime": time(), "disk.sizes": [], + "disk.names": [], + "disk.uuids": [], "nic.names": [nic.backend_uuid], "nic.ips": ["192.168.2.5"], "nic.macs": ["aa:00:bb:cc:dd:ee"], @@ -241,7 +235,8 @@ class NetworkReconciliationTest(TestCase): mrapi().GetNetworks.return_value = [{"name": net1.backend_id, "group_list": [["default", "bridged", - "prv0"]], + "prv0", + "1"]], "network": net1.subnet4.cidr, "map": "....", "external_reservations": ""}] diff --git a/snf-cyclades-app/synnefo/logic/tests/servers.py b/snf-cyclades-app/synnefo/logic/tests/servers.py index c48065cdf7930503b0b428fcdd6b1d980da658f5..5777ddd17ec4c143603be416a89eadfddfde2290 100644 --- a/snf-cyclades-app/synnefo/logic/tests/servers.py +++ b/snf-cyclades-app/synnefo/logic/tests/servers.py @@ -1,32 +1,18 @@ # vim: set fileencoding=utf-8 : -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # Provides automated tests for logic module from django.test import TransactionTestCase @@ -34,14 +20,28 @@ from django.test import TransactionTestCase from synnefo.logic import servers from synnefo import quotas from synnefo.db import models_factory as mfactory, models -from mock import patch +from mock import patch, Mock from snf_django.lib.api import faults from snf_django.utils.testing import mocked_quotaholder, override_settings from django.conf import settings from copy import deepcopy +fixed_image = Mock() +fixed_image.return_value = {'location': 'pithos://foo', + 'mapfile': 'test_mapfile', + "id": 1, + "name": "test_image", + "version": 42, + "is_public": True, + "owner": "user2", + "status": "AVAILABLE", + "size": 1000, + "is_snapshot": False, + 'disk_format': 'diskdump'} + +@patch('synnefo.api.util.get_image', fixed_image) @patch("synnefo.logic.rapi_pool.GanetiRapiClient") class ServerCreationTest(TransactionTestCase): def test_create(self, mrapi): @@ -51,9 +51,7 @@ class ServerCreationTest(TransactionTestCase): "name": "test_vm", "password": "1234", "flavor": flavor, - "image": {"id": "foo", "backend_id": "foo", "format": "diskdump", - "checksum": "test_checksum", - "metadata": "{}"}, + "image_id": "safs", "networks": [], "metadata": {"foo": "bar"}, "personality": [], @@ -85,8 +83,9 @@ class ServerCreationTest(TransactionTestCase): # test ext settings: req = deepcopy(kwargs) - ext_flavor = mfactory.FlavorFactory(disk_template="ext_archipelago", - disk=1) + ext_flavor = mfactory.FlavorFactory( + volume_type__disk_template="ext_archipelago", + disk=1) req["flavor"] = ext_flavor mrapi().CreateInstance.return_value = 42 backend.disk_templates = ["ext"] @@ -103,11 +102,14 @@ class ServerCreationTest(TransactionTestCase): with override_settings(settings, **osettings): vm = servers.create(**req) name, args, kwargs = mrapi().CreateInstance.mock_calls[-1] - self.assertEqual(kwargs["disks"][0], {"provider": "archipelago", - "origin": "test_checksum", - "foo": "mpaz", - "lala": "lolo", - "size": 1024}) + self.assertEqual(kwargs["disks"][0], + {"provider": "archipelago", + "origin": "test_mapfile", + "origin_size": 1000, + "name": vm.volumes.all()[0].backend_volume_uuid, + "foo": "mpaz", + "lala": "lolo", + "size": 1024}) @patch("synnefo.logic.rapi_pool.GanetiRapiClient") @@ -247,9 +249,14 @@ class ServerCommandTest(TransactionTestCase): with mocked_quotaholder() as m: try: servers.start(vm) - except: - m.resolve_commissions\ - .assert_called_once_with([], [vm.serial.serial]) + except Exception: + (accept, reject), kwargs = m.resolve_commissions.call_args + self.assertEqual(accept, []) + self.assertEqual(len(reject), 1) + self.assertEqual(kwargs, {}) + else: + raise AssertionError("Starting a server should raise an" + " exception.") def test_task_after(self, mrapi): return @@ -274,3 +281,24 @@ class ServerCommandTest(TransactionTestCase): servers.reboot(vm) self.assertEqual(vm.task, "REBOOT") self.assertEqual(vm.task_job_id, 3) + + def test_reassign_vm(self, mrapi): + volume = mfactory.VolumeFactory() + vm = volume.machine + another_project = "another_project" + with mocked_quotaholder(): + servers.reassign(vm, another_project) + self.assertEqual(vm.project, another_project) + vol = vm.volumes.get(id=volume.id) + self.assertNotEqual(vol.project, another_project) + + volume = mfactory.VolumeFactory() + volume.index = 0 + volume.save() + vm = volume.machine + another_project = "another_project" + with mocked_quotaholder(): + servers.reassign(vm, another_project) + self.assertEqual(vm.project, another_project) + vol = vm.volumes.get(id=volume.id) + self.assertEqual(vol.project, another_project) diff --git a/snf-cyclades-app/synnefo/logic/tests/utils_tests.py b/snf-cyclades-app/synnefo/logic/tests/utils_tests.py index 50d160de6d769e9e5d45f911bd1820288de30b31..ab0696af96f358fbaf76d1bb899e4effce33fd7a 100644 --- a/snf-cyclades-app/synnefo/logic/tests/utils_tests.py +++ b/snf-cyclades-app/synnefo/logic/tests/utils_tests.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.test import TestCase diff --git a/snf-cyclades-app/synnefo/logic/utils.py b/snf-cyclades-app/synnefo/logic/utils.py index 9fdf8a77c5b68c50c6d58b73635a37f4f1c34216..6bdbedad134b16727d3bf545c873430b66c1b44e 100644 --- a/snf-cyclades-app/synnefo/logic/utils.py +++ b/snf-cyclades-app/synnefo/logic/utils.py @@ -1,31 +1,17 @@ -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # Utility functions @@ -33,7 +19,7 @@ from synnefo.db.models import VirtualMachine, Network from snf_django.lib.api import faults from django.conf import settings from copy import deepcopy -from synnefo.util.text import uenc +from django.utils.encoding import smart_unicode def id_from_instance_name(name): @@ -42,7 +28,7 @@ def id_from_instance_name(name): Strips the ganeti prefix atm. Needs a better name! """ - sname = str(name) + sname = smart_unicode(name) if not sname.startswith(settings.BACKEND_PREFIX_ID): raise VirtualMachine.InvalidBackendIdError(sname) ns = sname.replace(settings.BACKEND_PREFIX_ID, "", 1) @@ -53,7 +39,7 @@ def id_from_instance_name(name): def id_to_instance_name(id): - return "%s%s" % (settings.BACKEND_PREFIX_ID, str(id)) + return "%s%s" % (settings.BACKEND_PREFIX_ID, smart_unicode(id)) def id_from_network_name(name): @@ -62,32 +48,51 @@ def id_from_network_name(name): Strips the ganeti prefix atm. Needs a better name! """ - if not str(name).startswith(settings.BACKEND_PREFIX_ID): - raise Network.InvalidBackendIdError(str(name)) - ns = str(name).replace(settings.BACKEND_PREFIX_ID + 'net-', "", 1) + name = smart_unicode(name) + if not name.startswith(settings.BACKEND_PREFIX_ID): + raise Network.InvalidBackendIdError(name) + ns = name.replace(settings.BACKEND_PREFIX_ID + 'net-', "", 1) if not ns.isdigit(): - raise Network.InvalidBackendIdError(str(name)) + raise Network.InvalidBackendIdError(smart_unicode(name)) return int(ns) def id_to_network_name(id): - return "%snet-%s" % (settings.BACKEND_PREFIX_ID, str(id)) + return "%snet-%s" % (settings.BACKEND_PREFIX_ID, smart_unicode(id)) def id_from_nic_name(name): """Returns NIC's Django id, given a Ganeti's NIC name. """ - if not str(name).startswith(settings.BACKEND_PREFIX_ID): + name = smart_unicode(name) + if not name.startswith(settings.BACKEND_PREFIX_ID): raise ValueError("Invalid NIC name: %s" % name) - ns = str(name).replace(settings.BACKEND_PREFIX_ID + 'nic-', "", 1) + ns = name.replace(settings.BACKEND_PREFIX_ID + 'nic-', "", 1) if not ns.isdigit(): raise ValueError("Invalid NIC name: %s" % name) return int(ns) +def id_from_disk_name(name): + """Returns Disk Django id, given a Ganeti's Disk name. + + """ + if not str(name).startswith(settings.BACKEND_PREFIX_ID): + raise ValueError("Invalid Disk name: %s" % name) + ns = str(name).replace(settings.BACKEND_PREFIX_ID + 'vol-', "", 1) + if not ns.isdigit(): + raise ValueError("Invalid Disk name: %s" % name) + + return int(ns) + + +def id_to_disk_name(id): + return "%svol-%s" % (settings.BACKEND_PREFIX_ID, str(id)) + + def get_rsapi_state(vm): """Returns the API state for a virtual machine @@ -139,7 +144,9 @@ TASK_STATE_FROM_ACTION = { "DESTROY": "DESTROYING", "RESIZE": "RESIZING", "CONNECT": "CONNECTING", - "DISCONNECT": "DISCONNECTING"} + "DISCONNECT": "DISCONNECTING", + "ATTACH_VOLUME": "ATTACHING_VOLUME", + "DETACH_VOLUME": "DETACHING_VOLUME"} def get_task_state(vm): @@ -162,6 +169,7 @@ OPCODE_TO_ACTION = { def get_action_from_opcode(opcode, job_fields): if opcode == "OP_INSTANCE_SET_PARAMS": nics = job_fields.get("nics") + disks = job_fields.get("disks") beparams = job_fields.get("beparams") if nics: try: @@ -174,6 +182,19 @@ def get_action_from_opcode(opcode, job_fields): return None except: return None + if disks: + try: + disk_action = disks[0][0] + if disk_action == "add": + return "ATTACH_VOLUME" + elif disk_action == "remove": + return "DETACH_VOLUME" + elif disk_action == "modify": + return "MODIFY_VOLUME" + else: + return None + except: + return None elif beparams: return "RESIZE" else: @@ -193,5 +214,6 @@ def hide_pass(kw): def check_name_length(name, max_length, message): """Check if a string is within acceptable value length""" - if len(uenc(name)) > max_length: + name = smart_unicode(name, encoding="utf-8") + if len(name) > max_length: raise faults.BadRequest(message) diff --git a/snf-cyclades-app/synnefo/management/cli_options.py b/snf-cyclades-app/synnefo/management/cli_options.py new file mode 100644 index 0000000000000000000000000000000000000000..de3b4201aec8af8f5141c261c94a2b9ba5fe75f4 --- /dev/null +++ b/snf-cyclades-app/synnefo/management/cli_options.py @@ -0,0 +1,81 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import astakosclient +from django.conf import settings +from optparse import make_option, OptionValueError +from snf_django.management.utils import parse_bool + + +class WrappedOptions(object): + """Wrapper class to provide access to options as object attributes""" + def __init__(self, options_dict): + self.__dict__.update(options_dict) + + +def make_boolean_option(*args, **kwargs): + """Helper function to create a boolean option.""" + def parse_boolean_option(option, option_str, value, parser): + if value is not None: + try: + value = parse_bool(value) + except ValueError: + choices = "True, False" + raise OptionValueError( + "option %s: invalid choice: %r (choose from %s)" + % (option, value, choices)) + setattr(parser.values, option.dest, value) + + return make_option(*args, + metavar="True|False", + type=str, + action="callback", + callback=parse_boolean_option, + **kwargs) + + +def parse_user_option(option, option_str, value, parser): + """Callback to parser -u/--user option + + Translate uuid <-> email and add 'user_id' and 'user_email' to command + options. + + """ + astakos = astakosclient.AstakosClient(settings.CYCLADES_SERVICE_TOKEN, + settings.ASTAKOS_AUTH_URL, + retry=2) + try: + if "@" in value: + email = value + uuid = astakos.service_get_uuid(email) + else: + uuid = value + email = astakos.service_get_username(uuid) + except astakosclient.errors.NoUUID: + raise OptionValueError("User with email %r does not exist" % email) + except astakosclient.errors.NoUserName: + raise OptionValueError("User with uuid %r does not exist" % uuid) + except astakosclient.errors.AstakosClientException as e: + raise OptionValueError("Failed to get user info:\n%r" % e) + + setattr(parser.values, 'user_id', uuid) + setattr(parser.values, 'user_email', email) + + +USER_OPT = make_option("-u", "--user", + default=None, type=str, + action="callback", callback=parse_user_option, + help="Specify the UUID or email of the user") diff --git a/snf-cyclades-app/synnefo/management/common.py b/snf-cyclades-app/synnefo/management/common.py index 87021e3d4f79f697fce51ef4b4d2553f6aa5fd32..3b25f640ae042b439c08383313f4f260ae242a9f 100644 --- a/snf-cyclades-app/synnefo/management/common.py +++ b/snf-cyclades-app/synnefo/management/common.py @@ -1,50 +1,35 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.core.management import CommandError from synnefo.db.models import (Backend, VirtualMachine, Network, Flavor, IPAddress, Subnet, BridgePoolTable, MacPrefixPoolTable, - NetworkInterface, IPAddressLog) + NetworkInterface, Volume, VolumeType) from functools import wraps +from django.conf import settings from snf_django.lib.api import faults from synnefo.api import util from synnefo.logic import backend as backend_mod from synnefo.logic.rapi import GanetiApiError, GanetiRapiClient from synnefo.logic.utils import (id_from_instance_name, - id_from_network_name) - + id_from_network_name, + id_from_nic_name, + id_from_disk_name) +from django.core.exceptions import ObjectDoesNotExist import logging log = logging.getLogger(__name__) @@ -55,17 +40,58 @@ def format_vm_state(vm): else: return vm.operstate +RESOURCE_MAP = { + "backend": Backend.objects, + "flavor": Flavor.objects, + "server": VirtualMachine.objects, + "volume": Volume.objects, + "network": Network.objects, + "subnet": Subnet.objects, + "port": NetworkInterface.objects, + "floating-ip": IPAddress.objects.filter(floating_ip=True), + "volume-type": VolumeType.objects} + -def get_backend(backend_id): +def get_resource(name, value, for_update=False): + """Get object from DB based by it's ID + + Helper function for getting an object from DB by it's DB and raising + appropriate command line errors if the object does not exist or the + ID is invalid. + + """ + objects = RESOURCE_MAP[name] + if name == "floating-ip": + capital_name = "Floating IP" + else: + capital_name = name.capitalize() + PREFIXED_RESOURCES = ["server", "network", "port", "volume"] + + if isinstance(value, basestring) and name in PREFIXED_RESOURCES: + if value.startswith(settings.BACKEND_PREFIX_ID): + try: + if name == "server": + value = id_from_instance_name(value) + elif name == "network": + value = id_from_network_name(value) + elif name == "port": + value = id_from_nic_name(value) + elif name == "volume": + value = id_from_disk_name(value) + except ValueError: + raise CommandError("Invalid {} ID: {}".format(capital_name, + value)) + + if for_update: + objects = objects.select_for_update() try: - backend_id = int(backend_id) - return Backend.objects.get(id=backend_id) - except ValueError: - raise CommandError("Invalid Backend ID: %s" % backend_id) - except Backend.DoesNotExist: - raise CommandError("Backend with ID %s not found in DB. " - " Use snf-manage backend-list to find" - " out available backend IDs." % backend_id) + return objects.get(id=value) + except ObjectDoesNotExist: + msg = ("{0} with ID {1} does not exist. Use {2}-list to find out" + " available {2} IDs.") + raise CommandError(msg.format(capital_name, value, name)) + except (ValueError, TypeError): + raise CommandError("Invalid {} ID: {}".format(capital_name, value)) def get_image(image_id, user_id): @@ -80,144 +106,6 @@ def get_image(image_id, user_id): raise CommandError("image-id is mandatory") -def get_vm(server_id, for_update=False): - """Get a VirtualMachine object by its ID. - - @type server_id: int or string - @param server_id: The server's DB id or the Ganeti name - - """ - try: - server_id = int(server_id) - except (ValueError, TypeError): - try: - server_id = id_from_instance_name(server_id) - except VirtualMachine.InvalidBackendIdError: - raise CommandError("Invalid server ID: %s" % server_id) - - try: - objs = VirtualMachine.objects - if for_update: - objs = objs.select_for_update() - return objs.get(id=server_id) - except VirtualMachine.DoesNotExist: - raise CommandError("Server with ID %s not found in DB." - " Use snf-manage server-list to find out" - " available server IDs." % server_id) - - -def get_network(network_id, for_update=True): - """Get a Network object by its ID. - - @type network_id: int or string - @param network_id: The networks DB id or the Ganeti name - - """ - - try: - network_id = int(network_id) - except (ValueError, TypeError): - try: - network_id = id_from_network_name(network_id) - except Network.InvalidBackendIdError: - raise CommandError("Invalid network ID: %s" % network_id) - - networks = Network.objects - if for_update: - networks = networks.select_for_update() - try: - return networks.get(id=network_id) - except Network.DoesNotExist: - raise CommandError("Network with ID %s not found in DB." - " Use snf-manage network-list to find out" - " available network IDs." % network_id) - - -def get_subnet(subnet_id, for_update=True): - """Get a Subnet object by its ID.""" - try: - subet_id = int(subnet_id) - except (ValueError, TypeError): - raise CommandError("Invalid subnet ID: %s" % subnet_id) - - try: - subnets = Subnet.objects - if for_update: - subnets.select_for_update() - return subnets.get(id=subnet_id) - except Subnet.DoesNotExist: - raise CommandError("Subnet with ID %s not found in DB." - " Use snf-manage subnet-list to find out" - " available subnet IDs" % subnet_id) - - -def get_port(port_id, for_update=True): - """Get a port object by its ID.""" - try: - port_id = int(port_id) - except (ValueError, TypeError): - raise CommandError("Invalid port ID: %s" % port_id) - - try: - ports = NetworkInterface.objects - if for_update: - ports.select_for_update() - return ports.get(id=port_id) - except NetworkInterface.DoesNotExist: - raise CommandError("Port with ID %s not found in DB." - " Use snf-manage port-list to find out" - " available port IDs" % port_id) - - -def get_flavor(flavor_id, for_update=False): - try: - flavor_id = int(flavor_id) - objs = Flavor.objects - if for_update: - objs = objs.select_for_update() - return objs.get(id=flavor_id) - except ValueError: - raise CommandError("Invalid flavor ID: %s", flavor_id) - except Flavor.DoesNotExist: - raise CommandError("Flavor with ID %s not found in DB." - " Use snf-manage flavor-list to find out" - " available flavor IDs." % flavor_id) - - -def get_floating_ip_by_address(address, for_update=False): - try: - objects = IPAddress.objects - if for_update: - objects = objects.select_for_update() - return objects.get(floating_ip=True, address=address, deleted=False) - except IPAddress.DoesNotExist: - raise CommandError("Floating IP does not exist.") - - -def get_floating_ip_log_by_address(address): - try: - objects = IPAddressLog.objects - return objects.filter(address=address).order_by("released_at") - except IPAddressLog.DoesNotExist: - raise CommandError("Floating IP does not exist or it hasn't be" - "attached to any server yet") - - -def get_floating_ip_by_id(floating_ip_id, for_update=False): - try: - floating_ip_id = int(floating_ip_id) - except (ValueError, TypeError): - raise CommandError("Invalid floating-ip ID: %s" % floating_ip_id) - - try: - objects = IPAddress.objects - if for_update: - objects = objects.select_for_update() - return objects.get(floating_ip=True, id=floating_ip_id, deleted=False) - except IPAddress.DoesNotExist: - raise CommandError("Floating IP %s does not exist." % floating_ip_id) - - def check_backend_credentials(clustername, port, username, password): try: client = GanetiRapiClient(clustername, port, username, password) diff --git a/snf-cyclades-app/synnefo/management/pprint.py b/snf-cyclades-app/synnefo/management/pprint.py index c20e15115eaadb114363fefb985eefe0f784b3da..3c6548d9fa49200703d2adf619df265bf55f811e 100644 --- a/snf-cyclades-app/synnefo/management/pprint.py +++ b/snf-cyclades-app/synnefo/management/pprint.py @@ -1,35 +1,17 @@ -#Copyright (C) 2013-2014 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -#Redistribution and use in source and binary forms, with or -#without modification, are permitted provided that the following -#conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -#THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -#OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -#WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -#PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A. OR -#CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -#SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -#LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -#USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -#AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -#LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -#ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -#POSSIBILITY OF SUCH DAMAGE. -# -#The views and conclusions contained in the software and -#documentation are those of the authors and should not be -#interpreted as representing official policies, either expressed -#or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import sys from snf_django.management.utils import pprint_table @@ -38,10 +20,10 @@ from snf_django.lib.astakos import UserCache from synnefo.settings import (CYCLADES_SERVICE_TOKEN as ASTAKOS_TOKEN, ASTAKOS_AUTH_URL) from synnefo.db.models import Backend, pooled_rapi_client -from synnefo.db.pools import bitarray_to_map from synnefo.logic.rapi import GanetiApiError -from synnefo.logic.reconciliation import nics_from_instance +from synnefo.logic.reconciliation import (nics_from_instance, + disks_from_instance) from synnefo.management.common import get_image @@ -54,12 +36,14 @@ def pprint_network(network, display_mails=False, stdout=None, title=None): ucache = UserCache(ASTAKOS_AUTH_URL, ASTAKOS_TOKEN) userid = network.userid + total_ips, free_ips = network.ip_count() db_network = OrderedDict([ ("name", network.name), ("backend-name", network.backend_id), ("state", network.state), ("userid", userid), - ("username", ucache.get_name(userid) if display_mails else None), + ("username", ucache.get_name(userid) if (display_mails and userid is + not None) else None), ("public", network.public), ("floating_ip_pool", network.floating_ip_pool), ("external_router", network.external_router), @@ -70,7 +54,10 @@ def pprint_network(network, display_mails=False, stdout=None, title=None): ("mode", network.mode), ("deleted", network.deleted), ("tags", "), ".join(network.backend_tag)), - ("action", network.action)]) + ("action", network.action), + ("free IPs", free_ips), + ("total IPs", total_ips), + ]) pprint_table(stdout, db_network.items(), None, separator=" | ", title=title) @@ -160,11 +147,11 @@ def pprint_ippool(subnet, stdout=None, title=None): for pool in subnet.get_ip_pools(): size = pool.pool_size - available = pool.available.count() info = OrderedDict([("First_IP", pool.return_start()), ("Last_IP", pool.return_end()), ("Size", size), - ("Available", available)]) + ("Available", pool.count_available()), + ("Reserved", pool.count_reserved())]) pprint_table(stdout, info.items(), None, separator=" | ", title=None) reserved = [pool.index_to_value(index) @@ -175,8 +162,7 @@ def pprint_ippool(subnet, stdout=None, title=None): stdout.write("\nExternally Reserved IPs:\n\n") stdout.write(", ".join(reserved) + "\n") - ip_sum = pool.available[:size] & pool.reserved[:size] - pprint_pool(None, bitarray_to_map(ip_sum), 80, stdout) + pprint_pool(None, pool.to_map(), 80, stdout) stdout.write("\n\n") @@ -206,15 +192,20 @@ def pprint_pool(name, pool_map, step=80, stdout=None): stdout.write("\n") -def pprint_port(port, stdout=None, title=None): +def pprint_port(port, display_mails=False, stdout=None, title=None): if stdout is None: stdout = sys.stdout if title is None: title = "State of Port %s in DB" % port.id + + ucache = UserCache(ASTAKOS_AUTH_URL, ASTAKOS_TOKEN) + userid = port.userid + port = OrderedDict([ ("id", port.id), ("name", port.name), ("userid", port.userid), + ("username", ucache.get_name(userid) if display_mails else None), ("server", port.machine_id), ("network", port.network_id), ("device_owner", port.device_owner), @@ -331,6 +322,25 @@ def pprint_server_nics(server, stdout=None, title=None): title=title) +def pprint_server_volumes(server, stdout=None, title=None): + if title is None: + title = "Volumes of Server %s" % server.id + if stdout is None: + stdout = sys.stdout + + vols = [] + for vol in server.volumes.filter(deleted=False): + volume_type = vol.volume_type + vols.append((vol.id, vol.name, vol.index, vol.size, + volume_type.template, volume_type.provider, + vol.status, vol.source)) + + headers = ["ID", "Name", "Index", "Size", "Template", "Provider", + "Status", "Source"] + pprint_table(stdout, vols, headers, separator=" | ", + title=title) + + def pprint_server_in_ganeti(server, print_jobs=False, stdout=None, title=None): if stdout is None: stdout = sys.stdout @@ -365,6 +375,13 @@ def pprint_server_in_ganeti(server, print_jobs=False, stdout=None, title=None): pprint_table(stdout, nics_values, nics_keys, separator=" | ", title="NICs of Server %s in Ganeti" % server.id) + stdout.write("\n") + disks = disks_from_instance(server_info) + disks_keys = ["name", "size"] + disks_values = [[disk[key] for key in disks_keys] for disk in disks] + pprint_table(stdout, disks_values, disks_keys, separator=" | ", + title="Disks of Server %s in Ganeti" % server.id) + if not print_jobs: return @@ -382,3 +399,87 @@ def pprint_server_in_ganeti(server, print_jobs=False, stdout=None, title=None): separator=" | ", title="Ganeti Job %s" % server_job["id"]) server.put_client(client) + + +def pprint_volume(volume, display_mails=False, stdout=None, title=None): + if stdout is None: + stdout = sys.stdout + if title is None: + title = "State of volume %s in DB" % volume.id + + ucache = UserCache(ASTAKOS_AUTH_URL, ASTAKOS_TOKEN) + userid = volume.userid + + volume_type = volume.volume_type + volume_dict = OrderedDict([ + ("id", volume.id), + ("size", volume.size), + ("disk_template", volume_type.template), + ("disk_provider", volume_type.provider), + ("server_id", volume.machine_id), + ("userid", volume.userid), + ("project", volume.project), + ("username", ucache.get_name(userid) if display_mails else None), + ("index", volume.index), + ("name", volume.name), + ("state", volume.status), + ("delete_on_termination", volume.delete_on_termination), + ("deleted", volume.deleted), + ("backendjobid", volume.backendjobid), + ]) + + pprint_table(stdout, volume_dict.items(), None, separator=" | ", + title=title) + + +def pprint_volume_in_ganeti(volume, stdout=None, title=None): + if stdout is None: + stdout = sys.stdout + if title is None: + title = "State of volume %s in Ganeti" % volume.id + + vm = volume.machine + if vm is None: + stdout.write("volume is not attached to any instance.\n") + return + + client = vm.get_client() + try: + vm_info = client.GetInstance(vm.backend_vm_id) + except GanetiApiError as e: + if e.code == 404: + stdout.write("Volume seems attached to server %s, but" + " server does not exist in backend.\n" + % vm) + return + raise e + + disks = disks_from_instance(vm_info) + try: + gnt_disk = filter(lambda disk: + disk.get("name") == volume.backend_volume_uuid, + disks)[0] + gnt_disk["instance"] = vm_info["name"] + except IndexError: + stdout.write("Volume %s is not attached to instance %s\n" % (volume.id, + vm.id)) + return + pprint_table(stdout, gnt_disk.items(), None, separator=" | ", + title=title) + + vm.put_client(client) + + +def pprint_volume_type(volume_type, stdout=None, title=None): + if stdout is None: + stdout = sys.stdout + if title is None: + title = "Volume Type %s" % volume_type.id + + vtype_info = OrderedDict([ + ("name", volume_type.name), + ("disk template", volume_type.disk_template), + ("deleted", volume_type.deleted), + ]) + + pprint_table(stdout, vtype_info.items(), separator=" | ", title=title) diff --git a/snf-cyclades-app/synnefo/plankton/backend.py b/snf-cyclades-app/synnefo/plankton/backend.py index 36d9dc5d2bdca69f1487e0bb94f89a60d62aec09..ba883bab875f35c19ce5485587ef028b92c9107a 100644 --- a/snf-cyclades-app/synnefo/plankton/backend.py +++ b/snf-cyclades-app/synnefo/plankton/backend.py @@ -1,36 +1,17 @@ -# Copyright 2011-2013 GRNET S.A. All rights reserved. - -# -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: -# -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# Copyright (C) 2010-2014 GRNET S.A. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. +# 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 General Public License for more details. # -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. """ The Plankton attributes are the following: @@ -46,25 +27,31 @@ The Plankton attributes are the following: - owner: the file's account - properties: stored as user meta prefixed with PROPERTY_PREFIX - size: the 'bytes' meta - - status: stored as a system meta - store: is always 'pithos' - updated_at: the 'modified' meta """ import json -import warnings import logging import os from time import time, gmtime, strftime from functools import wraps from operator import itemgetter +from collections import namedtuple +from copy import deepcopy +from urllib import quote, unquote from django.conf import settings from django.utils import importlib -from pithos.backends.base import NotAllowedError, VersionNotExists -from synnefo.util.text import uenc +from django.utils.encoding import smart_unicode, smart_str +from pithos.backends.base import (NotAllowedError, VersionNotExists, + QuotaError, LimitExceeded, + MAP_AVAILABLE, MAP_UNAVAILABLE, MAP_ERROR) +from pithos.backends.util import PithosBackendPool +from snf_django.lib.api import faults +Location = namedtuple("ObjectLocation", ["account", "container", "path"]) logger = logging.getLogger(__name__) @@ -74,360 +61,341 @@ PLANKTON_PREFIX = 'plankton:' PROPERTY_PREFIX = 'property:' PLANKTON_META = ('container_format', 'disk_format', 'name', - 'status', 'created_at') + 'status', 'created_at', 'volume_id', 'description') + +SNAPSHOTS_CONTAINER = "snapshots" +SNAPSHOTS_TYPE = "application/octet-stream" MAX_META_KEY_LENGTH = 128 - len(PLANKTON_DOMAIN) - len(PROPERTY_PREFIX) MAX_META_VALUE_LENGTH = 256 -from pithos.backends.util import PithosBackendPool -_pithos_backend_pool = \ - PithosBackendPool( - settings.PITHOS_BACKEND_POOL_SIZE, - astakos_auth_url=settings.ASTAKOS_AUTH_URL, - service_token=settings.CYCLADES_SERVICE_TOKEN, - astakosclient_poolsize=settings.CYCLADES_ASTAKOSCLIENT_POOLSIZE, - db_connection=settings.BACKEND_DB_CONNECTION, - block_path=settings.BACKEND_BLOCK_PATH) +_pithos_backend_pool = None def get_pithos_backend(): + global _pithos_backend_pool + if _pithos_backend_pool is None: + _pithos_backend_pool = PithosBackendPool( + settings.PITHOS_BACKEND_POOL_SIZE, + astakos_auth_url=settings.ASTAKOS_AUTH_URL, + service_token=settings.CYCLADES_SERVICE_TOKEN, + astakosclient_poolsize=settings.CYCLADES_ASTAKOSCLIENT_POOLSIZE, + db_connection=settings.BACKEND_DB_CONNECTION, + archipelago_conf_file=settings.PITHOS_BACKEND_ARCHIPELAGO_CONF, + xseg_pool_size=settings.PITHOS_BACKEND_XSEG_POOL_SIZE, + map_check_interval=settings.PITHOS_BACKEND_MAP_CHECK_INTERVAL, + resource_max_metadata=settings.PITHOS_RESOURCE_MAX_METADATA) return _pithos_backend_pool.pool_get() -def create_url(account, container, name): - assert "/" not in account, "Invalid account" - assert "/" not in container, "Invalid container" - return "pithos://%s/%s/%s" % (account, container, name) - - -def split_url(url): - """Returns (accout, container, object) from a url string""" - try: - assert(isinstance(url, basestring)) - t = url.split('/', 4) - assert t[0] == "pithos:", "Invalid url" - assert len(t) == 5, "Invalid url" - return t[2:5] - except AssertionError: - raise InvalidLocation("Invalid location '%s" % url) - - def format_timestamp(t): return strftime('%Y-%m-%d %H:%M:%S', gmtime(t)) -def handle_backend_exceptions(func): +def handle_pithos_backend(func): @wraps(func) - def wrapper(*args, **kwargs): - try: - return func(*args, **kwargs) - except NotAllowedError: - raise Forbidden - except NameError: - raise ImageNotFound - except VersionNotExists: - raise ImageNotFound - return wrapper - - -def commit_on_success(func): def wrapper(self, *args, **kwargs): backend = self.backend backend.pre_exec() + commit = False try: ret = func(self, *args, **kwargs) - except: - backend.post_exec(False) - raise + except NotAllowedError: + raise faults.Forbidden + except (NameError, VersionNotExists): + raise faults.ItemNotFound + except (AssertionError, ValueError): + raise faults.BadRequest + except QuotaError: + raise faults.OverLimit + except LimitExceeded, e: + raise faults.BadRequest(e.args[0]) else: - backend.post_exec(True) + commit = True + finally: + backend.post_exec(commit) return ret return wrapper -class ImageBackend(object): +OBJECT_AVAILABLE = "AVAILABLE" +OBJECT_UNAVAILABLE = "UNAVAILABLE" +OBJECT_ERROR = "ERROR" +OBJECT_DELETED = "DELETED" + +MAP_TO_OBJ_STATES = { + MAP_AVAILABLE: OBJECT_AVAILABLE, + MAP_UNAVAILABLE: OBJECT_UNAVAILABLE, + MAP_ERROR: OBJECT_ERROR +} + +OBJ_TO_MAP_STATES = dict([(v, k) for k, v in MAP_TO_OBJ_STATES.items()]) + + +class PlanktonBackend(object): """A wrapper arround the pithos backend to simplify image handling.""" def __init__(self, user): self.user = user - - original_filters = warnings.filters - warnings.simplefilter('ignore') # Suppress SQLAlchemy warnings self.backend = get_pithos_backend() - warnings.filters = original_filters # Restore warnings def close(self): """Close PithosBackend(return to pool)""" self.backend.close() - @handle_backend_exceptions - @commit_on_success - def get_image(self, image_uuid): - """Retrieve information about an image.""" - image_url = self._get_image_url(image_uuid) - return self._get_image(image_url) - - def _get_image_url(self, image_uuid): - """Get the Pithos url that corresponds to an image UUID.""" - account, container, name = self.backend.get_uuid(self.user, image_uuid) - return create_url(account, container, name) + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + self.backend = None + return False + + @handle_pithos_backend + def get_image(self, uuid, check_permissions=True): + return self._get_image(uuid, check_permissions=check_permissions) + + def _get_image(self, uuid, check_permissions=True): + location, metadata, permissions = \ + self.get_pithos_object(uuid, check_permissions=check_permissions) + return image_to_dict(location, metadata, permissions) + + @handle_pithos_backend + def add_property(self, uuid, key, value): + location, _m, _p = self.get_pithos_object(uuid) + properties = self._prefix_properties({key: value}) + self._update_metadata(uuid, location, properties, replace=False) + + @handle_pithos_backend + def remove_property(self, uuid, key): + location, _m, _p = self.get_pithos_object(uuid) + # Use empty string to delete a property + properties = self._prefix_properties({key: ""}) + self._update_metadata(uuid, location, properties, replace=False) + + @handle_pithos_backend + def update_properties(self, uuid, properties, replace=False): + location, _, _p = self.get_pithos_object(uuid) + properties = self._prefix_properties(properties) + self._update_metadata(uuid, location, properties, replace=replace) + + @staticmethod + def _prefix_properties(properties): + """Add property prefix to properties.""" + return dict([(PROPERTY_PREFIX + k, v) for k, v in properties.items()]) + + @staticmethod + def _unprefix_properties(properties): + """Remove property prefix from properties.""" + return dict([(k.replace(PROPERTY_PREFIX, "", 1), v) + for k, v in properties.items()]) + + @staticmethod + def _prefix_metadata(metadata): + """Add plankton prefix to metadata.""" + return dict([(PLANKTON_PREFIX + k, v) for k, v in metadata.items()]) + + @staticmethod + def _unprefix_metadata(metadata): + """Remove plankton prefix from metadata.""" + return dict([(k.replace(PLANKTON_PREFIX, "", 1), v) + for k, v in metadata.items()]) + + @handle_pithos_backend + def update_metadata(self, uuid, metadata): + location, _m, permissions = self.get_pithos_object(uuid) - def _get_image(self, image_url): - """Get information about an Image. - - Get all available information about an Image. - """ - account, container, name = split_url(image_url) - try: - meta = self._get_meta(image_url) - meta["deleted"] = "" - except NameError: - versions = self.backend.list_versions(self.user, account, - container, name) - if not versions: - raise Exception("Image without versions %s" % image_url) - # Object was deleted, use the latest version - version, timestamp = versions[-1] - meta = self._get_meta(image_url, version) - meta["deleted"] = timestamp - - # XXX: Check that an object is a plankton image! PithosBackend will - # return common metadata for an object, even if it has no metadata in - # plankton domain. All images must have a name, so we check if a file - # is an image by checking if they are having an image name. - if PLANKTON_PREFIX + 'name' not in meta: - raise ImageNotFound - - permissions = self._get_permissions(image_url) - return image_to_dict(image_url, meta, permissions) - - def _get_meta(self, image_url, version=None): - """Get object's metadata.""" - account, container, name = split_url(image_url) - return self.backend.get_object_meta(self.user, account, container, - name, PLANKTON_DOMAIN, version) - - def _update_meta(self, image_url, meta, replace=False): - """Update object's metadata.""" - account, container, name = split_url(image_url) - - prefixed = [(PLANKTON_PREFIX + uenc(k), uenc(v)) - for k, v in meta.items() - if k in PLANKTON_META or k.startswith(PROPERTY_PREFIX)] - prefixed = dict(prefixed) - - for k, v in prefixed.items(): - if len(k) > 128: - raise InvalidMetadata('Metadata keys should be less than %s ' - 'characters' % MAX_META_KEY_LENGTH) - if len(v) > 256: - raise InvalidMetadata('Metadata values should be less than %s ' - 'characters.' % MAX_META_VALUE_LENGTH) - - self.backend.update_object_meta(self.user, account, container, name, - PLANKTON_DOMAIN, prefixed, replace) - logger.debug("User '%s' updated image '%s', meta: '%s'", self.user, - image_url, prefixed) - - def _get_permissions(self, image_url): - """Get object's permissions.""" - account, container, name = split_url(image_url) - _a, path, permissions = \ - self.backend.get_object_permissions(self.user, account, container, - name) - - if path is None and permissions != {}: - logger.warning("Image '%s' got permissions '%s' from 'None' path.", - image_url, permissions) - raise Exception("Database Inconsistency Error:" - " Image '%s' got permissions from 'None' path." % - image_url) - - return permissions - - def _update_permissions(self, image_url, permissions): - """Update object's permissions.""" - account, container, name = split_url(image_url) - self.backend.update_object_permissions(self.user, account, container, - name, permissions) - logger.debug("User '%s' updated image '%s', permissions: '%s'", - self.user, image_url, permissions) + is_public = metadata.pop("is_public", None) + if is_public is not None: + assert(isinstance(is_public, bool)) + read = set(permissions.get("read", [])) + if is_public and "*" not in read: + read.add("*") + elif not is_public and "*" in read: + read.discard("*") + permissions["read"] = list(read) + self._update_permissions(uuid, location, permissions) - @handle_backend_exceptions - @commit_on_success - def unregister(self, image_uuid): - """Unregister an image. + # Each property is stored as a separate prefixed metadata + meta = deepcopy(metadata) + properties = meta.pop("properties", {}) + meta.update(self._prefix_properties(properties)) - Unregister an image, by removing all metadata from the Pithos - file that exist in the PLANKTON_DOMAIN. + self._update_metadata(uuid, location, metadata=meta, replace=False) - """ - image_url = self._get_image_url(image_uuid) - self._get_image(image_url) # Assert that it is an image - # Unregister the image by removing all metadata from domain - # 'PLANKTON_DOMAIN' - meta = {} - self._update_meta(image_url, meta, True) - logger.debug("User '%s' deleted image '%s'", self.user, image_url) + return self._get_image(uuid) - @handle_backend_exceptions - @commit_on_success - def add_user(self, image_uuid, add_user): - """Add a user as an image member. + def _update_metadata(self, uuid, location, metadata, replace=False): + prefixed = self._prefix_and_validate_metadata(metadata) - Update read permissions of Pithos file, to include the specified user. + account, container, path = location + self.backend.update_object_meta(self.user, account, container, path, + PLANKTON_DOMAIN, prefixed, replace) + logger.debug("User '%s' updated image '%s', metadata: '%s'", self.user, + uuid, prefixed) + + def _prefix_and_validate_metadata(self, metadata): + _prefixed_metadata = self._prefix_metadata(metadata) + prefixed = {} + for k, v in _prefixed_metadata.items(): + # Encode to UTF-8 + k, v = smart_unicode(k), smart_unicode(v) + # Check the length of key/value + if len(k) > MAX_META_KEY_LENGTH: + raise faults.BadRequest('Metadata keys should be less than %s' + ' characters' % MAX_META_KEY_LENGTH) + if len(v) > MAX_META_VALUE_LENGTH: + raise faults.BadRequest('Metadata values should be less than' + ' %s characters.' + % MAX_META_VALUE_LENGTH) + prefixed[k] = v + return prefixed + + def get_pithos_object(self, uuid, version=None, check_permissions=True, + check_image=True): + """Get a Pithos object based on its UUID. + + If 'version' is not specified, the latest non-deleted version of this + object will be retrieved. + If 'check_permissions' is set to False, the Pithos backend will not + check if the user has permissions to access this object. + Finally, the 'check_image' option is used to check whether the Pithos + object is an image or not. """ - image_url = self._get_image_url(image_uuid) - self._get_image(image_url) # Assert that it is an image - permissions = self._get_permissions(image_url) + if check_permissions: + user = self.user + else: + user = None + + meta, permissions, path = self.backend.get_object_by_uuid( + uuid=uuid, version=version, domain=PLANKTON_DOMAIN, + user=user, check_permissions=check_permissions) + account, container, path = path.split("/", 2) + location = Location(account, container, path) + + if check_image and PLANKTON_PREFIX + "name" not in meta: + # Check that object is an image by checking if it has an Image name + # in Plankton metadata + raise faults.ItemNotFound("Object '%s' does not exist." % uuid) + + return location, meta, permissions + + # Users and Permissions + @handle_pithos_backend + def add_user(self, uuid, user): + assert(isinstance(user, basestring)) + location, _, permissions = self.get_pithos_object(uuid) read = set(permissions.get("read", [])) - assert(isinstance(add_user, (str, unicode))) - read.add(add_user) - permissions["read"] = list(read) - self._update_permissions(image_url, permissions) - - @handle_backend_exceptions - @commit_on_success - def remove_user(self, image_uuid, remove_user): - """Remove the user from image members. - - Remove the specified user from the read permissions of the Pithos file. + if user not in read: + read.add(user) + permissions["read"] = list(read) + self._update_permissions(uuid, location, permissions) - """ - image_url = self._get_image_url(image_uuid) - self._get_image(image_url) # Assert that it is an image - permissions = self._get_permissions(image_url) + @handle_pithos_backend + def remove_user(self, uuid, user): + assert(isinstance(user, basestring)) + location, _, permissions = self.get_pithos_object(uuid) read = set(permissions.get("read", [])) - assert(isinstance(remove_user, (str, unicode))) - try: - read.remove(remove_user) - except ValueError: - return # TODO: User did not have access - permissions["read"] = list(read) - self._update_permissions(image_url, permissions) - - @handle_backend_exceptions - @commit_on_success - def replace_users(self, image_uuid, replace_users): - """Replace image members. - - Replace the read permissions of the Pithos files with the specified - users. If image is specified as public, we must preserve * permission. - - """ - image_url = self._get_image_url(image_uuid) - image = self._get_image(image_url) - permissions = self._get_permissions(image_url) - assert(isinstance(replace_users, list)) - permissions["read"] = replace_users - if image.get("is_public", False): - permissions["read"].append("*") - self._update_permissions(image_url, permissions) - - @handle_backend_exceptions - @commit_on_success - def list_users(self, image_uuid): - """List the image members. - - List the image members, by listing all users that have read permission - to the corresponding Pithos file. - - """ - image_url = self._get_image_url(image_uuid) - self._get_image(image_url) # Assert that it is an image - permissions = self._get_permissions(image_url) - return [user for user in permissions.get('read', []) if user != '*'] - - @handle_backend_exceptions - @commit_on_success - def update_metadata(self, image_uuid, metadata): - """Update Image metadata.""" - image_url = self._get_image_url(image_uuid) - self._get_image(image_url) # Assert that it is an image - - # 'is_public' metadata is translated in proper file permissions - is_public = metadata.pop("is_public", None) - if is_public is not None: - permissions = self._get_permissions(image_url) - read = set(permissions.get("read", [])) - if is_public: - read.add("*") - else: - read.discard("*") + if user in read: + read.remove(user) permissions["read"] = list(read) - self._update_permissions(image_url, permissions) + self._update_permissions(uuid, location, permissions) - # Extract the properties dictionary from metadata, and store each - # property as a separeted, prefixed metadata - properties = metadata.pop("properties", {}) - meta = dict([(PROPERTY_PREFIX + k, v) for k, v in properties.items()]) - # Also add the following metadata - meta.update(**metadata) + @handle_pithos_backend + def replace_users(self, uuid, users): + assert(isinstance(users, list)) + location, _, permissions = self.get_pithos_object(uuid) + read = set(permissions.get("read", [])) + if "*" in read: # Retain public permissions + users.append("*") + permissions["read"] = list(users) + self._update_permissions(uuid, location, permissions) + + @handle_pithos_backend + def list_users(self, uuid): + location, _, permissions = self.get_pithos_object(uuid) + return [user for user in permissions.get('read', []) if user != '*'] - self._update_meta(image_url, meta) - image_url = self._get_image_url(image_uuid) - return self._get_image(image_url) + def _update_permissions(self, uuid, location, permissions): + account, container, path = location + self.backend.update_object_permissions(self.user, account, container, + path, permissions) + logger.debug("User '%s' updated image '%s' permissions: '%s'", + self.user, uuid, permissions) - @handle_backend_exceptions - @commit_on_success + @handle_pithos_backend def register(self, name, image_url, metadata): # Validate that metadata are allowed if "id" in metadata: - raise ValueError("Passing an ID is not supported") + raise faults.BadRequest("Passing an ID is not supported") store = metadata.pop("store", "pithos") if store != "pithos": - raise ValueError("Invalid store '%s'. Only 'pithos' store is" - "supported" % store) + raise faults.BadRequest("Invalid store '%s'. Only 'pithos' store" + " is supported" % store) disk_format = metadata.setdefault("disk_format", settings.DEFAULT_DISK_FORMAT) if disk_format not in settings.ALLOWED_DISK_FORMATS: - raise ValueError("Invalid disk format '%s'" % disk_format) + raise faults.BadRequest("Invalid disk format '%s'" % disk_format) container_format =\ metadata.setdefault("container_format", settings.DEFAULT_CONTAINER_FORMAT) if container_format not in settings.ALLOWED_CONTAINER_FORMATS: - raise ValueError("Invalid container format '%s'" % - container_format) + raise faults.BadRequest("Invalid container format '%s'" % + container_format) - # Validate that 'size' and 'checksum' are valid - account, container, object = split_url(image_url) + account, container, path = split_url(image_url) + location = Location(account, container, path) + meta = self.backend.get_object_meta(self.user, account, container, + path, PLANKTON_DOMAIN, None) + uuid = meta["uuid"] - meta = self._get_meta(image_url) - - size = int(metadata.pop('size', meta['bytes'])) - if size != meta['bytes']: - raise ValueError("Invalid size") + # Validate that 'size' and 'checksum' + size = metadata.pop('size', int(meta['bytes'])) + if not isinstance(size, int) or int(size) != int(meta["bytes"]): + raise faults.BadRequest("Invalid 'size' field") checksum = metadata.pop('checksum', meta['hash']) - if checksum != meta['hash']: - raise ValueError("Invalid checksum") + if not isinstance(checksum, basestring) or checksum != meta['hash']: + raise faults.BadRequest("Invalid checksum field") + + users = [self.user] + public = metadata.pop("is_public", False) + if not isinstance(public, bool): + raise faults.BadRequest("Invalid value for 'is_public' metadata") + if public: + users.append("*") + permissions = {'read': users} + self._update_permissions(uuid, location, permissions) + + # Each property is stored as a separate prefixed metadata + meta = deepcopy(metadata) + properties = meta.pop("properties", {}) + meta.update(self._prefix_properties(properties)) + # Add extra metadata + meta["name"] = name + meta['created_at'] = str(time()) + self._update_metadata(uuid, location, metadata=meta, replace=False) + + logger.debug("User '%s' registered image '%s'('%s')", self.user, + uuid, location) + return self._get_image(uuid) + + @handle_pithos_backend + def unregister(self, uuid): + """Unregister an Image. + + Unregister an Image by removing all the metadata in the Plankton + domain. The Pithos file is not deleted. - # Fix permissions - is_public = metadata.pop('is_public', False) - if is_public: - permissions = {'read': ['*']} - else: - permissions = {'read': [self.user]} - - # Extract the properties dictionary from metadata, and store each - # property as a separeted, prefixed metadata - properties = metadata.pop("properties", {}) - meta = dict([(PROPERTY_PREFIX + k, v) for k, v in properties.items()]) - # Add creation(register) timestamp as a metadata, to avoid extra - # queries when retrieving the list of images. - meta['created_at'] = time() - # Update rest metadata - meta.update(name=name, status='available', **metadata) - - # Do the actualy update in the Pithos backend - self._update_meta(image_url, meta) - self._update_permissions(image_url, permissions) - logger.debug("User '%s' created image '%s'('%s')", self.user, - image_url, uenc(name)) - return self._get_image(image_url) - - def _list_images(self, user=None, filters=None, params=None): + """ + location, _m, _p = self.get_pithos_object(uuid) + self._update_metadata(uuid, location, metadata={}, replace=True) + logger.debug("User '%s' unregistered image '%s'", self.user, uuid) + + # List functions + def _list_images(self, user=None, filters=None, params=None, + check_permissions=True): filters = filters or {} # TODO: Use filters @@ -441,92 +409,145 @@ class ImageBackend(object): # size_range = (size_range[0], val) # else: # keys.append('%s = %s' % (PLANKTON_PREFIX + key, val)) - _images = self.backend.get_domain_objects(domain=PLANKTON_DOMAIN, - user=user) + _images = self.backend.get_domain_objects( + domain=PLANKTON_DOMAIN, user=user, + check_permissions=check_permissions) images = [] - for (location, meta, permissions) in _images: - image_url = "pithos://" + location - meta["modified"] = meta["version_timestamp"] - images.append(image_to_dict(image_url, meta, permissions)) + for (location, metadata, permissions) in _images: + location = Location(*location.split("/", 2)) + images.append(image_to_dict(location, metadata, permissions)) if params is None: params = {} + key = itemgetter(params.get('sort_key', 'created_at')) reverse = params.get('sort_dir', 'desc') == 'desc' images.sort(key=key, reverse=reverse) return images - @commit_on_success - def list_images(self, filters=None, params=None): + @handle_pithos_backend + def list_images(self, filters=None, params=None, check_permissions=True): return self._list_images(user=self.user, filters=filters, - params=params) + params=params, + check_permissions=check_permissions) - @commit_on_success + @handle_pithos_backend def list_shared_images(self, member, filters=None, params=None): images = self._list_images(user=self.user, filters=filters, params=params) is_shared = lambda img: not img["is_public"] and img["owner"] == member return filter(is_shared, images) - @commit_on_success + @handle_pithos_backend def list_public_images(self, filters=None, params=None): images = self._list_images(user=None, filters=filters, params=params) return filter(lambda img: img["is_public"], images) + # Snapshots + @handle_pithos_backend + def register_snapshot(self, name, mapfile, size, metadata): + metadata = self._prefix_and_validate_metadata(metadata) + snapshot_id = self.backend.register_object_map( + user=self.user, + account=self.user, + container=SNAPSHOTS_CONTAINER, + name=name, + mapfile=mapfile, + size=size, + domain=PLANKTON_DOMAIN, + type=SNAPSHOTS_TYPE, + meta=metadata, + replace_meta=True, + permissions=None) + return snapshot_id + + def list_snapshots(self, user=None, check_permissions=True): + _snapshots = self.list_images(check_permissions=check_permissions) + return [s for s in _snapshots if s["is_snapshot"]] + + @handle_pithos_backend + def get_snapshot(self, snapshot_uuid, check_permissions=True): + snap = self._get_image(snapshot_uuid, + check_permissions=check_permissions) + if snap.get("is_snapshot", False) is False: + raise faults.ItemNotFound("Snapshots '%s' does not exist" % + snapshot_uuid) + return snap + + @handle_pithos_backend + def delete_snapshot(self, snapshot_uuid): + self.backend.delete_by_uuid(self.user, snapshot_uuid) + + @handle_pithos_backend + def update_snapshot_state(self, snapshot_id, state): + state = OBJ_TO_MAP_STATES[state] + self.backend.update_object_status(snapshot_id, state=state) -class ImageBackendError(Exception): - pass - - -class ImageNotFound(ImageBackendError): - pass - -class Forbidden(ImageBackendError): - pass - - -class InvalidMetadata(ImageBackendError): - pass +def create_url(account, container, name): + """Create a Pithos URL from the object info""" + assert "/" not in account, "Invalid account" + assert "/" not in container, "Invalid container" + account = quote(smart_str(account, encoding="utf-8")) + container = quote(smart_str(container, encoding="utf-8")) + name = quote(smart_str(name, encoding="utf-8")) + return "pithos://%s/%s/%s" % (account, container, name) -class InvalidLocation(ImageBackendError): - pass +def split_url(url): + """Get object info from the Pithos URL""" + assert(isinstance(url, basestring)) + t = url.split('/', 4) + assert t[0] == "pithos:", "Invalid url" + assert len(t) == 5, "Invalid url" + account, container, name = t[2:5] + parse = lambda x: smart_unicode(unquote(x), encoding="utf-8") + return parse(account), parse(container), parse(name) -def image_to_dict(image_url, meta, permissions): +def image_to_dict(location, metadata, permissions): """Render an image to a dictionary""" - account, container, name = split_url(image_url) + account, container, name = location image = {} - if PLANKTON_PREFIX + 'name' not in meta: - logger.warning("Image without Plankton name!! url %s meta %s", - image_url, meta) - image[PLANKTON_PREFIX + "name"] = "" - - image["id"] = meta["uuid"] - image["location"] = image_url - image["checksum"] = meta["hash"] - created = meta.get("created_at", meta["modified"]) - image["created_at"] = format_timestamp(created) - deleted = meta.get("deleted", None) - image["deleted_at"] = format_timestamp(deleted) if deleted else "" - image["updated_at"] = format_timestamp(meta["modified"]) - image["size"] = meta["bytes"] - image["store"] = "pithos" + image["id"] = metadata["uuid"] + image["mapfile"] = metadata["mapfile"] + image["checksum"] = metadata["hash"] + image["location"] = create_url(account, container, name) + image["size"] = metadata["bytes"] image['owner'] = account + image["store"] = u"pithos" + image["is_snapshot"] = metadata["is_snapshot"] + image["version"] = metadata["version"] + + image["status"] = MAP_TO_OBJ_STATES.get(metadata.get("available"), + "UNKNOWN") # Permissions - image["is_public"] = "*" in permissions.get('read', []) + users = list(permissions.get("read", [])) + image["is_public"] = "*" in users + image["users"] = [u for u in users if u != "*"] + # Timestamps + updated_at = metadata["version_timestamp"] + created_at = metadata.get("created_at", updated_at) + image["created_at"] = format_timestamp(created_at) + image["updated_at"] = format_timestamp(updated_at) + if metadata.get("deleted", False): + image["deleted_at"] = image["updated_at"] + else: + image["deleted_at"] = "" + # Ganeti ID and job ID to be used for snapshot reconciliation + image["backend_info"] = metadata.pop(PLANKTON_PREFIX + "backend_info", + None) properties = {} - for key, val in meta.items(): + for key, val in metadata.items(): # Get plankton properties if key.startswith(PLANKTON_PREFIX): # Remove plankton prefix key = key.replace(PLANKTON_PREFIX, "") - # Keep only those in plankton meta + # Keep only those in plankton metadata if key in PLANKTON_META: if key != "created_at": # created timestamp is return in 'created_at' field @@ -534,6 +555,7 @@ def image_to_dict(image_url, meta, permissions): elif key.startswith(PROPERTY_PREFIX): key = key.replace(PROPERTY_PREFIX, "") properties[key] = val + image["properties"] = properties return image @@ -583,7 +605,7 @@ def get_backend(): backend_module = getattr(settings, 'PLANKTON_BACKEND_MODULE', None) if not backend_module: # no setting set - return ImageBackend + return PlanktonBackend parts = backend_module.split(".") module = ".".join(parts[:-1]) diff --git a/snf-cyclades-app/synnefo/plankton/management/commands/image-list.py b/snf-cyclades-app/synnefo/plankton/management/commands/image-list.py index d845d058f1799d1a6e0cf05042b6df310b070f96..2df569a84cb2c1cf4b4c1f18bca80160642613bf 100644 --- a/snf-cyclades-app/synnefo/plankton/management/commands/image-list.py +++ b/snf-cyclades-app/synnefo/plankton/management/commands/image-list.py @@ -1,63 +1,55 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of GRNET S.A. -# - -from django.core.management.base import BaseCommand from optparse import make_option +from snf_django.management.commands import SynnefoCommand from snf_django.management.utils import pprint_table -from synnefo.plankton.utils import image_backend +from synnefo.plankton.backend import PlanktonBackend -class Command(BaseCommand): - help = "List public images or images available to a user." - option_list = BaseCommand.option_list + ( +class Command(SynnefoCommand): + help = "List images." + option_list = SynnefoCommand.option_list + ( make_option( - '--user-id', + '--user', dest='userid', default=None, - help="List all images available to that user." - " If no user is specified, only public images" - " are displayed."), + help="List only images that are available to this user."), + make_option( + '--public', + dest='public', + action="store_true", + default=False, + help="List only public images."), ) def handle(self, **options): user = options['userid'] + check_perm = user is not None - with image_backend(user) as backend: - images = backend._list_images(user) + with PlanktonBackend(user) as backend: + images = backend.list_images(user, check_permissions=check_perm) + if options["public"]: + images = filter(lambda x: x['is_public'], images) images.sort(key=lambda x: x['created_at'], reverse=True) - headers = ("id", "name", "owner", "public") + headers = ("id", "name", "user.uuid", "public", "snapshot") table = [] for img in images: fields = (img["id"], img["name"], img["owner"], - str(img["is_public"])) + str(img["is_public"]), str(img["is_snapshot"])) table.append(fields) pprint_table(self.stdout, table, headers) diff --git a/snf-cyclades-app/synnefo/plankton/management/commands/image-show.py b/snf-cyclades-app/synnefo/plankton/management/commands/image-show.py index d14734f523b0d0048f960cf1ee9b692caf84f863..ada40d4412a3b5e0d492a65ed5f5099575e0e9d5 100644 --- a/snf-cyclades-app/synnefo/plankton/management/commands/image-show.py +++ b/snf-cyclades-app/synnefo/plankton/management/commands/image-show.py @@ -1,54 +1,44 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import CommandError -from synnefo.plankton.utils import image_backend +from snf_django.management.commands import SynnefoCommand +from synnefo.plankton.backend import PlanktonBackend +from synnefo.management import common from snf_django.management import utils -class Command(BaseCommand): +class Command(SynnefoCommand): args = "<image_id>" help = "Display available information about an image" + @common.convert_api_faults def handle(self, *args, **options): if len(args) != 1: raise CommandError("Please provide an image ID") image_id = args[0] - with image_backend(None) as backend: - images = backend._list_images(None) - try: - image = filter(lambda x: x["id"] == image_id, images)[0] - except IndexError: - raise CommandError("Image not found. Use snf-manage image-list" - " to get the list of all images.") - utils.pprint_table(out=self.stdout, table=[image.values()], headers=image.keys(), vertical=True) + try: + with PlanktonBackend(None) as backend: + image = backend.get_image(image_id, check_permissions=False) + except: + raise CommandError("An error occurred, verify that image or " + "user ID are valid") + + utils.pprint_table(out=self.stdout, table=[image.values()], + headers=image.keys(), vertical=True) diff --git a/snf-cyclades-app/synnefo/plankton/tests.py b/snf-cyclades-app/synnefo/plankton/tests.py index a90370abf82329872565cd6f80e3f83872012fa2..0d84c02a480db9f350885b4c799f9f2848188ae3 100644 --- a/snf-cyclades-app/synnefo/plankton/tests.py +++ b/snf-cyclades-app/synnefo/plankton/tests.py @@ -1,145 +1,33 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import json +import urllib from mock import patch from functools import wraps from copy import deepcopy +from decimal import Decimal from snf_django.utils.testing import BaseAPITest from synnefo.cyclades_settings import cyclades_services from synnefo.lib.services import get_service_path from synnefo.lib import join_urls - -class PlanktonAPITest(BaseAPITest): - def setUp(self, *args, **kwargs): - super(PlanktonAPITest, self).setUp(*args, **kwargs) - self.api_path = get_service_path(cyclades_services, 'image', - version='v1.0') - def myget(self, path, *args, **kwargs): - path = join_urls(self.api_path, path) - return self.get(path, *args, **kwargs) - - def myput(self, path, *args, **kwargs): - path = join_urls(self.api_path, path) - return self.put(path, *args, **kwargs) - - def mypost(self, path, *args, **kwargs): - path = join_urls(self.api_path, path) - return self.post(path, *args, **kwargs) - - def mydelete(self, path, *args, **kwargs): - path = join_urls(self.api_path, path) - return self.delete(path, *args, **kwargs) - - -FILTERS = ('name', 'container_format', 'disk_format', 'status', 'size_min', - 'size_max') -PARAMS = ('sort_key', 'sort_dir') -SORT_KEY_OPTIONS = ('id', 'name', 'status', 'size', 'disk_format', - 'container_format', 'created_at', 'updated_at') -SORT_DIR_OPTIONS = ('asc', 'desc') -LIST_FIELDS = ('status', 'name', 'disk_format', 'container_format', 'size', - 'id') -DETAIL_FIELDS = ('name', 'disk_format', 'container_format', 'size', 'checksum', - 'location', 'created_at', 'updated_at', 'deleted_at', - 'status', 'is_public', 'owner', 'properties', 'id') -ADD_FIELDS = ('name', 'id', 'store', 'disk_format', 'container_format', 'size', - 'checksum', 'is_public', 'owner', 'properties', 'location') -UPDATE_FIELDS = ('name', 'disk_format', 'container_format', 'is_public', - 'owner', 'properties', 'status') - - -DummyImages = { - '0786a349-9725-48ec-8b86-8598eefc4043': - {'checksum': u'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', - u'container_format': u'bare', - 'created_at': '2012-12-04 09:50:20', - 'deleted_at': '', - u'disk_format': u'diskdump', - 'id': u'0786a349-9725-48ec-8b86-8598eefc4043', - 'is_public': True, - 'location': u'pithos://foo@example.com/container/foo3', - u'name': u'dummyname', - 'owner': u'foo@example.com', - 'properties': {}, - 'size': 500L, - u'status': u'available', - 'store': 'pithos', - 'updated_at': '2012-12-04 09:50:54'}, - - 'd8aa85b8-410b-4550-953d-6797572534e6': - {'checksum': u'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', - u'container_format': u'bare', - 'created_at': '2012-11-26 11:56:42', - 'deleted_at': '', - u'disk_format': u'diskdump', - 'id': u'd8aa85b8-410b-4550-953d-6797572534e6', - 'is_public': False, - 'location': u'pithos://foo@example.com/container/private', - u'name': u'dummyname2', - 'owner': u'foo@example.com', - 'properties': {}, - 'size': 10000L, - u'status': u'available', - 'store': 'pithos', - 'updated_at': '2012-11-26 11:57:09'}, - - '264fb9ac-2458-421c-b460-6a765a92825c': - {'checksum': u'0c6d0586744781218672fff2d7ed94cc32efb02a6a8eb589a0628f0e22bd5a7f', - u'container_format': u'bare', - 'created_at': '2012-11-26 11:52:54', - 'deleted_at': '', - u'disk_format': u'diskdump', - 'id': u'264fb9ac-2458-421c-b460-6a765a92825c', - 'is_public': True, - 'location': u'pithos://foo@example.com/container/baz.diskdump', - u'name': u'"dummyname3"', - 'owner': u'foo@example.com', - 'properties': {u'description': u'Debian Squeeze Base System', - u'gui': u'No GUI', - u'kernel': u'2.6.32', - u'os': u'debian', - u'osfamily': u'linux', - u'root_partition': u'1', - u'size': u'451', - u'sortorder': u'1', - u'users': u'root'}, - 'size': 473772032L, - u'status': u'available', - 'store': 'pithos', - 'updated_at': '2012-11-26 11:55:40'}} +PLANKTON_URL = get_service_path(cyclades_services, 'image', + version='v1.0') +IMAGES_URL = join_urls(PLANKTON_URL, "images/") def assert_backend_closed(func): @@ -152,121 +40,285 @@ def assert_backend_closed(func): return wrapper -@patch("synnefo.plankton.backend.ImageBackend") -class PlanktonTest(PlanktonAPITest): - @assert_backend_closed - def test_list_images(self, backend): - backend.return_value.list_images.return_value =\ - deepcopy(DummyImages).values() - response = self.myget("images/") - self.assertSuccess(response) - images = json.loads(response.content) - for api_image in images: - id = api_image['id'] - pithos_image = dict([(key, val)\ - for key, val in DummyImages[id].items()\ - if key in LIST_FIELDS]) - self.assertEqual(api_image, pithos_image) - backend.return_value\ - .list_images.assert_called_once_with({}, {'sort_key': 'created_at', - 'sort_dir': 'desc'}) - - @assert_backend_closed - def test_list_images_detail(self, backend): - backend.return_value.list_images.return_value =\ - deepcopy(DummyImages).values() - response = self.myget("images/detail") - self.assertSuccess(response) - images = json.loads(response.content) - for api_image in images: - id = api_image['id'] - pithos_image = dict([(key, val)\ - for key, val in DummyImages[id].items()\ - if key in DETAIL_FIELDS]) - self.assertEqual(api_image, pithos_image) - backend.return_value\ - .list_images.assert_called_once_with({}, {'sort_key': 'created_at', - 'sort_dir': 'desc'}) +@patch("synnefo.plankton.backend.get_pithos_backend") +class PlanktonTest(BaseAPITest): + def test_register_image(self, backend): + required = { + "HTTP_X_IMAGE_META_NAME": u"TestImage\u2602", + "HTTP_X_IMAGE_META_LOCATION": "pithos://4321-4321/%E2%98%82/foo"} + # Check valid name + headers = deepcopy(required) + headers.pop("HTTP_X_IMAGE_META_NAME") + response = self.post(IMAGES_URL, **headers) + self.assertBadRequest(response) + self.assertTrue("name" in response.content) + headers["HTTP_X_IMAGE_META_NAME"] = "" + response = self.post(IMAGES_URL, **headers) + self.assertBadRequest(response) + self.assertTrue("name" in response.content) + # Check valid location + headers = deepcopy(required) + headers.pop("HTTP_X_IMAGE_META_LOCATION") + response = self.post(IMAGES_URL, **headers) + self.assertBadRequest(response) + self.assertTrue("location" in response.content) + headers["HTTP_X_IMAGE_META_LOCATION"] = "" + response = self.post(IMAGES_URL, **headers) + self.assertBadRequest(response) + self.assertTrue("location" in response.content) + headers["HTTP_X_IMAGE_META_LOCATION"] = "pitho://4321-4321/images/foo" + response = self.post(IMAGES_URL, **headers) + self.assertBadRequest(response) + self.assertTrue("location" in response.content) + headers["HTTP_X_IMAGE_META_LOCATION"] = "pithos://4321-4321/foo" + response = self.post(IMAGES_URL, **headers) + self.assertBadRequest(response) + self.assertTrue("location" in response.content) + # ID not supported + headers = deepcopy(required) + headers["HTTP_X_IMAGE_META_ID"] = "1234" + response = self.post(IMAGES_URL, **headers) + self.assertBadRequest(response) + headers = deepcopy(required) + # ID not supported + headers = deepcopy(required) + headers["HTTP_X_IMAGE_META_LOLO"] = "1234" + response = self.post(IMAGES_URL, **headers) + self.assertBadRequest(response) + headers = deepcopy(required) + headers["HTTP_X_IMAGE_META_STORE"] = "pitho" + response = self.post(IMAGES_URL, **headers) + self.assertBadRequest(response) + self.assertTrue("store " in response.content) + headers = deepcopy(required) + headers["HTTP_X_IMAGE_META_DISK_FORMAT"] = "diskdumpp" + response = self.post(IMAGES_URL, **headers) + self.assertBadRequest(response) + self.assertTrue("disk format" in response.content) + headers = deepcopy(required) + headers["HTTP_X_IMAGE_META_CONTAINER_FORMAT"] = "baree" + response = self.post(IMAGES_URL, **headers) + self.assertBadRequest(response) + self.assertTrue("container format" in response.content) - @assert_backend_closed - def test_list_images_filters(self, backend): - backend.return_value.list_images.return_value =\ - deepcopy(DummyImages).values() - response = self.myget("images/?size_max=1000") - self.assertSuccess(response) - backend.return_value\ - .list_images.assert_called_once_with({'size_max': 1000}, - {'sort_key': 'created_at', - 'sort_dir': 'desc'}) + backend().get_object_meta.return_value = {"uuid": "1234-1234-1234", + "bytes": 42, + "is_snapshot": True, + "hash": "unique_mapfile", + "is_snapshot": True, + "mapfile": "unique_mapfile"} + headers = deepcopy(required) + headers["HTTP_X_IMAGE_META_SIZE"] = "foo" + response = self.post(IMAGES_URL, **headers) + self.assertBadRequest(response) + self.assertTrue("size" in response.content) + headers["HTTP_X_IMAGE_META_SIZE"] = "43" + response = self.post(IMAGES_URL, **headers) + self.assertBadRequest(response) + self.assertTrue("size" in response.content) - @assert_backend_closed - def test_list_images_filters_error_1(self, backend): - response = self.myget("images/?size_max=") + # Unicode Error: + headers["HTTP_X_IMAGE_META_NAME"] = "\xc2" + response = self.post(IMAGES_URL, **headers) self.assertBadRequest(response) + headers["HTTP_X_IMAGE_META_NAME"] = u"TestImage\u2602" - @assert_backend_closed - def test_list_images_filters_error_2(self, backend): - response = self.myget("images/?size_min=foo") + headers["HTTP_X_IMAGE_META_SIZE"] = 42 + headers["HTTP_X_IMAGE_META_CHECKSUM"] = "wrong_checksum" + response = self.post(IMAGES_URL, **headers) self.assertBadRequest(response) - @assert_backend_closed - def test_update_image(self, backend): - db_image = DummyImages.values()[0] - response = self.myput("images/%s" % db_image['id'], - json.dumps({}), - 'json', HTTP_X_IMAGE_META_OWNER='user2') + backend().get_object_by_uuid.return_value = ( + {"uuid": "1234-1234-1234", + "bytes": 42, + "mapfile": "unique_mapfile", + "is_snapshot": True, + "hash": "unique_mapfile", + "version": 42, + 'version_timestamp': Decimal('1392487853.863673'), + "plankton:name": u"TestImage\u2602", + "plankton:container_format": "bare", + "plankton:disk_format": "diskdump", + "plankton:status": u"AVAILABLE"}, + {"read": []}, + u"4321-4321/\u2602/foo", + ) + headers = deepcopy(required) + response = self.post(IMAGES_URL, **headers) self.assertSuccess(response) - backend.return_value.update_metadata.assert_called_once_with(db_image['id'], - {"owner": "user2"}) + self.assertEqual(response["x-image-meta-location"], + "pithos://4321-4321/%E2%98%82/foo") + self.assertEqual(response["x-image-meta-id"], "1234-1234-1234") + self.assertEqual(response["x-image-meta-status"], "AVAILABLE") + self.assertEqual(response["x-image-meta-deleted-at"], "") + self.assertEqual(response["x-image-meta-is-public"], "False") + self.assertEqual(response["x-image-meta-owner"], "4321-4321") + self.assertEqual(response["x-image-meta-size"], "42") + self.assertEqual(response["x-image-meta-checksum"], "unique_mapfile") + self.assertEqual(urllib.unquote(response["x-image-meta-name"]), + u"TestImage\u2602".encode("utf-8")) + self.assertEqual(response["x-image-meta-container-format"], "bare") + self.assertEqual(response["x-image-meta-disk-format"], "diskdump") + self.assertEqual(response["x-image-meta-created-at"], + "2014-02-15 18:10:53") + self.assertEqual(response["x-image-meta-updated-at"], + "2014-02-15 18:10:53") - @assert_backend_closed - def test_add_image_member(self, backend): - image_id = DummyImages.values()[0]['id'] - response = self.myput("images/%s/members/user3" % image_id, - json.dumps({}), 'json') + # Extra headers,properties + backend().get_object_by_uuid.return_value = ( + {"uuid": "1234-1234-1234", + "bytes": 42, + "is_snapshot": True, + "hash": "unique_mapfile", + "mapfile": "unique_mapfile", + "version": 42, + 'version_timestamp': Decimal('1392487853.863673'), + "plankton:name": u"TestImage\u2602", + "plankton:container_format": "bare", + "plankton:disk_format": "diskdump", + "plankton:status": u"AVAILABLE"}, + {"read": []}, + u"4321-4321/\u2602/foo", + ) + headers = deepcopy(required) + headers["HTTP_X_IMAGE_META_IS_PUBLIC"] = True + headers["HTTP_X_IMAGE_META_PROPERTY_KEY1"] = "val1" + headers["HTTP_X_IMAGE_META_PROPERTY_KEY2"] = u"\u2601" + response = self.post(IMAGES_URL, **headers) + name, args, kwargs = backend().update_object_meta.mock_calls[-1] + metadata = args[5] + self.assertEqual(metadata["plankton:property:key1"], "val1") + self.assertEqual(metadata["plankton:property:key2"], u"\u2601") self.assertSuccess(response) - backend.return_value.add_user.assert_called_once_with(image_id, - 'user3') - @assert_backend_closed - def test_remove_image_member(self, backend): - image_id = DummyImages.values()[0]['id'] - response = self.mydelete("images/%s/members/user3" % image_id) + def test_unregister_image(self, backend): + backend().get_object_by_uuid.return_value = ( + {"uuid": "img_uuid", + "bytes": 42, + "plankton:name": "test"}, + {"read": []}, + "img_owner/images/foo" + ) + response = self.delete(join_urls(IMAGES_URL, "img_uuid")) + self.assertEqual(response.status_code, 204) + backend().update_object_meta.assert_called_once_with( + "user", "img_owner", "images", "foo", "plankton", {}, True) + + def test_users(self, backend): + """Test adding/removing and replacing image members""" + # Add user + backend.reset_mock() + backend().get_object_by_uuid.return_value = ( + {"uuid": "img_uuid", + "bytes": 42, + "plankton:name": "test"}, + {"read": []}, + "img_owner/images/foo") + response = self.put(join_urls(IMAGES_URL, "img_uuid/members/user1"), + user="user1") self.assertSuccess(response) - backend.return_value.remove_user.assert_called_once_with(image_id, - 'user3') + backend().update_object_permissions.assert_called_once_with( + "user1", "img_owner", "images", "foo", {"read": ["user1"]}) - @assert_backend_closed - def test_add_image(self, backend): - location = "pithos://uuid/container/name/" - response = self.mypost("images/", - json.dumps({}), - 'json', - HTTP_X_IMAGE_META_NAME='dummy_name', - HTTP_X_IMAGE_META_OWNER='dummy_owner', - HTTP_X_IMAGE_META_LOCATION=location) + # Remove user + backend().update_object_permissions.reset_mock() + backend().get_object_by_uuid.return_value = ( + {"uuid": "img_uuid", + "bytes": 42, + "plankton:name": "test"}, + {"read": ["user1"]}, + "img_owner/images/foo") + response = self.delete(join_urls(IMAGES_URL, "img_uuid/members/user1"), + user="user1") self.assertSuccess(response) - backend.return_value.register.assert_called_once_with('dummy_name', - location, - {'owner': 'dummy_owner'}) + backend().update_object_permissions.assert_called_once_with( + "user1", "img_owner", "images", "foo", {"read": []}) - @assert_backend_closed - def test_get_image(self, backend): - response = self.myget("images/123") - self.assertEqual(response.status_code, 501) + # Update users + backend().get_object_by_uuid.return_value = ( + {"uuid": "img_uuid", + "bytes": 42, + "plankton:name": "test"}, + {"read": ["user1", "user2", "user3"]}, + "img_owner/images/foo") + backend().update_object_permissions.reset_mock() + response = self.put(join_urls(IMAGES_URL, "img_uuid/members"), + params=json.dumps({"memberships": + [{"member_id": "foo1"}, + {"member_id": "foo2"}]}), + ctype="json", + user="user1") + self.assertSuccess(response) + backend().update_object_permissions.assert_called_once_with( + "user1", "img_owner", "images", "foo", {"read": ["foo1", "foo2"]}) - @assert_backend_closed - def test_delete_image(self, backend): - response = self.mydelete("images/123") - self.assertEqual(response.status_code, 204) - backend.return_value.unregister.assert_called_once_with('123') - backend.return_value._delete.assert_not_called() + # List users + backend().get_object_by_uuid.return_value = ( + {"uuid": "img_uuid", + "bytes": 42, + "plankton:name": "test"}, + {"read": ["user1", "user2", "user3"]}, + "img_owner/images/foo", + ) + response = self.get(join_urls(IMAGES_URL, "img_uuid/members")) + self.assertSuccess(response) + res_members = [{"member_id": m, "can_share": False} + for m in ["user1", "user2", "user3"]] + self.assertEqual(json.loads(response.content)["members"], res_members) + + def test_metadata(self, backend): + backend().get_object_by_uuid.return_value = ( + {"uuid": "img_uuid", + "bytes": 42, + "is_snapshot": True, + "hash": "unique_mapfile", + "mapfile": "unique_mapfile", + "version": 42, + 'version_timestamp': Decimal('1392487853.863673'), + "plankton:name": u"TestImage\u2602", + "plankton:container_format": "bare", + "plankton:disk_format": "diskdump", + "plankton:status": u"AVAILABLE"}, + {"read": ["*", "user1"]}, + "img_owner/images/foo/foo1/foo2/foo3", + ) + response = self.head(join_urls(IMAGES_URL, "img_uuid2")) + self.assertSuccess(response) + self.assertEqual(response["x-image-meta-location"], + "pithos://img_owner/images/foo/foo1/foo2/foo3") + self.assertEqual(response["x-image-meta-id"], "img_uuid") + self.assertEqual(response["x-image-meta-status"], "AVAILABLE") + self.assertEqual(response["x-image-meta-deleted-at"], "") + self.assertEqual(response["x-image-meta-is-public"], "True") + self.assertEqual(response["x-image-meta-owner"], "img_owner") + self.assertEqual(response["x-image-meta-size"], "42") + self.assertEqual(response["x-image-meta-checksum"], "unique_mapfile") + self.assertEqual(urllib.unquote(response["x-image-meta-name"]), + u"TestImage\u2602".encode("utf-8")) + self.assertEqual(response["x-image-meta-container-format"], "bare") + self.assertEqual(response["x-image-meta-disk-format"], "diskdump") + self.assertEqual(response["x-image-meta-created-at"], + "2014-02-15 18:10:53") + self.assertEqual(response["x-image-meta-updated-at"], + "2014-02-15 18:10:53") + response = self.head(join_urls(IMAGES_URL, "img_uuid2")) + + headers = {"HTTP_X_IMAGE_META_IS_PUBLIC": False, + "HTTP_X_IMAGE_META_PROPERTY_KEY1": "Val1"} + response = self.put(join_urls(IMAGES_URL, "img_uuid"), **headers) + self.assertSuccess(response) + backend().update_object_permissions.assert_called_once_with( + "user", "img_owner", "images", "foo/foo1/foo2/foo3", + {"read": ["user1"]}) - @assert_backend_closed def test_catch_wrong_api_paths(self, *args): - response = self.myget('nonexistent') + response = self.get(join_urls(PLANKTON_URL, 'nonexistent')) self.assertEqual(response.status_code, 400) try: - error = json.loads(response.content) + json.loads(response.content) except ValueError: self.assertTrue(False) + + def test_list_images_filters_error_1(self, backend): + response = self.get(join_urls(IMAGES_URL, "?size_max=")) + self.assertBadRequest(response) diff --git a/snf-cyclades-app/synnefo/plankton/urls.py b/snf-cyclades-app/synnefo/plankton/urls.py index 27626a469d3e5b6ffba0426b5758b359c29d5e44..678a5e006aee90796e67aac16c3aadb8284168d8 100644 --- a/snf-cyclades-app/synnefo/plankton/urls.py +++ b/snf-cyclades-app/synnefo/plankton/urls.py @@ -1,35 +1,17 @@ -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.conf.urls import patterns, include diff --git a/snf-cyclades-app/synnefo/plankton/utils.py b/snf-cyclades-app/synnefo/plankton/utils.py deleted file mode 100644 index 2e52f77bbcfe0226f2d8add8ec90b6f4eba00842..0000000000000000000000000000000000000000 --- a/snf-cyclades-app/synnefo/plankton/utils.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright 2011-2013 GRNET S.A. All rights reserved. -# -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: -# -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. -# -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. - -from contextlib import contextmanager -from synnefo.plankton import backend -from snf_django.lib.api import faults - - -@contextmanager -def image_backend(user_id): - """Context manager for ImageBackend. - - Context manager for using ImageBackend in API methods. Handles - opening and closing a connection to Pithos and converting backend - erros to cloud faults. - - """ - image_backend = backend.get_backend()(user_id) - try: - yield image_backend - except backend.Forbidden as e: - raise faults.Forbidden - except backend.ImageNotFound: - raise faults.ItemNotFound - except backend.InvalidMetadata as e: - raise faults.BadRequest(str(e)) - finally: - image_backend.close() diff --git a/snf-cyclades-app/synnefo/plankton/views.py b/snf-cyclades-app/synnefo/plankton/views.py index 2c04b859980676f60447ebc419cd01d1e76e652f..b72c76e9d1f46d5e8dfbdd04d34ec33ac7a7ecf7 100644 --- a/snf-cyclades-app/synnefo/plankton/views.py +++ b/snf-cyclades-app/synnefo/plankton/views.py @@ -1,50 +1,34 @@ -# Copyright 2011-2014 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import json from logging import getLogger from string import punctuation -from urllib import unquote +from urllib import unquote, quote from django.conf import settings from django.http import HttpResponse +from django.utils.encoding import (smart_unicode, smart_str, + DjangoUnicodeDecodeError) from snf_django.lib import api from snf_django.lib.api import faults -from synnefo.util.text import uenc -from synnefo.plankton.utils import image_backend -from synnefo.plankton.backend import split_url, InvalidLocation +from synnefo.plankton.backend import (PlanktonBackend, OBJECT_AVAILABLE, + OBJECT_UNAVAILABLE, OBJECT_ERROR) +from synnefo.plankton.backend import split_url FILTERS = ('name', 'container_format', 'disk_format', 'status', 'size_min', @@ -62,7 +46,10 @@ LIST_FIELDS = ('status', 'name', 'disk_format', 'container_format', 'size', DETAIL_FIELDS = ('name', 'disk_format', 'container_format', 'size', 'checksum', 'location', 'created_at', 'updated_at', 'deleted_at', - 'status', 'is_public', 'owner', 'properties', 'id') + 'status', 'is_public', 'owner', 'properties', 'id', + 'is_snapshot', 'description') + +PLANKTON_FIELDS = DETAIL_FIELDS + ('store',) ADD_FIELDS = ('name', 'id', 'store', 'disk_format', 'container_format', 'size', 'checksum', 'is_public', 'owner', 'properties', 'location') @@ -77,51 +64,92 @@ CONTAINER_FORMATS = ('aki', 'ari', 'ami', 'bare', 'ovf') STORE_TYPES = ('pithos') +META_PREFIX = 'HTTP_X_IMAGE_META_' +META_PREFIX_LEN = len(META_PREFIX) +META_PROPERTY_PREFIX = 'HTTP_X_IMAGE_META_PROPERTY_' +META_PROPERTY_PREFIX_LEN = len(META_PROPERTY_PREFIX) + + log = getLogger('synnefo.plankton') +API_STATUS_FROM_IMAGE_STATUS = { + OBJECT_AVAILABLE: "AVAILABLE", + OBJECT_UNAVAILABLE: "SAVING", + OBJECT_ERROR: "ERROR", + "DELETED": "DELETED"} # Unused status + + def _create_image_response(image): + """Encode the image parameters to HTTP Response Headers. + + This function converts all image parameters to HTTP response headers. + All parameters are 'utf-8' encoded. User provided values like the + image name and image properties are also properly quoted. + + """ response = HttpResponse() for key in DETAIL_FIELDS: if key == 'properties': - for k, v in image.get('properties', {}).items(): - name = 'x-image-meta-property-' + k.replace('_', '-') - response[name] = uenc(v) + for pkey, pval in image.get('properties', {}).items(): + pkey = 'x-image-meta-property-' + pkey.replace('_', '-') + pkey = quote(smart_str(pkey, encoding='utf-8')) + pval = quote(smart_str(pval, encoding='utf-8')) + response[pkey] = pval else: - name = 'x-image-meta-' + key.replace('_', '-') - response[name] = uenc(image.get(key, '')) + val = image.get(key, '') + if key == 'status': + val = API_STATUS_FROM_IMAGE_STATUS.get(val.upper(), "UNKNOWN") + if key == 'name' or key == 'description': + val = quote(smart_str(val, encoding='utf-8')) + key = 'x-image-meta-' + key.replace('_', '-') + response[key] = val return response -def _get_image_headers(request): - def normalize(s): - return ''.join('_' if c in punctuation else c.lower() for c in s) - - META_PREFIX = 'HTTP_X_IMAGE_META_' - META_PREFIX_LEN = len(META_PREFIX) - META_PROPERTY_PREFIX = 'HTTP_X_IMAGE_META_PROPERTY_' - META_PROPERTY_PREFIX_LEN = len(META_PROPERTY_PREFIX) - - headers = {'properties': {}} +def headers_to_image_params(request): + """Decode the HTTP request headers to the acceptable image parameters. - for key, val in request.META.items(): - if key.startswith(META_PROPERTY_PREFIX): - name = normalize(key[META_PROPERTY_PREFIX_LEN:]) - headers['properties'][unquote(name)] = unquote(val) - elif key.startswith(META_PREFIX): - name = normalize(key[META_PREFIX_LEN:]) - headers[unquote(name)] = unquote(val) + Get the image parameters from the headers of the HTTP request. All + parameters must be encoded using 'utf-8' encoding. User provided parameters + like the image name or the image properties must be quoted, so we need to + unquote them. + Finally, all image parameters name (HTTP header keys) are lowered + and all punctuation characters are replaced with underscore. - is_public = headers.get('is_public', None) - if is_public is not None: - headers['is_public'] = True if is_public.lower() == 'true' else False + """ - if not headers['properties']: - del headers['properties'] + def normalize(s): + return ''.join('_' if c in punctuation else c.lower() for c in s) - return headers + params = {} + properties = {} + try: + for key, val in request.META.items(): + if key.startswith(META_PREFIX): + if key.startswith(META_PROPERTY_PREFIX): + key = key[META_PROPERTY_PREFIX_LEN:] + key = smart_unicode(unquote(key), encoding='utf-8') + val = smart_unicode(unquote(val), encoding='utf-8') + properties[normalize(key)] = val + else: + key = smart_unicode(key[META_PREFIX_LEN:], + encoding='utf-8') + key = normalize(key) + if key in PLANKTON_FIELDS: + if key == "name": + val = smart_unicode(unquote(val), encoding='utf-8') + elif key == "is_public" and not isinstance(val, bool): + val = True if val.lower() == 'true' else False + params[key] = val + except DjangoUnicodeDecodeError: + raise faults.BadRequest("Could not decode request as UTF-8 string") + + params['properties'] = properties + + return params @api.api_method(http_method="POST", user_required=True, logger=log) @@ -143,7 +171,7 @@ def add_image(request): instead of uploading the data. """ - params = _get_image_headers(request) + params = headers_to_image_params(request) log.debug('add_image %s', params) if not set(params.keys()).issubset(set(ADD_FIELDS)): @@ -152,7 +180,7 @@ def add_image(request): name = params.pop('name', None) if name is None: raise faults.BadRequest("Image 'name' parameter is required") - elif len(uenc(name)) == 0: + elif len(smart_unicode(name, encoding="utf-8")) == 0: raise faults.BadRequest("Invalid image name") location = params.pop('location', None) if location is None: @@ -160,17 +188,17 @@ def add_image(request): try: split_url(location) - except InvalidLocation: + except AssertionError: raise faults.BadRequest("Invalid location '%s'" % location) validate_fields(params) if location: - with image_backend(request.user_uniq) as backend: + with PlanktonBackend(request.user_uniq) as backend: image = backend.register(name, location, params) else: - #f = StringIO(request.body) - #image = backend.put(name, f, params) + # f = StringIO(request.body) + # image = backend.put(name, f, params) return HttpResponse(status=501) # Not Implemented if not image: @@ -193,7 +221,7 @@ def delete_image(request, image_id): """ log.info("delete_image '%s'" % image_id) userid = request.user_uniq - with image_backend(userid) as backend: + with PlanktonBackend(userid) as backend: backend.unregister(image_id) log.info("User '%s' deleted image '%s'" % (userid, image_id)) return HttpResponse(status=204) @@ -211,7 +239,7 @@ def add_image_member(request, image_id, member): """ log.debug('add_image_member %s %s', image_id, member) - with image_backend(request.user_uniq) as backend: + with PlanktonBackend(request.user_uniq) as backend: backend.add_user(image_id, member) return HttpResponse(status=204) @@ -227,18 +255,6 @@ def get_image(request, image_id): * The implementation is very inefficient as it loads the whole image in memory. """ - - #image = backend.get_image(image_id) - #if not image: - # return HttpResponseNotFound() - # - #response = _create_image_response(image) - #data = backend.get_data(image) - #response.content = data - #response['Content-Length'] = len(data) - #response['Content-Type'] = 'application/octet-stream' - #response['ETag'] = image['checksum'] - #return response return HttpResponse(status=501) # Not Implemented @@ -250,7 +266,7 @@ def get_image_meta(request, image_id): 3.4. Requesting Detailed Metadata on a Specific Image """ - with image_backend(request.user_uniq) as backend: + with PlanktonBackend(request.user_uniq) as backend: image = backend.get_image(image_id) return _create_image_response(image) @@ -263,7 +279,7 @@ def list_image_members(request, image_id): 3.7. Requesting Image Memberships """ - with image_backend(request.user_uniq) as backend: + with PlanktonBackend(request.user_uniq) as backend: users = backend.list_users(image_id) members = [{'member_id': u, 'can_share': False} for u in users] @@ -313,7 +329,7 @@ def list_images(request, detail=False): except ValueError: raise faults.BadRequest("Malformed request.") - with image_backend(request.user_uniq) as backend: + with PlanktonBackend(request.user_uniq) as backend: images = backend.list_images(filters, params) # Remove keys that should not be returned @@ -342,10 +358,9 @@ def list_shared_images(request, member): log.debug('list_shared_images %s', member) images = [] - with image_backend(request.user_uniq) as backend: + with PlanktonBackend(request.user_uniq) as backend: for image in backend.list_shared_images(member=member): - image_id = image['id'] - images.append({'image_id': image_id, 'can_share': False}) + images.append({'image_id': image["id"], 'can_share': False}) data = json.dumps({'shared_images': images}, indent=settings.DEBUG) return HttpResponse(data) @@ -360,7 +375,7 @@ def remove_image_member(request, image_id, member): """ log.debug('remove_image_member %s %s', image_id, member) - with image_backend(request.user_uniq) as backend: + with PlanktonBackend(request.user_uniq) as backend: backend.remove_user(image_id, member) return HttpResponse(status=204) @@ -378,7 +393,7 @@ def update_image(request, image_id): and status. """ - meta = _get_image_headers(request) + meta = headers_to_image_params(request) log.debug('update_image %s', meta) if not set(meta.keys()).issubset(set(UPDATE_FIELDS)): @@ -386,7 +401,7 @@ def update_image(request, image_id): validate_fields(meta) - with image_backend(request.user_uniq) as backend: + with PlanktonBackend(request.user_uniq) as backend: image = backend.update_metadata(image_id, meta) return _create_image_response(image) @@ -403,15 +418,17 @@ def update_image_members(request, image_id): """ log.debug('update_image_members %s', image_id) + data = api.utils.get_json_body(request) members = [] - try: - data = json.loads(request.body) - for member in data['memberships']: - members.append(member['member_id']) - except (ValueError, KeyError, TypeError): - return HttpResponse(status=400) - with image_backend(request.user_uniq) as backend: + memberships = api.utils.get_attribute(data, "memberships", attr_type=list) + for member in memberships: + if not isinstance(member, dict): + raise faults.BadRequest("Invalid 'memberships' field") + member = api.utils.get_attribute(member, "member_id") + members.append(member) + + with PlanktonBackend(request.user_uniq) as backend: backend.replace_users(image_id, members) return HttpResponse(status=204) diff --git a/snf-cyclades-app/synnefo/quotas/__init__.py b/snf-cyclades-app/synnefo/quotas/__init__.py index ccb217f4932a733c5c31176f5bb808c94511bfba..6b780c1ee2e4abfda1f1f666ecdbbf2c140f3f0d 100644 --- a/snf-cyclades-app/synnefo/quotas/__init__.py +++ b/snf-cyclades-app/synnefo/quotas/__init__.py @@ -1,44 +1,30 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. -from django.utils import simplejson as json -from django.db import transaction +from synnefo.db import transaction from snf_django.lib.api import faults from synnefo.db.models import (QuotaHolderSerial, VirtualMachine, Network, - IPAddress) + IPAddress, Volume) from synnefo.settings import (CYCLADES_SERVICE_TOKEN as ASTAKOS_TOKEN, ASTAKOS_AUTH_URL) from astakosclient import AstakosClient from astakosclient import errors +from collections import defaultdict import logging log = logging.getLogger(__name__) @@ -46,7 +32,6 @@ log = logging.getLogger(__name__) QUOTABLE_RESOURCES = [VirtualMachine, Network, IPAddress] -DEFAULT_SOURCE = 'system' RESOURCES = [ "cyclades.vm", "cyclades.total_cpu", @@ -75,18 +60,38 @@ class Quotaholder(object): class AstakosClientExceptionHandler(object): def __init__(self, *args, **kwargs): - pass + self.user = kwargs.get("user") + self.projects = kwargs.get("projects") def __enter__(self): pass + def check_not_found(self): + if not self.user or not self.projects: + return + try: + qh = Quotaholder.get() + user_quota = qh.service_get_quotas(self.user) + except errors.AstakosClientException as e: + log.exception("Unexpected error %s" % e.message) + raise faults.InternalServerError("Unexpected error") + + user_quota = user_quota[self.user] + for project in self.projects: + try: + user_quota[project] + except KeyError: + m = "User %s not in project %s" % (self.user, project) + raise faults.BadRequest(m) + def __exit__(self, exc_type, value, traceback): if value is not None: # exception if not isinstance(value, errors.AstakosClientException): return False # reraise if exc_type is errors.QuotaLimit: - msg, details = render_overlimit_exception(value) - raise faults.OverLimit(msg, details=details) + raise faults.OverLimit(value.message, details=value.details) + if exc_type is errors.NotFound: + self.check_not_found() log.exception("Unexpected error %s" % value.message) raise faults.InternalServerError("Unexpected error") @@ -108,13 +113,27 @@ def issue_commission(resource, action, name="", force=False, auto_accept=False, return None user = resource.userid - source = DEFAULT_SOURCE + projects = set(p for (p, r) in provisions.keys()) qh = Quotaholder.get() - if True: # placeholder - with AstakosClientExceptionHandler(): - serial = qh.issue_one_commission(user, source, - provisions, name=name, + if action == "REASSIGN": + try: + from_project = action_fields["from_project"] + except KeyError: + raise Exception("Missing project attribute.") + + ext_provisions = {} + for (project, res), quantity in provisions.iteritems(): + ext_provisions[(from_project, project, res)] = quantity + projects.add(from_project) + with AstakosClientExceptionHandler(user=user, projects=projects): + serial = qh.issue_resource_reassignment(user, ext_provisions, + name=name, + force=force, + auto_accept=auto_accept) + else: + with AstakosClientExceptionHandler(user=user, projects=projects): + serial = qh.issue_one_commission(user, provisions, name=name, force=force, auto_accept=auto_accept) @@ -128,36 +147,37 @@ def issue_commission(resource, action, name="", force=False, auto_accept=False, serial_info["resolved"] = True serial = QuotaHolderSerial.objects.create(**serial_info) - - # Correlate the serial with the resource. Resolved serials are not - # attached to resources - if not auto_accept: - resource.serial = serial - resource.save() - return serial def accept_resource_serial(resource, strict=True): serial = resource.serial - assert serial.pending or serial.accept, "%s can't be accepted" % serial - log.debug("Accepting serial %s of resource %s", serial, resource) - _resolve_commissions(accept=[serial.serial], strict=strict) + accept_serial(serial, strict=strict) resource.serial = None resource.save() return resource +def accept_serial(serial, strict=True): + assert serial.pending or serial.accept, "%s can't be accepted" % serial + log.debug("Accepting serial %s", serial) + _resolve_commissions(accept=[serial.serial], strict=strict) + + def reject_resource_serial(resource, strict=True): serial = resource.serial - assert serial.pending or not serial.accept, "%s can't be rejected" % serial - log.debug("Rejecting serial %s of resource %s", serial, resource) - _resolve_commissions(reject=[serial.serial], strict=strict) + reject_serial(serial) resource.serial = None resource.save() return resource +def reject_serial(serial, strict=True): + assert serial.pending or not serial.accept, "%s can't be rejected" % serial + log.debug("Rejecting serial %s", serial) + _resolve_commissions(reject=[serial.serial], strict=strict) + + def _resolve_commissions(accept=None, reject=None, strict=True): if accept is None: accept = [] @@ -229,33 +249,6 @@ def get_quotaholder_pending(): return pending_serials -def render_overlimit_exception(e): - resource_name = {"vm": "Virtual Machine", - "cpu": "CPU", - "ram": "RAM", - "network.private": "Private Network", - "floating_ip": "Floating IP address"} - details = json.loads(e.details) - data = details['overLimit']['data'] - usage = data["usage"] - limit = data["limit"] - available = limit - usage - provision = data['provision'] - requested = provision['quantity'] - resource = provision['resource'] - res = resource.replace("cyclades.", "", 1) - try: - resource = resource_name[res] - except KeyError: - resource = res - - msg = "Resource Limit Exceeded for your account." - details = "Limit for resource '%s' exceeded for your account."\ - " Available: %s, Requested: %s"\ - % (resource, available, requested) - return msg, details - - @transaction.commit_on_success def issue_and_accept_commission(resource, action="BUILD", action_fields=None): """Issue and accept a commission to Quotaholder. @@ -287,7 +280,7 @@ def issue_and_accept_commission(resource, action="BUILD", action_fields=None): try: # Accept the commission to quotaholder - accept_resource_serial(resource) + accept_serial(serial) except: # Do not crash if we can not accept commission to Quotaholder. Quotas # have already been reserved and the resource already exists in DB. @@ -295,17 +288,30 @@ def issue_and_accept_commission(resource, action="BUILD", action_fields=None): log.exception("Failed to accept commission: %s", resource.serial) +def get_volume_resources(volumes): + resources = defaultdict(lambda: 0) + for volume in volumes: + volproj = volume.project + resources[(volproj, "cyclades.disk")] += int(volume.size) << 30 + return resources + + def get_commission_info(resource, action, action_fields=None): + project = resource.project if isinstance(resource, VirtualMachine): + resources = defaultdict(lambda: 0) flavor = resource.flavor - resources = {"cyclades.vm": 1, - "cyclades.total_cpu": flavor.cpu, - "cyclades.disk": 1073741824 * flavor.disk, - "cyclades.total_ram": 1048576 * flavor.ram} - online_resources = {"cyclades.cpu": flavor.cpu, - "cyclades.ram": 1048576 * flavor.ram} + offline_resources = {(project, "cyclades.vm"): 1, + (project, "cyclades.total_cpu"): flavor.cpu, + (project, "cyclades.total_ram"): flavor.ram << 20, + } + online_resources = {(project, "cyclades.cpu"): flavor.cpu, + (project, "cyclades.ram"): flavor.ram << 20} if action == "BUILD": + new_volumes = resource.volumes.filter(status="CREATING") + resources.update(offline_resources) resources.update(online_resources) + resources.update(get_volume_resources(new_volumes)) return resources if action == "START": if resource.operstate == "STOPPED": @@ -323,6 +329,15 @@ def get_commission_info(resource, action, action_fields=None): else: return None elif action == "DESTROY": + volumes = resource.volumes.filter(deleted=False) + if resource.operstate not in ["BUILD", "ERROR"]: + # Count only the volumes that are in the 'IN_USE' status, + # because a pending commission exists for the other volumes. + # The pending commission will be rejected, but + # snf-dispatcher will finally fix the quotas. + volumes = volumes.filter(status="IN_USE") + resources.update(offline_resources) + resources.update(get_volume_resources(volumes)) if resource.operstate in ["STARTED", "BUILD", "ERROR"]: resources.update(online_resources) return reverse_quantities(resources) @@ -330,28 +345,71 @@ def get_commission_info(resource, action, action_fields=None): beparams = action_fields.get("beparams") cpu = beparams.get("vcpus", flavor.cpu) ram = beparams.get("maxmem", flavor.ram) - return {"cyclades.total_cpu": cpu - flavor.cpu, - "cyclades.total_ram": 1048576 * (ram - flavor.ram)} + return {(project, "cyclades.total_cpu"): cpu - flavor.cpu, + (project, "cyclades.total_ram"): (ram - flavor.ram) << 20} + elif action == "REASSIGN": + resources.update(offline_resources) + system_volumes = resource.volumes.filter(index=0, deleted=False) + resources.update(get_volume_resources(system_volumes)) + if resource.operstate in ["STARTED", "BUILD", "ERROR"]: + resources.update(online_resources) + return resources + elif action in ["ATTACH_VOLUME", "DETACH_VOLUME", "MODIFY_VOLUME"]: + if action_fields is not None: + volumes_changes = action_fields.get("disks") + if volumes_changes is not None: + for action, db_volume, info in volumes_changes: + project = db_volume.project + resources[(project, "cyclades.disk")] += \ + get_volume_size_delta(action, db_volume, info) + return resources else: - #["CONNECT", "DISCONNECT", "SET_FIREWALL_PROFILE"]: + # ["CONNECT", "DISCONNECT", "SET_FIREWALL_PROFILE"]: return None elif isinstance(resource, Network): - resources = {"cyclades.network.private": 1} + resources = {(project, "cyclades.network.private"): 1} if action == "BUILD": return resources elif action == "DESTROY": return reverse_quantities(resources) + elif action == "REASSIGN": + return resources elif isinstance(resource, IPAddress): if resource.floating_ip: - resources = {"cyclades.floating_ip": 1} + resources = {(project, "cyclades.floating_ip"): 1} if action == "BUILD": return resources elif action == "DESTROY": return reverse_quantities(resources) + elif action == "REASSIGN": + return resources + else: + return None + elif isinstance(resource, Volume): + size = resource.size + resources = {(project, "cyclades.disk"): size << 30} + if resource.status == "CREATING" and action == "BUILD": + return resources + elif action == "DESTROY": + return reverse_quantities(resources) + elif action == "REASSIGN": + return resources else: return None +def get_volume_size_delta(action, db_volume, info): + """Compute the change in the size of a volume""" + if action == "add": + return int(db_volume.size) << 30 + elif action == "remove": + return -int(db_volume.size) << 30 + elif action == "modify": + return info.get("size_delta", 0) << 30 + else: + raise ValueError("Unknown volume action '%s'" % action) + + def reverse_quantities(resources): return dict((r, -s) for r, s in resources.items()) @@ -377,6 +435,13 @@ def handle_resource_commission(resource, action, commission_name, serial = issue_commission(resource, action, name=commission_name, force=force, auto_accept=auto_accept, action_fields=action_fields) + + # Correlate the serial with the resource. Resolved serials are not + # attached to resources + if not auto_accept: + resource.serial = serial + resource.save() + return serial diff --git a/snf-cyclades-app/synnefo/quotas/enforce.py b/snf-cyclades-app/synnefo/quotas/enforce.py index dadc443fe7d599d2d28cb2f440ba3167ee034bb5..89e4943b2af7997535c8252506d9865407e338cc 100644 --- a/snf-cyclades-app/synnefo/quotas/enforce.py +++ b/snf-cyclades-app/synnefo/quotas/enforce.py @@ -1,41 +1,26 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import time -from synnefo.db.models import VirtualMachine, IPAddress, NetworkInterface +from synnefo.db.models import VirtualMachine, IPAddress, NetworkInterface,\ + Volume from synnefo.logic import servers from synnefo.logic import ips as logic_ips from synnefo.logic import backend +from synnefo.volume import volumes as volumes_logic +from synnefo.lib.ordereddict import OrderedDict MiB = 2 ** 20 @@ -60,7 +45,7 @@ CHANGE = { "cyclades.vm": lambda vm: 1, "cyclades.total_ram": lambda vm: vm.flavor.ram * MiB, "cyclades.total_cpu": lambda vm: vm.flavor.cpu, - "cyclades.disk": lambda vm: vm.flavor.disk * GiB, + "cyclades.disk": lambda volume: volume.size * GiB, "cyclades.floating_ip": lambda vm: 1, } @@ -90,7 +75,8 @@ def sort_vms(): return f -def handle_stop_active(viol_id, resource, vms, diff, actions): +def handle_stop_active(viol_id, resource, vms, diff, actions, remains, + options=None): vm_actions = actions["vm"] vms = [vm for vm in vms if vm.operstate in ["STARTED", "BUILD", "ERROR"]] vms = sorted(vms, key=sort_vms(), reverse=True) @@ -103,14 +89,120 @@ def handle_stop_active(viol_id, resource, vms, diff, actions): vm_actions[vm.id] = viol_id, vm.operstate, vm.backend_id, action -def handle_destroy(viol_id, resource, vms, diff, actions): +def has_extra_disks(volumes): + return bool([vol for vol in volumes if vol.index != 0]) + + +def handle_destroy(viol_id, resource, vms, diff, actions, remains, + options=None): + cascade_remove = options.get("cascade_remove", False) vm_actions = actions["vm"] + if "volume" not in actions: + actions["volume"] = OrderedDict() + volume_actions = actions["volume"] vms = sorted(vms, key=sort_vms(), reverse=True) + all_volumes = Volume.objects.filter(deleted=False, machine__in=vms) + all_volumes = _partition_by(lambda vol: vol.machine_id, all_volumes) for vm in vms: if diff < 1: break + volumes = all_volumes.get(vm.id, []) + if has_extra_disks(volumes) and not cascade_remove: + continue diff -= CHANGE[resource](vm) - vm_actions[vm.id] = viol_id, vm.operstate, vm.backend_id, "REMOVE" + vm_actions[vm.id] = vm_remove_action(viol_id, vm) + for volume in volumes: + volume_actions[volume.id] = volume_remove_action( + viol_id, volume, machine=vm) + if diff > 0: + remains[resource].append(viol_id) + + +def volume_remove_action(viol_id, volume, machine=None): + backend_id = (machine.backend_id if machine is not None + else volume.machine.backend_id) + return (viol_id, volume.status, backend_id, "REMOVE") + + +def vm_remove_action(viol_id, vm): + return (viol_id, vm.operstate, vm.backend_id, "REMOVE") + + +VOLUME_SORT_LEVEL = { + "ERROR": 7, + "ERROR_DELETING": 6, + "CREATING": 5, + "AVAILABLE": 4, + "ATTACHING": 3, + "DETACHING": 3, + "DELETING": 2, + "DELETED": 2, + "BACKING_UP": 1, + "RESTORING_BACKUP": 1, + "ERROR_RESTORING": 1, + "IN_USE": 0, +} + + +def sort_volumes(removed): + def f(volume): + level = VOLUME_SORT_LEVEL[volume.status] + return (volume.id in removed, level, volume.id) + return f + + +def _is_system_volume(volume): + return volume.index == 0 + + +def handle_volume(viol_id, resource, volumes, diff, actions, remains, + options=None): + if "vm" not in actions: + actions["vm"] = OrderedDict() + vm_actions = actions["vm"] + volume_actions = actions["volume"] + remove_system_volumes = options.get("remove_system_volumes", False) + cascade_remove = options.get("cascade_remove", False) + other_removed = set(volume_actions.keys()) + volumes = sorted(volumes, key=sort_volumes(other_removed), reverse=True) + volume_ids = set(vol.id for vol in volumes) + machines = set(volume.machine_id for volume in volumes) + all_volumes = Volume.objects.filter(deleted=False, machine__in=machines) + all_volumes = _partition_by(lambda vol: vol.machine_id, all_volumes) + counted = set() + + for volume in volumes: + if diff < 1: + break + if volume.id in counted: + continue + if volume.id in other_removed: + diff -= CHANGE[resource](volume) + counted.add(volume.id) + continue + if not remove_system_volumes and _is_system_volume(volume): + continue + if not _is_system_volume(volume): + diff -= CHANGE[resource](volume) + volume_actions[volume.id] = volume_remove_action(viol_id, volume) + counted.add(volume) + continue + vm = volume.machine + sec_volumes = [v for v in all_volumes.get(vm.id, []) + if v.id != volume.id] + if sec_volumes and not cascade_remove: + continue + volume_actions[volume.id] = volume_remove_action(viol_id, volume) + diff -= CHANGE[resource](volume) + counted.add(volume) + vm_actions[vm.id] = vm_remove_action(viol_id, vm) + for vol in sec_volumes: + volume_actions[vol.id] = volume_remove_action(viol_id, vol) + if vol.id in volume_ids and vol.id not in counted: + diff -= CHANGE[resource](vol) + counted.add(vol.id) + if diff > 0: + remains[resource].append(viol_id) def _state_after_action(vm, action): @@ -139,7 +231,8 @@ def sort_ips(vm_actions): return f -def handle_floating_ip(viol_id, resource, ips, diff, actions): +def handle_floating_ip(viol_id, resource, ips, diff, actions, remains, + options=None): vm_actions = actions.get("vm", {}) ip_actions = actions["floating_ip"] ips = sorted(ips, key=sort_ips(vm_actions), reverse=True) @@ -155,30 +248,70 @@ def handle_floating_ip(viol_id, resource, ips, diff, actions): ip_actions[ip.id] = viol_id, state, backend_id, "REMOVE" -def get_vms(users=None): +def get_vms(users=None, projects=None): vms = VirtualMachine.objects.filter(deleted=False).\ select_related("flavor").order_by('-id') if users is not None: vms = vms.filter(userid__in=users) + if projects is not None: + vms = vms.filter(project__in=projects) - return _partition_by(lambda vm: vm.userid, vms) + vmsdict = _partition_by(lambda vm: vm.project, vms) + for project, projectdict in vmsdict.iteritems(): + vmsdict[project] = _partition_by(lambda vm: vm.userid, projectdict) + return vmsdict -def get_floating_ips(users=None): +def get_floating_ips(users=None, projects=None): ips = IPAddress.objects.filter(deleted=False, floating_ip=True).\ select_related("nic__machine") if users is not None: ips = ips.filter(userid__in=users) + if projects is not None: + ips = ips.filter(project__in=projects) - return _partition_by(lambda ip: ip.userid, ips) + ipsdict = _partition_by(lambda ip: ip.project, ips) + for project, projectdict in ipsdict.iteritems(): + ipsdict[project] = _partition_by(lambda ip: ip.userid, projectdict) + return ipsdict -def get_actual_resources(resource_type, users=None): +def get_volumes(users=None, projects=None): + volumes = Volume.objects.select_related("machine").\ + filter(deleted=False).order_by("-id") + if users is not None: + volumes = volumes.filter(userid__in=users) + if projects is not None: + volumes = volumes.filter(project__in=projects) + + volumesdict = _partition_by(lambda volume: volume.project, volumes) + for project, projectdict in volumesdict.iteritems(): + volumesdict[project] = _partition_by( + lambda volume: volume.userid, projectdict) + return volumesdict + + +def get_actual_resources(resource_type, users=None, projects=None): ACTUAL_RESOURCES = { "vm": get_vms, "floating_ip": get_floating_ips, + "volume": get_volumes, } - return ACTUAL_RESOURCES[resource_type](users=users) + return ACTUAL_RESOURCES[resource_type](users=users, projects=projects) + + +def skip_check(obj, to_check=None, excluded=None): + return (to_check is not None and obj not in to_check or + excluded is not None and obj in excluded) + + +def pick_project_resources(project_dict, users=None, excluded_users=None): + resources = [] + for user, user_resources in project_dict.iteritems(): + if skip_check(user, users, excluded_users): + continue + resources += user_resources + return resources VM_ACTION = { @@ -219,6 +352,33 @@ def perform_vm_actions(actions, opcount, maxops=None, fix=False, options={}): return log +def remove_volume(volume_id): + try: + objs = Volume.objects.select_for_update() + volume = objs.get(id=volume_id) + machine = volume.machine + if not machine.deleted and machine.task != "DESTROY": + volumes_logic.delete(volume) + return True + except BaseException: + return False + + +def perform_volume_actions(actions, opcount, maxops=None, fix=False, + options={}): + log = [] + for volume_id, value in actions.iteritems(): + (viol_id, state, backend_id, volume_action) = value + if not allow_operation(backend_id, opcount, maxops): + continue + data = ("volume", volume_id, state, backend_id, volume_action, viol_id) + if fix: + r = remove_volume(volume_id) + data += ("DONE" if r else "FAILED",) + log.append(data) + return log + + def wait_for_ip(ip_id): for i in range(100): ip = IPAddress.objects.get(id=ip_id) @@ -266,6 +426,7 @@ def perform_actions(actions, maxops=None, fix=False, options={}): ACTION_HANDLING = [ ("floating_ip", perform_floating_ip_actions), ("vm", perform_vm_actions), + ("volume", perform_volume_actions), ] opcount = {} @@ -285,7 +446,7 @@ RESOURCE_HANDLING = [ ("cyclades.ram", handle_stop_active, "vm"), ("cyclades.total_cpu", handle_destroy, "vm"), ("cyclades.total_ram", handle_destroy, "vm"), - ("cyclades.disk", handle_destroy, "vm"), ("cyclades.vm", handle_destroy, "vm"), + ("cyclades.disk", handle_volume, "volume"), ("cyclades.floating_ip", handle_floating_ip, "floating_ip"), ] diff --git a/snf-cyclades-app/synnefo/quotas/management/commands/enforce-resources-cyclades.py b/snf-cyclades-app/synnefo/quotas/management/commands/enforce-resources-cyclades.py index 2460a173a21829f99bc532e56956557308622df4..ec9ec4d5446921a1f07fc6d9615ddccd9f5f3c72 100644 --- a/snf-cyclades-app/synnefo/quotas/management/commands/enforce-resources-cyclades.py +++ b/snf-cyclades-app/synnefo/quotas/management/commands/enforce-resources-cyclades.py @@ -1,39 +1,21 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import string from optparse import make_option -from django.db import transaction +from synnefo.db import transaction from synnefo.lib.ordereddict import OrderedDict from synnefo.quotas import util @@ -41,6 +23,7 @@ from synnefo.quotas import enforce from synnefo.quotas import errors from snf_django.management.commands import SynnefoCommand, CommandError from snf_django.management.utils import pprint_table +from collections import defaultdict DEFAULT_RESOURCES = ["cyclades.cpu", @@ -48,11 +31,17 @@ DEFAULT_RESOURCES = ["cyclades.cpu", "cyclades.floating_ip", ] +DESTROY_RESOURCES = ["cyclades.vm", + "cyclades.total_cpu", + "cyclades.total_ram", + ] + class Command(SynnefoCommand): help = """Check and fix quota violations for Cyclades resources. """ - option_list = SynnefoCommand.option_list + ( + + command_option_list = ( make_option("--max-operations", help="Limit operations per backend."), make_option("--users", dest="users", @@ -60,6 +49,12 @@ class Command(SynnefoCommand): "of users, e.g uuid1,uuid2")), make_option("--exclude-users", help=("Exclude list of users from resource enforcement")), + make_option("--projects", + help=("Enforce resources only for the specified list " + "of projects, e.g uuid1,uuid2")), + make_option("--exclude-projects", + help=("Exclude list of projects from resource enforcement") + ), make_option("--resources", help="Specify resources to check, default: %s" % ",".join(DEFAULT_RESOURCES)), @@ -74,6 +69,17 @@ class Command(SynnefoCommand): "remove a vm")), make_option("--shutdown-timeout", help="Force vm shutdown after given seconds."), + make_option("--remove-system-volumes", + default=False, + action="store_true", + help=("Allow removal of system volumes. This will also " + "remove the VM.")), + make_option("--cascade-remove", + default=False, + action="store_true", + help=("Allow removal of a VM which has additional " + "(non system) volumes attached. This will also " + "remove these volumes")), ) def confirm(self): @@ -110,6 +116,7 @@ class Command(SynnefoCommand): write = self.stderr.write fix = options["fix"] force = options["force"] + handlers = self.get_handlers(options["resources"]) maxops = options["max_operations"] if maxops is not None: try: @@ -126,36 +133,101 @@ class Command(SynnefoCommand): m = "Expected integer shutdown timeout." raise CommandError(m) - users = options['users'] - if users is not None: - users = users.split(',') + remove_system_volumes = options["remove_system_volumes"] + cascade_remove = options["cascade_remove"] - excluded = options['exclude_users'] - excluded = set(excluded.split(',') if excluded is not None else []) + excluded_users = options['exclude_users'] + excluded_users = set(excluded_users.split(',') + if excluded_users is not None else []) + + users_to_check = options['users'] + if users_to_check is not None: + users_to_check = list(set(users_to_check.split(',')) - + excluded_users) + + try: + qh_holdings = util.get_qh_users_holdings(users_to_check) + except errors.AstakosClientException as e: + raise CommandError(e) + + excluded_projects = options["exclude_projects"] + excluded_projects = set(excluded_projects.split(',') + if excluded_projects is not None else []) + + projects_to_check = options["projects"] + if projects_to_check is not None: + projects_to_check = list(set(projects_to_check.split(',')) - + excluded_projects) - handlers = self.get_handlers(options["resources"]) try: - qh_holdings = util.get_qh_users_holdings(users) + qh_project_holdings = util.get_qh_project_holdings( + projects_to_check) except errors.AstakosClientException as e: raise CommandError(e) + qh_project_holdings = sorted(qh_project_holdings.items()) qh_holdings = sorted(qh_holdings.items()) resources = set(h[0] for h in handlers) dangerous = bool(resources.difference(DEFAULT_RESOURCES)) + self.stderr.write("Checking resources %s...\n" % + ",".join(list(resources))) + + hopts = {"cascade_remove": cascade_remove, + "remove_system_volumes": remove_system_volumes, + } opts = {"shutdown_timeout": shutdown_timeout} actions = {} overlimit = [] viol_id = 0 + remains = defaultdict(list) + + if users_to_check is None: + for resource, handle_resource, resource_type in handlers: + if resource_type not in actions: + actions[resource_type] = OrderedDict() + actual_resources = enforce.get_actual_resources( + resource_type, projects=projects_to_check) + for project, project_quota in qh_project_holdings: + if enforce.skip_check(project, projects_to_check, + excluded_projects): + continue + try: + qh = util.transform_project_quotas(project_quota) + qh_value, qh_limit, qh_pending = qh[resource] + except KeyError: + write("Resource '%s' does not exist in Quotaholder" + " for project '%s'!\n" % + (resource, project)) + continue + if qh_pending: + write("Pending commission for project '%s', " + "resource '%s'. Skipping\n" % + (project, resource)) + continue + diff = qh_value - qh_limit + if diff > 0: + viol_id += 1 + overlimit.append((viol_id, "project", project, "", + resource, qh_limit, qh_value)) + relevant_resources = enforce.pick_project_resources( + actual_resources[project], users=users_to_check, + excluded_users=excluded_users) + handle_resource(viol_id, resource, relevant_resources, + diff, actions, remains, options=hopts) + for resource, handle_resource, resource_type in handlers: if resource_type not in actions: actions[resource_type] = OrderedDict() actual_resources = enforce.get_actual_resources(resource_type, - users) + users_to_check) for user, user_quota in qh_holdings: - if user in excluded: + if enforce.skip_check(user, users_to_check, excluded_users): continue for source, source_quota in user_quota.iteritems(): + if enforce.skip_check(source, projects_to_check, + excluded_projects): + continue try: qh = util.transform_quotas(source_quota) qh_value, qh_limit, qh_pending = qh[resource] @@ -172,17 +244,18 @@ class Command(SynnefoCommand): diff = qh_value - qh_limit if diff > 0: viol_id += 1 - overlimit.append((viol_id, user, source, resource, - qh_limit, qh_value)) - relevant_resources = actual_resources[user] + overlimit.append((viol_id, "user", user, source, + resource, qh_limit, qh_value)) + relevant_resources = actual_resources[source][user] handle_resource(viol_id, resource, relevant_resources, - diff, actions) + diff, actions, remains, options=hopts) if not overlimit: write("No violations.\n") return - headers = ("#", "User", "Source", "Resource", "Limit", "Usage") + headers = ("#", "Type", "Holder", "Source", "Resource", "Limit", + "Usage") pprint_table(self.stdout, overlimit, headers, options["output_format"], title="Violations") @@ -191,7 +264,7 @@ class Command(SynnefoCommand): if fix: if dangerous and not force: write("You are enforcing resources that may permanently " - "remove a vm.\n") + "remove a vm or volume.\n") self.confirm() write("Applying actions. Please wait...\n") title = "Applied Actions" if fix else "Suggested Actions" @@ -202,3 +275,29 @@ class Command(SynnefoCommand): headers += ("Result",) pprint_table(self.stdout, log, headers, options["output_format"], title=title) + + def explain(resource): + if resource == "cyclades.disk": + if not remove_system_volumes: + return (", because this would need to remove system " + "volumes; if you want to do so, use the " + "--remove-system-volumes option:") + if not cascade_remove: + return (", because this would trigger the removal of " + "attached volumes, too; if you want to do " + "so, use the --cascade-remove option:") + elif resource in DESTROY_RESOURCES: + if not cascade_remove: + return (", because this would trigger the removal of " + "attached volumes, too; if you want to do " + "so, use the --cascade-remove option:") + return ":" + + if remains: + self.stderr.write("\n") + for resource, viols in remains.iteritems(): + self.stderr.write( + "The following violations for resource '%s' " + "could not be resolved%s\n" + % (resource, explain(resource))) + self.stderr.write(" %s\n" % ",".join(map(str, viols))) diff --git a/snf-cyclades-app/synnefo/quotas/management/commands/reconcile-commissions-cyclades.py b/snf-cyclades-app/synnefo/quotas/management/commands/reconcile-commissions-cyclades.py index a18cefb29eb7ad1fdcd51c7298bc4922a2224edf..e51996f9ef2ca93df6f0cf9f7f23c7ac8ff0244f 100644 --- a/snf-cyclades-app/synnefo/quotas/management/commands/reconcile-commissions-cyclades.py +++ b/snf-cyclades-app/synnefo/quotas/management/commands/reconcile-commissions-cyclades.py @@ -1,46 +1,28 @@ -# Copyright 2012-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. -from django.core.management.base import BaseCommand from optparse import make_option +from snf_django.management.commands import SynnefoCommand from synnefo import quotas -class Command(BaseCommand): +class Command(SynnefoCommand): help = "Detect and resolve pending commissions to Quotaholder" output_transaction = True - option_list = BaseCommand.option_list + ( + option_list = SynnefoCommand.option_list + ( make_option("--fix", dest="fix", action='store_true', default=False, diff --git a/snf-cyclades-app/synnefo/quotas/management/commands/reconcile-resources-cyclades.py b/snf-cyclades-app/synnefo/quotas/management/commands/reconcile-resources-cyclades.py index 38892ba3653833dd8687ec29dbb750018562d878..71ccd74e82c54ba1fb895082bc16010fc2c90f1e 100644 --- a/snf-cyclades-app/synnefo/quotas/management/commands/reconcile-resources-cyclades.py +++ b/snf-cyclades-app/synnefo/quotas/management/commands/reconcile-resources-cyclades.py @@ -1,58 +1,42 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from datetime import datetime -from django.core.management.base import BaseCommand from optparse import make_option - from synnefo import quotas -from synnefo.quotas.util import (get_db_holdings, get_quotaholder_holdings, - transform_quotas) +from synnefo.quotas import util +from synnefo.quotas import errors from snf_django.management.utils import pprint_table +from snf_django.management.commands import SynnefoCommand, CommandError +from snf_django.utils import reconcile -class Command(BaseCommand): +class Command(SynnefoCommand): help = """Reconcile resource usage of Astakos with Cyclades DB. Detect unsynchronized usage between Astakos and Cyclades DB resources and synchronize them if specified so. """ - option_list = BaseCommand.option_list + ( - make_option("--userid", dest="userid", + option_list = SynnefoCommand.option_list + ( + make_option("--user", dest="userid", default=None, help="Reconcile resources only for this user"), + make_option("--project", + help="Reconcile resources only for this project"), make_option("--fix", dest="fix", default=False, action="store_true", @@ -67,68 +51,52 @@ class Command(BaseCommand): def handle(self, *args, **options): write = self.stderr.write userid = options['userid'] + project = options["project"] # Get holdings from Cyclades DB - db_holdings = get_db_holdings(userid) - # Get holdings from QuotaHolder - qh_holdings = get_quotaholder_holdings(userid) - - users = set(db_holdings.keys()) - users.update(qh_holdings.keys()) - # Remove 'None' user - users.discard(None) - - if userid and userid not in users: - write("User '%s' does not exist in Quotaholder!", userid) - return + db_holdings = util.get_db_holdings(user=userid, project=project) + db_project_holdings = util.get_db_holdings(project=project, + for_users=False) - pending_exists = False - unknown_user_exists = False - unsynced = [] - for user in users: - db = db_holdings.get(user, {}) - try: - qh_all = qh_holdings[user] - except KeyError: - write("User '%s' does not exist in Quotaholder!\n" % - user) - unknown_user_exists = True - continue - - # Assuming only one source - qh = qh_all.get(quotas.DEFAULT_SOURCE, {}) - qh = transform_quotas(qh) - for resource in quotas.RESOURCES: - db_value = db.pop(resource, 0) - try: - qh_value, _, qh_pending = qh[resource] - except KeyError: - write("Resource '%s' does not exist in Quotaholder" - " for user '%s'!\n" % (resource, user)) - continue - if qh_pending: - write("Pending commission. User '%s', resource '%s'.\n" % - (user, resource)) - pending_exists = True - continue - if db_value != qh_value: - data = (user, resource, db_value, qh_value) - unsynced.append(data) - - headers = ("User", "Resource", "Database", "Quotaholder") + # Get holdings from QuotaHolder + try: + qh_holdings = util.get_qh_users_holdings( + [userid] if userid is not None else None, + [project] if project is not None else None) + qh_project_holdings = util.get_qh_project_holdings( + [project] if project is not None else None) + except errors.AstakosClientException as e: + raise CommandError(e) + + unsynced_users, users_pending, users_unknown =\ + reconcile.check_users(self.stderr, quotas.RESOURCES, + db_holdings, qh_holdings) + + unsynced_projects, projects_pending, projects_unknown =\ + reconcile.check_projects(self.stderr, quotas.RESOURCES, + db_project_holdings, qh_project_holdings) + pending_exists = users_pending or projects_pending + unknown_exists = users_unknown or projects_unknown + + headers = ("Type", "Holder", "Source", "Resource", + "Database", "Quotaholder") + unsynced = unsynced_users + unsynced_projects if unsynced: pprint_table(self.stdout, unsynced, headers) if options["fix"]: qh = quotas.Quotaholder.get() - request = {} - request["force"] = options["force"] - request["auto_accept"] = True - request["name"] = \ - ("client: reconcile-resources-cyclades, time: %s" - % datetime.now()) - request["provisions"] = map(create_provision, unsynced) + force = options["force"] + name = ("client: reconcile-resources-cyclades, time: %s" + % datetime.now()) + user_provisions = reconcile.create_user_provisions( + unsynced_users) + project_provisions = reconcile.create_project_provisions( + unsynced_projects) try: - qh.issue_commission(request) + qh.issue_commission_generic( + user_provisions, project_provisions, + name=name, force=force, + auto_accept=True) except quotas.errors.QuotaLimit: write("Reconciling failed because a limit has been " "reached. Use --force to ignore the check.\n") @@ -138,13 +106,5 @@ class Command(BaseCommand): if pending_exists: write("Found pending commissions. Run 'snf-manage" " reconcile-commissions-cyclades'\n") - elif not (unsynced or unknown_user_exists): + elif not (unsynced or unknown_exists): write("Everything in sync.\n") - - -def create_provision(provision_info): - user, resource, db_value, qh_value = provision_info - return {"holder": user, - "source": quotas.DEFAULT_SOURCE, - "resource": resource, - "quantity": db_value - qh_value} diff --git a/snf-cyclades-app/synnefo/quotas/resources.py b/snf-cyclades-app/synnefo/quotas/resources.py index d2f8dbd9a67bf7a4781cfc8bcaf35b548c45e6e3..b9af38f985214ef36294ad8e9a84afb893722446 100644 --- a/snf-cyclades-app/synnefo/quotas/resources.py +++ b/snf-cyclades-app/synnefo/quotas/resources.py @@ -1,39 +1,20 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. -from synnefo.util.keypath import get_path from synnefo.api.services import cyclades_services resources = \ - get_path(cyclades_services, 'cyclades_compute.resources').values() +\ - get_path(cyclades_services, 'cyclades_network.resources').values() + cyclades_services['cyclades_compute']['resources'].values() +\ + cyclades_services['cyclades_network']['resources'].values() diff --git a/snf-cyclades-app/synnefo/quotas/tests.py b/snf-cyclades-app/synnefo/quotas/tests.py index 118121ba886a61902726d7263b704ba79b8f8cfd..332c00a3e73a0c935929e2436497495a651cfc9e 100644 --- a/snf-cyclades-app/synnefo/quotas/tests.py +++ b/snf-cyclades-app/synnefo/quotas/tests.py @@ -2,38 +2,20 @@ # # -*- coding: utf-8 -*- # -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # # from mock import patch @@ -45,22 +27,24 @@ from synnefo.quotas import util class GetDBHoldingsTestCase(TestCase): + maxDiff = None + def test_no_holdings(self): holdings = util.get_db_holdings(user=None) self.assertEqual(holdings, {}) def test_vm_holdings(self): - flavor = mfactory.FlavorFactory(cpu=24, ram=8192, disk=20, - disk_template='drbd') - mfactory.VirtualMachineFactory() + flavor = mfactory.FlavorFactory(cpu=24, ram=8192, disk=20) + mfactory.VirtualMachineFactory(userid="user1", deleted=True) mfactory.VirtualMachineFactory(flavor=flavor, userid="user1", operstate="BUILD") - user_holdings = {"user1": {"cyclades.vm": 1, - "cyclades.total_cpu": 24, - "cyclades.cpu": 24, - "cyclades.disk": 21474836480, - "cyclades.total_ram": 8589934592, - "cyclades.ram": 8589934592}} + mfactory.VolumeFactory(userid="user1", size=20, machine=None) + user_holdings = {"user1": {"user1": {"cyclades.vm": 1, + "cyclades.total_cpu": 24, + "cyclades.cpu": 24, + "cyclades.disk": 20 << 30, + "cyclades.total_ram": 8192 << 20, + "cyclades.ram": 8192 << 20}}} holdings = util.get_db_holdings(user="user1") self.assertEqual(holdings, user_holdings) holdings = util.get_db_holdings() @@ -69,27 +53,28 @@ class GetDBHoldingsTestCase(TestCase): ## mfactory.VirtualMachineFactory(flavor=flavor, userid="user2", operstate="STARTED") - user_holdings = {"user2": {"cyclades.vm": 1, - "cyclades.total_cpu": 24, - "cyclades.cpu": 24, - "cyclades.disk": 21474836480, - "cyclades.total_ram": 8589934592, - "cyclades.ram": 8589934592}} + mfactory.VolumeFactory(userid="user2", size=30, machine=None) + user_holdings = {"user2": {"user2": {"cyclades.vm": 1, + "cyclades.total_cpu": 24, + "cyclades.cpu": 24, + "cyclades.disk": 30 << 30, + "cyclades.total_ram": 8192 << 20, + "cyclades.ram": 8192 << 20}}} holdings = util.get_db_holdings(user="user2") self.assertEqual(holdings, user_holdings) mfactory.VirtualMachineFactory(flavor=flavor, userid="user3", operstate="STOPPED") - user_holdings = {"user3": {"cyclades.vm": 1, - "cyclades.total_cpu": 24, - "cyclades.disk": 21474836480, - "cyclades.total_ram": 8589934592}} + user_holdings = {"user3": {"user3": {"cyclades.vm": 1, + "cyclades.total_cpu": 24, + "cyclades.total_ram": 8589934592}} + } holdings = util.get_db_holdings(user="user3") self.assertEqual(holdings, user_holdings) def test_network_holdings(self): mfactory.NetworkFactory(userid="user1") mfactory.NetworkFactory(userid="user2") - user_holdings = {"user2": {"cyclades.network.private": 1}} + user_holdings = {"user2": {"user2": {"cyclades.network.private": 1}}} holdings = util.get_db_holdings(user="user2") self.assertEqual(holdings, user_holdings) holdings = util.get_db_holdings() @@ -101,9 +86,9 @@ class GetDBHoldingsTestCase(TestCase): mfactory.IPv4AddressFactory(userid="user2", floating_ip=True) mfactory.IPv4AddressFactory(userid="user3", floating_ip=True) holdings = util.get_db_holdings() - self.assertEqual(holdings["user1"]["cyclades.floating_ip"], 2) - self.assertEqual(holdings["user2"]["cyclades.floating_ip"], 1) - self.assertEqual(holdings["user3"]["cyclades.floating_ip"], 1) + self.assertEqual(holdings["user1"]["user1"]["cyclades.floating_ip"], 2) + self.assertEqual(holdings["user2"]["user2"]["cyclades.floating_ip"], 1) + self.assertEqual(holdings["user3"]["user3"]["cyclades.floating_ip"], 1) @patch("synnefo.quotas.get_quotaholder_pending") @@ -132,9 +117,15 @@ class ResolvePendingTestCase(TestCase): class GetCommissionInfoTest(TestCase): + maxDiff = None + def test_commissions(self): flavor = mfactory.FlavorFactory(cpu=2, ram=1024, disk=20) vm = mfactory.VirtualMachineFactory(flavor=flavor) + mfactory.VolumeFactory(size=20, machine=vm, deleted=False, + status="IN_USE", + delete_on_termination=True) + vm.volumes.update(project=vm.project) #commission = quotas.get_commission_info(vm, "BUILD") #self.assertEqual({"cyclades.vm": 1, # "cyclades.cpu": 2, @@ -144,45 +135,46 @@ class GetCommissionInfoTest(TestCase): # "cyclades.disk": 1073741824 * 20}, commission) vm.operstate = "STARTED" vm.save() + project = vm.project commission = quotas.get_commission_info(vm, "STOP") - self.assertEqual({"cyclades.cpu": -2, - "cyclades.ram": 1048576 * -1024}, commission) + self.assertEqual({(project, "cyclades.cpu"): -2, + (project, "cyclades.ram"): 1048576 * -1024}, commission) # Check None quotas if vm is already stopped vm.operstate = "STOPPED" vm.save() commission = quotas.get_commission_info(vm, "STOP") self.assertEqual(None, commission) commission = quotas.get_commission_info(vm, "START") - self.assertEqual({"cyclades.cpu": 2, - "cyclades.ram": 1048576 * 1024}, commission) + self.assertEqual({(project, "cyclades.cpu"): 2, + (project, "cyclades.ram"): 1048576 * 1024}, commission) vm.operstate = "STARTED" vm.save() commission = quotas.get_commission_info(vm, "DESTROY") - self.assertEqual({"cyclades.vm": -1, - "cyclades.total_cpu": -2, - "cyclades.cpu": -2, - "cyclades.total_ram": 1048576 * -1024, - "cyclades.ram": 1048576 * -1024, - "cyclades.disk": 1073741824 * -20}, commission) + self.assertEqual({(project, "cyclades.vm"): -1, + (project, "cyclades.total_cpu"): -2, + (project, "cyclades.cpu"): -2, + (project, "cyclades.total_ram"): 1048576 * -1024, + (project, "cyclades.ram"): 1048576 * -1024, + (project, "cyclades.disk"): 1073741824 * -20}, commission) vm.operstate = "STOPPED" vm.save() commission = quotas.get_commission_info(vm, "DESTROY") - self.assertEqual({"cyclades.vm": -1, - "cyclades.total_cpu": -2, - "cyclades.total_ram": 1048576 * -1024, - "cyclades.disk": 1073741824 * -20}, commission) + self.assertEqual({(project, "cyclades.vm"): -1, + (project, "cyclades.total_cpu"): -2, + (project, "cyclades.total_ram"): -1024 << 20, + (project, "cyclades.disk"): -20 << 30}, commission) commission = quotas.get_commission_info(vm, "RESIZE") self.assertEqual(None, commission) commission = quotas.get_commission_info(vm, "RESIZE", {"beparams": {"vcpus": 4, "maxmem": 2048}}) - self.assertEqual({"cyclades.total_cpu": 2, - "cyclades.total_ram": 1048576 * 1024}, commission) + self.assertEqual({(project, "cyclades.total_cpu"): 2, + (project, "cyclades.total_ram"): 1048576 * 1024}, commission) vm.operstate = "STOPPED" vm.save() commission = quotas.get_commission_info(vm, "REBOOT") - self.assertEqual({"cyclades.cpu": 2, - "cyclades.ram": 1048576 * 1024}, commission) + self.assertEqual({(project, "cyclades.cpu"): 2, + (project, "cyclades.ram"): 1048576 * 1024}, commission) vm.operstate = "STARTED" vm.save() commission = quotas.get_commission_info(vm, "REBOOT") diff --git a/snf-cyclades-app/synnefo/quotas/util.py b/snf-cyclades-app/synnefo/quotas/util.py index 1f8cc1f8b2b0c12ce6f2b719f786c8b48fdcfb43..6039be0a5760af8338419e45734d5851ec2e49d9 100644 --- a/snf-cyclades-app/synnefo/quotas/util.py +++ b/snf-cyclades-app/synnefo/quotas/util.py @@ -1,95 +1,111 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.db.models import Sum, Count, Q -from synnefo.db.models import VirtualMachine, Network, IPAddress +from synnefo.db.models import VirtualMachine, Network, IPAddress, Volume from synnefo.quotas import Quotaholder +from collections import defaultdict + +QuotaDict = lambda: defaultdict(lambda: defaultdict(dict)) + +MiB = 2 ** 20 +GiB = 2 ** 30 + +def get_db_holdings(user=None, project=None, for_users=True): + """Get per user or per project holdings from Cyclades DB.""" -def get_db_holdings(user=None): - """Get holdings from Cyclades DB.""" - holdings = {} + if for_users is False and user is not None: + raise ValueError( + "Computing per project holdings; setting a user is meaningless.") + holdings = QuotaDict() vms = VirtualMachine.objects.filter(deleted=False) networks = Network.objects.filter(deleted=False) floating_ips = IPAddress.objects.filter(deleted=False, floating_ip=True) + volumes = Volume.objects.filter(deleted=False) - if user is not None: + if for_users and user is not None: vms = vms.filter(userid=user) networks = networks.filter(userid=user) floating_ips = floating_ips.filter(userid=user) - - # Get resources related with VMs - vm_resources = vms.values("userid")\ - .annotate(num=Count("id"), - total_ram=Sum("flavor__ram"), - total_cpu=Sum("flavor__cpu"), - disk=Sum("flavor__disk")) - vm_active_resources = \ - vms.values("userid")\ - .filter(Q(operstate="STARTED") | Q(operstate="BUILD") | - Q(operstate="ERROR"))\ - .annotate(ram=Sum("flavor__ram"), - cpu=Sum("flavor__cpu")) - + volumes = volumes.filter(userid=user) + + if project is not None: + vms = vms.filter(project=project) + networks = networks.filter(project=project) + floating_ips = floating_ips.filter(project=project) + volumes = volumes.filter(project=project) + + values = ["project"] + if for_users: + values.append("userid") + + vm_resources = vms.values(*values)\ + .annotate(num=Count("id"), + total_ram=Sum("flavor__ram"), + total_cpu=Sum("flavor__cpu")) for vm_res in vm_resources.iterator(): - user = vm_res['userid'] + project = vm_res['project'] res = {"cyclades.vm": vm_res["num"], "cyclades.total_cpu": vm_res["total_cpu"], - "cyclades.disk": 1073741824 * vm_res["disk"], - "cyclades.total_ram": 1048576 * vm_res["total_ram"]} - holdings[user] = res + "cyclades.total_ram": vm_res["total_ram"] * MiB} + pholdings = holdings[vm_res['userid']] if for_users else holdings + pholdings[project] = res + + vm_active_resources = vms.values(*values)\ + .filter(Q(operstate="STARTED") | Q(operstate="BUILD") | + Q(operstate="ERROR"))\ + .annotate(ram=Sum("flavor__ram"), + cpu=Sum("flavor__cpu")) for vm_res in vm_active_resources.iterator(): - user = vm_res['userid'] - holdings[user]["cyclades.cpu"] = vm_res["cpu"] - holdings[user]["cyclades.ram"] = 1048576 * vm_res["ram"] + project = vm_res['project'] + pholdings = holdings[vm_res['userid']] if for_users else holdings + pholdings[project]["cyclades.cpu"] = vm_res["cpu"] + pholdings[project]["cyclades.ram"] = vm_res["ram"] * MiB + + # Get disk resource + disk_resources = volumes.values(*values).annotate(Sum("size")) + for disk_res in disk_resources.iterator(): + project = disk_res['project'] + pholdings = (holdings[disk_res['userid']] + if for_users else holdings) + pholdings[project]["cyclades.disk"] = disk_res["size__sum"] * GiB # Get resources related with networks - net_resources = networks.values("userid")\ + net_resources = networks.values(*values)\ .annotate(num=Count("id")) + for net_res in net_resources.iterator(): - user = net_res['userid'] - holdings.setdefault(user, {}) - holdings[user]["cyclades.network.private"] = net_res["num"] + project = net_res['project'] + if project is None: + continue + pholdings = holdings[net_res['userid']] if for_users else holdings + pholdings[project]["cyclades.network.private"] = net_res["num"] - floating_ips_resources = floating_ips.values("userid")\ + floating_ips_resources = floating_ips.values(*values)\ .annotate(num=Count("id")) + for floating_ip_res in floating_ips_resources.iterator(): - user = floating_ip_res["userid"] - holdings.setdefault(user, {}) - holdings[user]["cyclades.floating_ip"] = floating_ip_res["num"] + project = floating_ip_res["project"] + pholdings = (holdings[floating_ip_res["userid"]] + if for_users else holdings) + pholdings[project]["cyclades.floating_ip"] = \ + floating_ip_res["num"] return holdings @@ -103,24 +119,14 @@ def get_quotaholder_holdings(user=None): return qh.service_get_quotas(user) -def get_qh_users_holdings(users=None): +def get_qh_users_holdings(users=None, projects=None): qh = Quotaholder.get() - if users is None or len(users) != 1: - req = None - else: - req = users[0] - quotas = qh.service_get_quotas(req) + return qh.service_get_quotas(user=users, project_id=projects) - if users is None: - return quotas - qs = {} - for user in users: - try: - qs[user] = quotas[user] - except KeyError: - pass - return qs +def get_qh_project_holdings(projects=None): + qh = Quotaholder.get() + return qh.service_get_project_quotas(project_id=projects) def transform_quotas(quotas): @@ -131,3 +137,13 @@ def transform_quotas(quotas): pending = counters['pending'] d[resource] = (used, limit, pending) return d + + +def transform_project_quotas(quotas): + d = {} + for resource, counters in quotas.iteritems(): + used = counters['project_usage'] + limit = counters['project_limit'] + pending = counters['project_pending'] + d[resource] = (used, limit, pending) + return d diff --git a/snf-cyclades-app/synnefo/tools/add_unique_name_to_disks b/snf-cyclades-app/synnefo/tools/add_unique_name_to_disks new file mode 100755 index 0000000000000000000000000000000000000000..12a6ccf34ab134f6871110d5da927400d0f36896 --- /dev/null +++ b/snf-cyclades-app/synnefo/tools/add_unique_name_to_disks @@ -0,0 +1,152 @@ +#!/usr/bin/env python +"""Tool to update Ganeti instances. + +Add a unique name to the disks of all Ganeti instances, which is +based on the PK of the corresponding Volume in Cyclades DB. + +""" + +# Gevent patching +import gevent +from gevent import monkey +monkey.patch_all() + +import sys +import subprocess +from optparse import OptionParser, TitledHelpFormatter + +# Configure Django env +from synnefo import settings +from django.core.management import setup_environ +setup_environ(settings) + +from django.db import close_connection +from synnefo.db.models import Backend, pooled_rapi_client +from synnefo.management.common import get_resource + +import logging +logger = logging.getLogger("migrate_disks") +handler = logging.StreamHandler() + +formatter = logging.Formatter("[%(levelname)s] %(message)s") +handler.setFormatter(formatter) +logger.setLevel(logging.DEBUG) +logger.addHandler(handler) +logger.propagate = False + +DESCRIPTION = """\ +Tool to update all Ganeti instances in order to add a unique name to the disks +of all instances. +""" + + +def main(): + parser = OptionParser(description=DESCRIPTION, + formatter=TitledHelpFormatter()) + parser.add_option("--backend-id", dest="backend_id", + help="Update instances only of this Ganeti backend."), + parser.add_option("--dry-run", dest="dry_run", default=False, + action="store_true", + help="Do not send any jobs to Ganeti backend.") + parser.add_option("--ganeti-dry-run", dest="ganeti_dry_run", default=False, + action="store_true", + help="Pass --dry-run option to Ganeti jobs.") + parser.add_option("--parallel", dest="parallel", default=False, + action="store_true", + help="Use a seperate process for each backend.") + parser.add_option("-d", "--debug", dest="debug", default=False, + action="store_true", + help="Display debug information.") + options, args = parser.parse_args() + + if options.backend_id: + backends = [get_resource("backend", options.backend_id)] + else: + if Backend.objects.filter(offline=True).exists(): + msg = "Can not update intances. An 'offline' backend exists." + raise Exception(msg) + backends = Backend.objects.all() + + if options.debug: + logger.setLevel(logging.DEBUG) + + if len(backends) > 1 and options.parallel: + cmd = sys.argv + processes = [] + for backend in backends: + p = subprocess.Popen(cmd + ["--backend-id=%s" % backend.id]) + processes.append(p) + for p in processes: + p.wait() + return + else: + [upgrade_backend(b, options.dry_run, options.ganeti_dry_run) + for b in backends] + return + + +def upgrade_backend(backend, dry_run, ganeti_dry_run): + jobs = [] + instances_ids = get_instances_with_anonymous_disks(backend) + for vm in backend.virtual_machines.filter(id__in=instances_ids): + jobs.append(gevent.spawn(upgrade_vm, vm, dry_run, ganeti_dry_run)) + + if jobs: + for job_chunk in [jobs[x:x+25] for x in range(0, len(jobs), 25)]: + gevent.joinall(jobs) + else: + logger.info("No anonymous disks in backend '%s'. Nothing to do!", + backend.clustername) + return + + +def get_instances_with_anonymous_disks(backend): + """Get all Ganeti instances that have Disks without names.""" + with pooled_rapi_client(backend) as rc: + instances = rc.GetInstances(bulk=True) + # Filter snf- instances + instances = filter(lambda i: + i["name"].startswith(settings.BACKEND_PREFIX_ID), + instances) + # Filter instances with anonymous disks + instances = filter(lambda i: None in i["disk.names"], instances) + # Get IDs of those instances + instances_ids = map(lambda i: + i["name"].replace(settings.BACKEND_PREFIX_ID, "", 1), + instances) + return instances_ids + + +def upgrade_vm(vm, dry_run, ganeti_dry_run): + """Add names to Ganeti Disks.""" + logger.info("Updating disks of instance %s" % vm.backend_vm_id) + index_to_uuid = {} + # Compute new Disk names + for vol in vm.volumes.exclude(status="DELETING"): + if vol.index is None: + msg = ("Cannot update disk '%s'. The index of the disk is unknown." + " Please run snf-manage reconcile-servers --fix-all and" + " retry!") + logger.critical(msg) + continue + uuid = vol.backend_volume_uuid + # Map index -> UUID + index_to_uuid[vol.index] = uuid + + renamed_disks = [("modify", index, {"name": name}) + for index, name in index_to_uuid.items()] + + #instance = vm.backend_vm_id + with pooled_rapi_client(vm) as rc: + # Add names to disks + logger.debug("Modifying disks of instance '%s'. New disks: '%s'", + vm.backend_vm_id, renamed_disks) + if not dry_run: + rc.ModifyInstance(vm.backend_vm_id, + disks=renamed_disks, dry_run=ganeti_dry_run) + close_connection() + + +if __name__ == "__main__": + main() + sys.exit(0) diff --git a/snf-cyclades-app/synnefo/tools/add_unique_name_to_nics b/snf-cyclades-app/synnefo/tools/add_unique_name_to_nics index 5fa33843ed7381eb567383e757a4d85898a63d06..6f27d041528519034b379041501df534aa026224 100755 --- a/snf-cyclades-app/synnefo/tools/add_unique_name_to_nics +++ b/snf-cyclades-app/synnefo/tools/add_unique_name_to_nics @@ -29,7 +29,7 @@ setup_environ(settings) from django.db import close_connection from synnefo.db.models import Backend, pooled_rapi_client -from synnefo.management.common import get_backend +from synnefo.management.common import get_resource import logging logger = logging.getLogger("migrate_nics") @@ -67,7 +67,7 @@ def main(): options, args = parser.parse_args() if options.backend_id: - backends = [get_backend(options.backend_id)] + backends = [get_resource("backend", options.backend_id)] else: if Backend.objects.filter(offline=True).exists(): msg = "Can not update intances. An 'offline' backend exists." diff --git a/snf-cyclades-app/synnefo/tools/update_to_floating_ips b/snf-cyclades-app/synnefo/tools/update_to_floating_ips index 955dad062b756b2f6560ce68ea8ed4804a0530c1..a98729bf44a8c59b9c3d7e1deeda0bfef5689b18 100755 --- a/snf-cyclades-app/synnefo/tools/update_to_floating_ips +++ b/snf-cyclades-app/synnefo/tools/update_to_floating_ips @@ -15,7 +15,7 @@ setup_environ(settings) from synnefo.management.common import get_network from synnefo.db.models import IPAddress -from django.db import transaction +from synnefo.db import transaction logger = logging.getLogger("update_floating_ips") handler = logging.StreamHandler() diff --git a/snf-cyclades-app/synnefo/ui/__init__.py b/snf-cyclades-app/synnefo/ui/__init__.py index 5be1bb67e270877dfb2eae4c385d74278597ad7c..908dd6f9a1a10e8706b1d1efea72847a942c7da9 100644 --- a/snf-cyclades-app/synnefo/ui/__init__.py +++ b/snf-cyclades-app/synnefo/ui/__init__.py @@ -1,33 +1,15 @@ -# Copyright 2011 GRNET S.A. All rights reserved. -# -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: -# -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. -# -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # diff --git a/snf-cyclades-app/synnefo/ui/models.py b/snf-cyclades-app/synnefo/ui/models.py index 83aa38b3d381987278ab02461723b6db72cf493d..62911f2d9287002abf56ce65ad13f289c4a7d6e6 100644 --- a/snf-cyclades-app/synnefo/ui/models.py +++ b/snf-cyclades-app/synnefo/ui/models.py @@ -1,35 +1,17 @@ -# Copyright 2011 GRNET S.A. All rights reserved. -# -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: -# -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. -# -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # from django.db import models diff --git a/snf-cyclades-app/synnefo/ui/settings.py b/snf-cyclades-app/synnefo/ui/settings.py index 0058cfe8212a8cbdc32a6f3c982447c2eee1c429..0d70eca6e98ac73cdd244421b167371ed2cd8a9f 100644 --- a/snf-cyclades-app/synnefo/ui/settings.py +++ b/snf-cyclades-app/synnefo/ui/settings.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # import logging @@ -52,6 +34,7 @@ if not BASE_PATH.startswith("/"): cyclades_services = cyclades_settings.cyclades_services +VOLUME_URL = endpoint(cyclades_services, 'volume', 'v2.0').rstrip('/') GLANCE_URL = endpoint(cyclades_services, 'image', 'v1.0').rstrip('/') COMPUTE_URL = endpoint(cyclades_services, 'compute', 'v2.0').rstrip('/') NETWORK_URL = endpoint(cyclades_services, 'network', 'v2.0').rstrip('/') diff --git a/snf-cyclades-app/synnefo/ui/static/snf/css/main.css b/snf-cyclades-app/synnefo/ui/static/snf/css/main.css index 8cf29f74d59ff91ba5b12a613c4056c739bd3ca7..a72be40f96007fa6cf6c7d6a57e251f13ca31a6d 100644 --- a/snf-cyclades-app/synnefo/ui/static/snf/css/main.css +++ b/snf-cyclades-app/synnefo/ui/static/snf/css/main.css @@ -155,6 +155,8 @@ h5 { .tab-name, .machine-container .name, h5.namecontainer span, +.entry h3 span, +.nested-model-list .model-item .inner.main-content .title, .single .machine-detail.title { font-family: 'Ubuntu', sans-serif; } @@ -610,8 +612,10 @@ div#footer-text{ width: 140px; } -.overlay-networks-create .form-action { - float: left; +.overlay-networks-create .clear { + float: none; + width: 100%; + margin-top: 5px !important; } .overlay-networks-create .fixpos { @@ -638,6 +642,13 @@ form .fields-desc { padding: 1px; } +.overlay-networks-create .col-fields .form-field.project-select { + float: none; + margin: 0; + border-bottom: none; + height: 34px; +} + .overlay-networks-create #network-create-subnet-custom { width: 125px; } @@ -733,33 +744,10 @@ div.image-container { padding-left: 50px; } -/* slider root element */ -.slider { - border: 1px solid #666; - cursor: pointer; - display: inline !important; - float: left; - margin: 5px 0 20px 10px; - position: relative; - width: 250px; -} - -.sliders { - float:left; - width: 40px; - margin-left: 10px; - margin-top: 3px; -} - .units { padding-left:10px; } -.slider-container { - padding-bottom: 15px; - margin-left: 5px; -} - /* drag handle */ .handle { -moz-box-shadow: 0 0 2px #000000; @@ -857,7 +845,7 @@ div.image-container { #machinesview .build-progress .btn { cursor: pointer; - background-color: #5e1616; + background-color: #666666; color: #FFF; padding: 3px; } @@ -876,7 +864,7 @@ div.image-container { } .icon .machine-info { - padding: 20px 0; + padding: 28px 0; } .icon .machine-details .name { @@ -913,7 +901,7 @@ div.image-container { .icon .vm-actions, .network .vm-actions { width: 180px; - height: 96px; + height: 111px; float: left; } @@ -991,11 +979,14 @@ div.list div.actions a.enabled.destroy { .vm-actions .action-container.destroy a { } -.icon .light-background .machine-data, div.network.light-background, div.network.expand { +.icon .light-background .machine-data, +.icon .action-pending .machine-data, +div.network.light-background, div.network.expand { background-color:#aed2e3 !important; } -.single .light-background { +.single .light-background, +.single .action-pending { background-color:#aed2e3; } @@ -1061,8 +1052,8 @@ div.machine div.actions .disabled { } div.machine div.actions .disabled-visible a, -div.machine div.vm-actions .disabled-visible a { - color: #aaa; +div.vm-actions .disabled-visible a { + color: #aaa !important; } div.single-container div.vm-actions .disabled { @@ -1085,7 +1076,7 @@ div.connect-arrow { display: none; left: -3px; position: absolute; - top: 22px; + top: 30px; } div.connect-arrow:hover, div.connect-arrow.border-hover, div.connect-arrow-ie, div.connect-arrow.border-ie { @@ -1113,7 +1104,7 @@ div.connect-border:hover { display: none; left: -15px; position: absolute; - top: 22px; + top: 30px; } .standard .machine .logo { @@ -1152,7 +1143,7 @@ div.connect-border:hover { margin: 0; } -.icon div.cont-toggler-wrapper.ips { +.icon div.cont-toggler-wrapper { margin-top: 2px; font-size: 75%; } @@ -1186,6 +1177,14 @@ div.indicator1, div.indicator2, div.indicator3, div.indicator4 { background: transparent; } +.connecting-state .indicator1, .connecting-state .indicator2, .connecting-state .indicator3, .connecting-state .indicator4 { + background-color: #d4aa00; +} + +.disconnecting-state .indicator1, .disconnecting-state .indicator2, .disconnecting-state .indicator3, .disconnecting-state .indicator4 { + background-color: #d4aa00; +} + .running-state .indicator1, .running-state .indicator2, .running-state .indicator3, .running-state .indicator4 { background-color: #63cf1c; } @@ -1203,7 +1202,7 @@ div.indicator1, div.indicator2, div.indicator3, div.indicator4 { } .build-state .indicator1, .build-state .indicator2, .build-state .indicator3, .build-state .indicator4 { - background-color: #FF7F2A; + background-color: #4085a5 !important; } .destroying-state .indicator1, .destroying-state .indicator3, .destroying-state .indicator2, .destroying-state .indicator4 { @@ -2536,8 +2535,12 @@ div.list table thead .vmname { margin: 10px 6px 0 15px; } +.progress-indicator.spinner { + margin-top: 50px; +} + .icon .spinner { - margin: 20px 4px 0 15px !important; + margin: 33px 4px 0 15px !important; } .single .state .spinner { @@ -3244,40 +3247,60 @@ div.reboot-dialog button.details:hover { display: none; } -.machine .info-content.ips .collection { + +.machine .info-content.inner-collection .model-item .actions-content.inline { + right: -180px; + padding-top: 0px; +} + +.machine .info-content.inner-collection .inner-item .nested .actions-content.inline { + padding-top: 6px !important; +} + +.machine .info-content.inner-collection .collection { padding: 0; } -.machine .info-content.ips .ips .model-item .subnet { +.machine .info-content.inner-collection .nested .model-item .subnet { padding-top: 2px; width: 35%; float: left; } -.machine .info-content.ips .ips .port-ip-item:last-child { +.machine .info-content.inner-collection .nested-item:last-child { margin-bottom: 0; } -.machine .info-content.ips.ips .port-item:hover { +.machine .info-content.inner-collection .inner-item:hover { background-color: #75A7BF; } -.machine .info-content.ips .ips .port-ip-item { +.machine .info-content.volumes.inner-collection .nested-item { + margin-bottom:0px; +} + +.machine .info-content.inner-collection .nested-item { padding: 6px; margin-bottom: 2px; } -.machine .info-content.ips .ips .model-item .ip { +.machine .info-content.inner-collection .model-item .title-display { padding-top: 4px; width: 55%; float: left; } -.single .machine .info-content.ips .ips .model-item .ip { +.single .machine .info-content.inner-collection .model-item .title-display { padding-top: 5px; } -.machine .info-content.ips .ips .model-item .type { +.machine .info-content.inner-collection .index-display { + float: left; + padding: 4px; + margin-right: 7px; +} + +.machine .info-content.inner-collection .type-display { float: left; margin-right: 10px; margin-top: 1px; @@ -3285,49 +3308,54 @@ div.reboot-dialog button.details:hover { color: #fff; background-color: #4085A5; padding: 3px; + min-width: 22px; + text-align: center; +} + +.machine .info-content.inner-collection .volume-item .type-display { + min-width: 26px; } -.single .machine .info-content.ips .ips .model-item .ip { +.single .machine .info-content.inner-collection .model-item .title { width: 85%; } -.single .machine .info-content.ips .ips .model-item .cidr { +.single .machine .info-content.inner-collection .model-item .cidr { padding-left: 27px; } -.single .machine .info-content.ips .ips .model-item .type { +.single .machine .info-content.inner-collection .type-display { font-size: 0.9em; padding: 1px 4px; margin-top: 4px; } -.machine .info-content.ips .port { +.machine .info-content.inner-collection .port { width: 77%; float: left; margin-top: 9px; } -.machine .info-content.ips .ips { +.machine .info-content.inner-collection .nested { width: 50%; float: left; } -.machine .info-content.ips .port-item:last-child { +.machine .info-content.inner-collection .inner-item:last-child { border-bottom: none; - padding-bottom: 0; margin-bottom: 0; } -.machine .info-content.ips .port-item { +.machine .info-content.inner-collection .inner-item { border-bottom: 1px solid #75A9C1; } -.machine .info-content.ips .port-item img.in-progress { +.machine .info-content.inner-collection .inner-item img.in-progress { margin: 4px 2px; margin-right: 20px; } -.machine .info-content.ips .port-item img { +.machine .info-content.inner-collection .inner-item img { float: left; position: relative; left: 4px; @@ -3335,44 +3363,44 @@ div.reboot-dialog button.details:hover { margin-right: 10px; } -.single .machine .info-content.ips .port-item img { +.single .machine .info-content.inner-collection .inner-item img { margin-top: 13px; } -.machine .info-content.ips .port-item .network-header { +.machine .info-content.inner-collection .inner-item .network-header { width: 50%; float: left; } -.single .machine .info-content.ips .port-item .network-header { +.single .machine .info-content.inner-collection .inner-item .network-header { width: 100%; float: none; } -.single .machine .info-content.ips .ips { +.single .machine .info-content.inner-collection .nested { width: 100%; } -.single .machine .info-content.ips .ips, -.single .machine .info-content.ips .port-item .network-header { +.single .machine .info-content.inner-collection .nested, +.single .machine .info-content.inner-collection .inner-item .network-header { float: none; } -.single .machine .info-content.ips .port-item .network-header { +.single .machine .info-content.inner-collection .inner-item .network-header { padding: 5px; margin-top: -10px; } -.machine .info-content.ips .port-item .port { +.machine .info-content.inner-collection .inner-item .port { } -.single .machine .info-content.ips { +.single .machine .info-content.inner-collection { font-size: 0.8em; color: #222; } -.machine .info-content.ips { +.machine .info-content.inner-collection { background-color: #84B7D0; padding: 0px 0px; font-size: 0.6em; @@ -3568,6 +3596,11 @@ div.machine a.manage-metadata:hover { width: 140px; } +.single .column1 .project-name-cont { + margin-top: 27px; + text-align: center; +} + .single .column1 .state { float: left; margin-left: 4px; @@ -3597,7 +3630,7 @@ div.machine a.manage-metadata:hover { .single .single-actions { width: 150px; - height: 77px; + height: 80px; margin-bottom: 45px; margin-left: -10px; } @@ -3672,6 +3705,7 @@ div.machine a.manage-metadata:hover { line-height: 17px; margin: 0 0 10px 5px; width: 358px; + margin-bottom: 80px; } .single .column2 .machine-labels { @@ -3744,10 +3778,12 @@ div.machine a.manage-metadata:hover { font-size: 0.9em !important; } +.single .volumes-content.toggler-content, .single .ips-content.toggler-content { padding: 0; } +.single .volumes-content.toggler-content .action-container, .single .ips-content.toggler-content .action-container { display: none !important; } @@ -3964,7 +4000,6 @@ div.single div.column3 div.server-name:hover { background: transparent; margin-bottom: 10px; overflow: visible; - overflow: hidden; width: 700px; background: #EFF7FA repeat scroll 0 0; } @@ -4110,7 +4145,7 @@ div.console-footer { .icon .wave { margin-right: 4px !important; - margin-top: 15px !important; + margin-top: 33px !important; } .icon .status { @@ -4127,7 +4162,7 @@ div.console-footer { } .icon div.action-indicator { - margin-top: 14px; + margin-top: 30px; margin-right: 4px; } @@ -4268,13 +4303,14 @@ h3.overlay-inner-title { } #creation-password-overlay .password { - font-size: 1.5em; + font-size: 1.5em !important; font-weight: bold; letter-spacing: 2px; - font-family: Georgia, Times, serif; - margin-right: 5em; + font-family: Georgia, Times, serif !important; + margin-right: 2em; float: right; margin-top: -4px; + width: 200px; } .feedback-form .description { @@ -4430,6 +4466,7 @@ h3.overlay-inner-title { } .fqdn { + width: 100%; } .column2 .fqdn { @@ -4445,6 +4482,11 @@ h3.overlay-inner-title { z-index: 50000; } +.tooltip.warning { + background-color: #940606; + color: #FFFFFF; +} + /*404 and 500 pages*/ .error_page { @@ -4603,37 +4645,6 @@ tbody.machines .spinner { float: none !important; } -.slider .slider-point { - width: 4px; - height: 3px; - margin-left: 1px; - background-color: transparent; - display: block; - position: absolute; - z-index: 10; - bottom: 0px; -} - -.slider .slider-point-light { - background-color: transparent; -} - -.slider-point-text { - font-size: 0.6em; - position: absolute; - top: 4px; - border-top: 5px solid #C5DEE9; - padding: 3px; - color: #4085A5; - display: block; - min-width: 6px; - text-align: middle; -} - -.slider .handle { - z-index: 50; -} - .modal p.desc { margin: 5px 0; margin-left: 37px; @@ -4746,10 +4757,6 @@ table.list-machines .wave { width: 600px; } -.overlay.overlay-createvm { - width: 640px; -} - .overlay a { color: #387693; } @@ -5233,7 +5240,7 @@ table.list-machines .wave { } #network-vms-select-content .empty-list { - font-size: 1.2em; + font-size: 1.2em !important; } #network-vms-select-content li.options-object .value { @@ -5367,53 +5374,54 @@ table.list-machines .wave { color: #fff; } -#createvm-overlay-content { - padding: 0; +.overlay-wizard .container { + width: 624px !important; } -.overlay-createvm .container { - width: 624px !important; +.overlay.overlay-wizard { + width: 640px; } -.create-vm .vm-network .list-cont.personalize-cont:last-child .confirm-params { +.create-wizard-overlay .vm-network .list-cont.personalize-cont:last-child .confirm-params { margin-right: 0!important; } -.create-vm .vm-network .list-cont.personalize-cont:last-child { +.create-wizard-overlay .list-cont.personalize-cont.noborder, +.create-wizard-overlay .vm-network .list-cont.personalize-cont:last-child { border-right: none; margin-right: 0; width: 298px; } -.create-vm .vm-network .list-cont.personalize-cont .confirm-params { +.create-wizard-overlay .vm-network .list-cont.personalize-cont .confirm-params { max-height: 240px; } -.create-vm .vm-network .list-cont.personalize-cont { +.create-wizard-overlay .vm-network .list-cont.personalize-cont { height: 330px; } -.create-vm .vm-network .list-cont.personalize-cont { +.create-wizard-overlay .vm-network .list-cont.personalize-cont { width: 47%; } -.create-vm .header-step.current { +.create-wizard-overlay .header-step.current { font-weight: bold; } -.create-vm .create-step-cont { +.create-wizard-overlay .create-step-cont { min-height: 240px; } -.create-vm .create-controls { +.create-wizard-overlay .create-controls { padding: 10px; } -.create-vm ul li { +.create-wizard-overlay ul li { cursor: pointer; padding: 4px; } -.create-vm ul li.selected { +.create-wizard-overlay ul li.selected { background-color: #aaa; } @@ -5474,6 +5482,12 @@ table.list-machines .wave { margin-right: 10px; } +.form-field.error textarea, +.form-field.error select, +.form-field.error input { + border-color: #f00 !important; +} + .form-field.error label { color: #ff0000; text-decoration: underline; @@ -5498,6 +5512,18 @@ table.list-machines .wave { border-top: 0px; } +.create-volume .no-project-notice { + padding: 4px; + margin-right: 6px; +} + +.no-project-notice { + float: right; + font-size: 0.9em; + color: #800; + padding: 10px; +} + .form-action { float: right; min-width: 140px; @@ -5527,6 +5553,11 @@ table.list-machines .wave { color: #fff; } +.form-action.alt { + min-width: 100px; + margin-right: 5px; +} + .form-action.next, .form-action.ok { background-color: #080; @@ -5556,38 +5587,34 @@ table.list-machines .wave { color: transparent; } -#createvm-overlay-content { - padding: 0; -} - -.create-vm .image-details.selected .size { +.create-wizard-overlay .image-details.selected .size { } -.create-vm .image-details .show-details:hover { +.create-wizard-overlay .image-details .show-details:hover { color: #aaa !important; } -.create-vm .image-details.selected .show-details:hover { +.create-wizard-overlay .image-details.selected .show-details:hover { color: #fff !important; } -.create-vm .image-details.selected .show-details, -.create-vm .image-details.selected .size { +.create-wizard-overlay .image-details.selected .show-details, +.create-wizard-overlay .image-details.selected .size { color: #eee; } -.create-vm .image-details.selected span.owner { +.create-wizard-overlay .image-details.selected span.owner { color: #fff; } -.create-vm .image-details p { +.create-wizard-overlay .image-details p { font-size: 0.8em; padding-left: 27px; display:block; } -.create-vm .image-details span.owner { +.create-wizard-overlay .image-details span.owner { display: block; font-size: 0.9em; float: right; @@ -5597,7 +5624,7 @@ table.list-machines .wave { right: 5px; } -.create-vm .select-image .show-details { +.create-wizard-overlay .select-image .show-details { display: none; font-size: 0.8em; text-decoration: underline; @@ -5607,47 +5634,109 @@ table.list-machines .wave { right: 5px; } -.create-vm .image-details .size { +.create-wizard-overlay .image-details .size { margin-top: 2px; font-size: 0.8em; color: #aaa; margin-left: 10px; } -.create-vm .step-cont { +.create-wizard-overlay .step-cont { margin: 15px; } -.create-vm .create-step-cont { +.create-wizard-overlay.create-volume .steps-history +.steps-history-cont span.description { + width: 280px; +} + +.create-wizard-overlay.create-volume .steps-history +.steps-history-cont .title { + width: 150px; + text-align: left; +} + +.create-wizard-overlay.create-volume .vms-list { + max-height: 290px; + overflow: auto; +} + +.create-wizard-overlay.create-volume .volume-description { + margin-top: 10px; +} + +.create-wizard-overlay.create-volume h3.item-name { + background-image: url("../images/volume-icon-small.png"); +} + +.create-wizard-overlay.create-volume .rename input.volume-name { + background-image: url("../images/volume-icon-small.png"); + padding-left: 30px !important; + width: 93.5% !important; +} + +.create-wizard-overlay.create-volume .volume-description textarea { + width: 98%; + height: 250px; + padding: 4px; + max-width: 582px; + max-height: 250px; +} + +.create-wizard-overlay.create-volume .vms-list .model-item .indicators { + float: right; +} + +.create-wizard-overlay.create-volume .vms-list .model-item .ico { + margin: 0 5px; +} + +.create-wizard-overlay.create-volume .vms-list .model-item .name { + width: 82%; +} + +.create-wizard-overlay.create-volume .vms-list .model-item .state span { + display: none; +} + +.create-wizard-overlay.create-volume .step-cont .project-select { + margin-bottom: 10px; +} + +.overlay-info.overlay-ip-create .project-select { + margin-bottom: 17px; +} + +.create-wizard-overlay .create-step-cont { min-height: 250px; float: left; width: 624px; } -.create-vm .create-controls { +.create-wizard-overlay .create-controls { padding: 10px; border-top: 1px solid #ddd; } -.create-vm .empty { +.create-wizard-overlay .empty { font-size: 0.8em; color: #444; } -.create-vm h4 { +.create-wizard-overlay h4 { color: #5CA1C0; margin-bottom: 0.5em; font-family: arial; } -.create-vm ul li { +.create-wizard-overlay ul li { cursor: pointer; padding: 4px; font-size: 0.9em; } -.create-vm .create-step-cont li.ssh-key-option.selected, -.create-vm .create-step-cont li.list-item-option.selected, -.create-vm ul li.selected { +.create-wizard-overlay .create-step-cont li.ssh-key-option.selected, +.create-wizard-overlay .create-step-cont li.list-item-option.selected, +.create-wizard-overlay ul li.selected { background-color: #FF7F2A; background-image:linear-gradient(top, #FF9955, #E88B4D); background-image:-webkit-linear-gradient(top, #FF9955, #E88B4D); @@ -5656,11 +5745,11 @@ table.list-machines .wave { color: #fff; } -.create-vm .images-list-cont.loading .loading-indicator { +.create-wizard-overlay .images-list-cont.loading .loading-indicator { display: block !important; } -.create-vm .images-list-cont .loading-indicator { +.create-wizard-overlay .images-list-cont .loading-indicator { display: none; position: absolute; right: -13px; @@ -5671,44 +5760,51 @@ table.list-machines .wave { background-image: url("../images/icons/indicators/medium/progress.gif"); } -.create-vm .images-list-cont h4 { +.create-wizard-overlay .images-list-cont h4 { position: relative; } -.create-vm .images-list-cont { +.create-wizard-overlay .images-list-cont { width: 40%; float: left; padding-left: 3%; padding-right: 3%; } -.create-vm li p.desc { +.create-wizard-overlay li p.desc { font-size: 0.9em; } -.create-vm p.desc.warning { +.create-wizard-overlay p.desc.warning { color: #880000; } -.create-vm p.desc.empty { +.create-wizard-overlay p.desc.empty { color: #000; } -.create-vm p.desc { +.create-wizard-overlay p.desc { font-size: 0.8em; color: #888; margin-bottom: 10px; } -.create-vm li.role .values .val:hover { +.create-wizard-overlay li.role .values .val:hover { background-color: #eee; } -.create-vm li.role .values .val.selected, .create-vm li.role .values .val.selected:hover { + +.create-wizard-overlay li.role .values .val.selected, +.create-vm li.role .values .val.selected:hover { color: #fff; background-color: #FF9955; } -.create-vm .images-filter-cont, .create-vm .flavors-predefined-cont { +.create-wizard-overlay .images-filters .cont { + margin-bottom: 15px; +} + +.create-wizard-overlay .images-filter-cont, +.create-vm .flavors-predefined-cont { width: 18%; padding-right: 4%; float:left; @@ -5716,13 +5812,45 @@ table.list-machines .wave { overflow: auto; } -.create-vm .flavor-options-cont { +.create-vm .project-select { + margin-top: 0 !important; + margin-bottom: 7px !important; +} + +.create-vm .images-list-cont.content-cont, +.create-vm .images-filter-cont.content-cont { + height: 370px; +} + +.overlay-vm-resize .create-vm .flavor-options-cont { + height: auto !important; +} + +.create-vm .flavor-options-cont, +.create-vm .flavors-predefined-cont { + height: 327px !important; +} + +.create-wizard-overlay .flavor-options-cont.volume.name h4 { + border: none; +} + +.create-wizard-overlay .flavor-options-cont.volume { + height: auto; +} + +.create-wizard-overlay .flavor-options-cont.wide { + width: 99%; + margin-left: 3px; +} + +.create-wizard-overlay .flavor-options-cont { width: 74%; float: left; margin-left: 20px; } -.create-vm .flavor-options-cont .flavor-options li:hover { +.create-wizard-overlay .flavor-options-cont .flavor-options li:hover { background-image:-webkit-linear-gradient(top, #E8F4FA, #D1E7F0); background-image:-o-linear-gradient(top, #E8F4FA, #D1E7F0); background-image:-moz-linear-gradient(top, #E8F4FA, #D1E7F0); @@ -5730,11 +5858,11 @@ table.list-machines .wave { filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#E8F4FA', endColorstr='#D1E7F0', GradientType=0); } -.create-vm .flavor-options-cont .flavor-options li.disabled * { +.create-wizard-overlay .flavor-options-cont .flavor-options li.disabled * { color: #eee !important; } -.create-vm .flavor-options-cont .flavor-options li.disabled { +.create-wizard-overlay .flavor-options-cont .flavor-options li.disabled { background-image:linear-gradient(top, #aaa, #ddd); background-image:-webkit-linear-gradient(top, #aaa, #ddd); background-image:-o-linear-gradient(top, #aaa, #ddd); @@ -5742,7 +5870,7 @@ table.list-machines .wave { filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#aaaaaa', endColorstr='#dddddd', GradientType=0); } -.create-vm .flavor-options-cont .flavor-options li.selected { +.create-wizard-overlay .flavor-options-cont .flavor-options li.selected { background-color: #FF9955; background-image:linear-gradient(top, #FF9955, #E88B4D); background-image:-webkit-linear-gradient(top, #FF9955, #E88B4D); @@ -5752,7 +5880,7 @@ table.list-machines .wave { border: 1px solid #C97943; } -.create-vm .flavor-options-cont .flavor-options li.selected.disabled { +.create-wizard-overlay .flavor-options-cont .flavor-options li.selected.disabled { background-color: #AAA; background-image:linear-gradient(top, #AAA, #E88B4D); background-image:-webkit-linear-gradient(top, #AAA, #E88B4D); @@ -5762,11 +5890,11 @@ table.list-machines .wave { border: 1px solid #999; } -.create-vm .predefined-list li.disabled { +.create-wizard-overlay .predefined-list li.disabled { color: #ddd !important; } -.create-vm .flavor-options-cont .flavor-options li { +.create-wizard-overlay .flavor-options-cont .flavor-options li { display: block; float: left; margin-right: 10px; @@ -5780,32 +5908,32 @@ table.list-machines .wave { filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#D1E7F0', endColorstr='#E8F4FA', GradientType=0); } -.create-vm .flavor-opts-list.compact li { +.create-wizard-overlay .flavor-opts-list.compact li { padding: 7px 9px; margin-right: 7px; } -.create-vm .flavor-options-cont .flavor-options { +.create-wizard-overlay .flavor-options-cont .flavor-options { margin-bottom: 2px; } -.create-vm .flavor-options .metric { +.create-wizard-overlay .flavor-options .metric { font-size: 0.8em; margin-left: 2px; } -.create-vm .flavor-options span.available { +.create-wizard-overlay .flavor-options span.available { font-size: 0.8em; font-weight: normal; margin-left: 5px; } -.create-vm .flavor-options span.title { +.create-wizard-overlay .flavor-options span.title { color: #444; } -.create-vm .flavor-options span.desc { +.create-wizard-overlay .flavor-options span.desc { display: block; color: #aaa; font-weight: normal; @@ -5819,18 +5947,18 @@ table.list-machines .wave { top: 20px; } -.create-vm .flavor-options .flavors-disk-template-list { +.create-wizard-overlay .flavor-options .flavors-disk-template-list { position: relative; } -.create-vm .flavor-options .disk_template.option { +.create-wizard-overlay .flavor-options .disk_template.option { z-index: 10; min-width: 50px; text-align: center; } /*0 position is -470px*/ -.create-vm .flavor-options .disk-template-description { +.create-wizard-overlay .flavor-options .disk-template-description { font-size: 0.8em; color: #444; background-image: url("../images/horizontal-pointer.png"); @@ -5846,7 +5974,7 @@ table.list-machines .wave { width: 100%; } -.create-vm .flavor-options .disk_template.option .description { +.create-wizard-overlay .flavor-options .disk_template.option .description { display: none; position: absolute; bottom: -20px; @@ -5863,36 +5991,36 @@ table.list-machines .wave { display: none; } -.create-vm .flavor-options .selected .value { +.create-wizard-overlay .flavor-options .selected .value { color: #FFF; } -.create-vm .flavor-options .value { +.create-wizard-overlay .flavor-options .value { font-weight: bold; color: #5CA1C0; } -.create-vm .flavor-options-cont h4 { +.create-wizard-overlay .flavor-options-cont h4 { border-bottom: 1px solid #A1C8DB; padding-bottom: 5px; } -.create-vm .images-info-cont { +.create-wizard-overlay .images-info-cont { width: 28%; padding-left: 3%; float: left; border-left: 1px solid #A1C8DB; } -.create-vm .select-image.wide .show-details { +.create-wizard-overlay .select-image.wide .show-details { display: inline; } -.create-vm .select-image .images-info-cont .hide { +.create-wizard-overlay .select-image .images-info-cont .hide { display: none; } -.create-vm .select-image.wide .images-info-cont .hide { +.create-wizard-overlay .select-image.wide .images-info-cont .hide { display: block; float: right; position: absolute; @@ -5904,34 +6032,41 @@ table.list-machines .wave { cursor: pointer; } -.create-vm .select-image.wide .images-list-cont { +.create-wizard-overlay .select-image.wide .images-list-cont { width: 74%; padding-right: 0; } -.create-vm .select-image.wide .images-info-cont .description .title { +.create-wizard-overlay .select-image.wide .images-info-cont .description .title { display: none; float: none; } -.create-vm .select-image.wide .images-info-cont .description p { +.create-wizard-overlay .select-image.wide .images-info-cont .description p { background-color: #fff; border: 1px solid #ddd; padding: 10px; float: none; } -.create-vm .select-image.wide .selected .size { +.create-wizard-overlay .select-image.wide .selected .size { color: #FFF !important; } -.create-vm .select-image.wide .image-details .size { +.create-wizard-overlay .select-image.wide .image-details .size { color: #5CA1C0; - position: absolute; + position: relative; top: 5px; + display: block; + left: 17px; + top: -3px; +} + +.create-wizard-overlay .select-image.wide .image-details.disabled { + background-color: #aaa; } -.create-vm .select-image.wide .images-info-cont h3 { +.create-wizard-overlay .select-image.wide .images-info-cont h3 { color: #5CA1C0; margin: 10px 0; margin-top: 5px; @@ -5939,32 +6074,32 @@ table.list-machines .wave { font-size: 0.9em; } -.create-vm .select-image.wide .images-info-cont .description p { +.create-wizard-overlay .select-image.wide .images-info-cont .description p { height: 50px; } -.create-vm .select-image.wide .images-info-cont .description { +.create-wizard-overlay .select-image.wide .images-info-cont .description { width: 100% !important; float: none !important; background-color: transparent !important; padding: 0 !important; font-size: 1.1em; } -.create-vm .select-image.wide .images-info-cont .extra-details { +.create-wizard-overlay .select-image.wide .images-info-cont .extra-details { height: 160px; overflow-y: scroll; padding-right: 15px; } -.create-vm .select-image.wide .images-info-cont .extra-details .image-detail.extra-meta .title .custom { +.create-wizard-overlay .select-image.wide .images-info-cont .extra-details .image-detail.extra-meta .title .custom { display: inline-block; } -.create-vm .select-image.wide .images-info-cont .extra-details .image-detail.extra-meta .title { +.create-wizard-overlay .select-image.wide .images-info-cont .extra-details .image-detail.extra-meta .title { background-color: #999 !important; border-color: #888 !important; } -.create-vm .select-image.wide .images-info-cont .extra-details .image-detail .custom { +.create-wizard-overlay .select-image.wide .images-info-cont .extra-details .image-detail .custom { float: right; display: none; font-size: 0.8em; @@ -5973,13 +6108,13 @@ table.list-machines .wave { margin-top: 2px; } -.create-vm .select-image.wide .images-info-cont .extra-details .image-detail { +.create-wizard-overlay .select-image.wide .images-info-cont .extra-details .image-detail { padding: 0px; background-color: transparent; margin-bottom: 4px; } -.create-vm .select-image.wide .images-info-cont .extra-details .title { +.create-wizard-overlay .select-image.wide .images-info-cont .extra-details .title { float: left; display: block; width: 39%; @@ -5990,7 +6125,7 @@ table.list-machines .wave { font-size: 0.8em; } -.create-vm .select-image.wide .images-info-cont .extra-details .value { +.create-wizard-overlay .select-image.wide .images-info-cont .extra-details .value { float: right; display: block; width: 55%; @@ -6004,10 +6139,10 @@ table.list-machines .wave { } -.create-vm .select-image.wide .images-info-cont .image-detail { +.create-wizard-overlay .select-image.wide .images-info-cont .image-detail { } -.create-vm .select-image.wide ul.images-list { +.create-wizard-overlay .select-image.wide ul.images-list { height: 310px; overflow-y: scroll; padding-right: 3%; @@ -6020,7 +6155,7 @@ table.list-machines .wave { .flavor-options-cont { } -.create-vm .select-image.wide .images-info-cont { +.create-wizard-overlay .select-image.wide .images-info-cont { position: absolute; width: 88%; background-color: #DAE9F0; @@ -6034,7 +6169,7 @@ table.list-machines .wave { } -.create-vm .images-info-cont h4, .create-vm .create-step-cont .param h4{ +.create-wizard-overlay .images-info-cont h4, .create-vm .create-step-cont .param h4, h4.project-name { color: #FF9955; margin-bottom: 1em; font-size: 1.2em; @@ -6043,14 +6178,14 @@ table.list-machines .wave { margin-left: 20px; } -.create-vm .images-info-cont span.title { +.create-wizard-overlay .images-info-cont span.title { color: #4085A5; display: block; margin-bottom: 2px; font-size: 0.8em; } -.create-vm .type-filter li { +.create-wizard-overlay .type-filter li { font-size: 0.8em; /*font-weight: bold;*/ padding: 4px; @@ -6058,97 +6193,99 @@ table.list-machines .wave { color: #FF7F2A; } -.create-vm .images-list li { +.create-wizard-overlay .images-list #create-vm-image-empty-disk .owner, +.create-wizard-overlay .images-list #create-vm-image-empty-disk .show-details, +.create-wizard-overlay .images-list #create-vm-image-empty-disk .size { + display: none !important; +} + +.create-wizard-overlay .images-list li { min-height: 30px; } -.create-vm .images-list .image-details:hover { +.create-wizard-overlay .images-list .image-details:hover { background-color: #eee; } -.create-vm .images-list .image-details.selected:hover { +.create-wizard-overlay .images-list .image-details.selected:hover { background-color: #FF7F2A; } -.create-vm .images-list .image-details.selected { +.create-wizard-overlay .images-list .image-details.selected { /*font-weight: bold;*/ } -.create-vm .images-list .image-details { +.create-wizard-overlay .images-list .image-details { padding: 6px; margin-bottom:1px; position: relative; + word-wrap: break-word; } -.create-vm .images-list .image-details img { +.create-wizard-overlay .images-list .image-details img { vertical-align: middle; margin-right: 10px; } -.create-vm .images-info-cont .image-detail:last-child p { +.create-wizard-overlay .images-info-cont .image-detail:last-child p { border-bottom: none; } -.create-vm .images-info-cont h4 img { +.create-wizard-overlay .images-info-cont h4 img { vertical-align: middle; margin-right: 7px; margin-bottom: 5px; } -.create-vm .images-info-cont .description p { +.create-wizard-overlay .images-info-cont .description p { font-size: 0.8em; } -.create-vm .images-info-cont p { +.create-wizard-overlay .images-info-cont p { margin-bottom: 7px; font-size: 0.9em; border-bottom: 1px solid #A1C8DB; padding-bottom: 7px; } -.create-vm .step-header { +.create-wizard-overlay .step-header { padding-bottom:0; position: relative; } -.create-vm .step-header .header-step .info span.subtitle { +.create-wizard-overlay .step-header .header-step .info span.subtitle { font-size: 1.2em; color: #fff; font-weight: bold; } -.create-vm .step-header .header-step .info span { +.create-wizard-overlay .step-header .header-step .info span { float: none; text-align: right; } -.create-vm .step-header .header-step .info { +.create-wizard-overlay .step-header .header-step .info { position: absolute; right: 15px; top: 20px; font-size: 0.8em; } -.create-vm .step-header .header-step span { +.create-wizard-overlay .step-header .header-step span { float: left; display: block; } -.create-vm .steps-container { +.create-wizard-overlay .steps-container { width: 2000em; } -.create-vm .step-header .header-step .title { +.create-wizard-overlay .step-header .header-step .title { margin-top: 20px; font-size: 1em; margin-left: 10px; } -#createvm-overlay-content { - width: 624px; - overflow: hidden; -} - -.create-vm .steps-history .steps-history-cont.current .title { +.create-wizard-overlay .steps-history .steps-history-cont.current .title { display: block; top: 23px; left: 43px; @@ -6158,58 +6295,62 @@ table.list-machines .wave { font-family: 'Ubuntu', sans-serif !important; } -.create-vm .steps-history .steps-history-cont.current .subnum { +.create-wizard-overlay .steps-history .steps-history-cont.current .subnum { display: none; float: none; font-size: 0.9em; font-family: 'Ubuntu', sans-serif !important; } -.create-vm .steps-history .steps-history-cont.current .subtitle, -.create-vm .steps-history .steps-history-cont.current .description { +.create-wizard-overlay .steps-history .steps-history-cont.current .subtitle, +.create-wizard-overlay .steps-history .steps-history-cont.current .description { font-family: 'Ubuntu', sans-serif !important; } -.create-vm .steps-history .steps-history-cont.current .info { +.create-wizard-overlay .steps-history .steps-history-cont.current .info { display: block; font-family: 'Ubuntu', sans-serif !important; } -.create-vm .steps-history .steps-history-cont.completed .title, -.create-vm .steps-history .steps-history-cont.completed .num { +.create-wizard-overlay .steps-history .steps-history-cont.completed .title, +.create-wizard-overlay .steps-history .steps-history-cont.completed .num { color: #A1C8DB; } -.create-vm .steps-history .steps-history-cont.completed { +.create-wizard-overlay .steps-history .steps-history-cont.completed { background-color: #4085A5; color: #fff; cursor: pointer; } -.create-vm .steps-history .steps-history-cont.completed .steps-history-step { +.create-wizard-overlay .steps-history .steps-history-cont.completed .steps-history-step { background-image: url("../images/check.png"); } -.create-vm .steps-history .steps-history-cont.current .steps-history-step { +.create-wizard-overlay .steps-history .steps-history-cont.current .steps-history-step { width: 320px; } -.create-vm .steps-history .steps-history-cont.current .num { +.create-wizard-overlay .steps-history .steps-history-cont.current .num { color: #fff; } -.create-vm .steps-history .steps-history-cont.current .info { +.create-wizard-overlay .steps-history .steps-history-cont.current .info { color: #4085A5; font-size: 0.8em; } -.create-vm .steps-history .steps-history-cont.current { +.create-wizard-overlay .steps-history .steps-history-cont.current { background-color: #A1C8DB; color: #fff; width: 416px; } -.create-vm .steps-history-step { +.create-wizard-overlay.create-volume .steps-history .steps-history-cont.current { + width: 416px; +} + +.create-wizard-overlay .steps-history-step { padding: 4px; padding-left: 7px; font-size: 1em; @@ -6221,21 +6362,21 @@ table.list-machines .wave { background-repeat: no-repeat; } -.create-vm .steps-history { +.create-wizard-overlay .steps-history { background-color: #4085A5; } -.create-vm .steps-history .steps-history-cont.last { +.create-wizard-overlay .steps-history .steps-history-cont.last { border-right: none; } -.create-vm .steps-history .steps-history-cont .num { +.create-wizard-overlay .steps-history .steps-history-cont .num { margin-left: 5px; margin-top: -10px; padding-bottom: 10px; } -.create-vm .steps-history .steps-history-cont .title { +.create-wizard-overlay .steps-history .steps-history-cont .title { display: none; position: absolute; bottom: 0px; @@ -6248,12 +6389,12 @@ table.list-machines .wave { font-size: 0.8em; } -.create-vm .steps-history .steps-history-cont .subnum, -.create-vm .steps-history .steps-history-cont .info { +.create-wizard-overlay .steps-history .steps-history-cont .subnum, +.create-wizard-overlay .steps-history .steps-history-cont .info { display: none; } -.create-vm .steps-history .steps-history-cont { +.create-wizard-overlay .steps-history .steps-history-cont { height: 70px; width: 51px; float: left; @@ -6273,7 +6414,7 @@ table.list-machines .wave { border-bottom: 1px solid #aaa; } -.create-vm .step-header .header-step .num { +.create-wizard-overlay .step-header .header-step .num { color: #225871; font-size: 4em; margin-bottom: -5px; @@ -6281,7 +6422,7 @@ table.list-machines .wave { font-weight: normal !important; } -.create-vm .step-header .header-step { +.create-wizard-overlay .step-header .header-step { color: #; margin-bottom: -6px; width: 25%; @@ -6290,23 +6431,23 @@ table.list-machines .wave { float: left; } -.create-vm .step-header .header-step.current { +.create-wizard-overlay .step-header .header-step.current { color: #387693; } -.create-vm .image-filters-title { +.create-wizard-overlay .image-filters-title { margin-top: 1em; margin-bottom: 0.5em; } -.create-vm .create-step-cont span.clear { +.create-wizard-overlay .create-step-cont span.clear { font-size: 0.8em; font-weight: bold; color: #A1C8DB; display: block; } -.create-vm .category-filters li { +.create-wizard-overlay .category-filters li { float:left; display: block; padding: 4px; @@ -6316,26 +6457,35 @@ table.list-machines .wave { margin-bottom: 5px; } -.create-vm .content-cont { +.create-wizard-overlay .content-cont { height: 340px; overflow: auto; } -.create-vm .vm-confirm .confirm-params span.cval { +.create-wizard-overlay .vm-confirm .confirm-params .selected-ip-address .ip { +} + +.create-wizard-overlay .vm-confirm .confirm-params .selected-ip-address .project { + display: block; + font-size: 0.8em; + color: #aaaaaa; +} + +.create-wizard-overlay .vm-confirm .confirm-params span.cval { margin-left: 8px; color: #444; } -.create-vm .vm-confirm .confirm-params span.ckey { +.create-wizard-overlay .vm-confirm .confirm-params span.ckey { color: #4085A5; font-weight: bold; } -.create-vm .vm-confirm .confirm-params { +.create-wizard-overlay .vm-confirm .confirm-params { margin-bottom: 15px; } -.create-vm .vm-confirm h3.vm-name { +.create-wizard-overlay .vm-confirm h3.item-name { background-repeat: no-repeat; background-position: left center; font-size: 1.4em; @@ -6343,12 +6493,12 @@ table.list-machines .wave { color: #4085A5; } -.create-vm .images-list-cont h4 a { +.create-wizard-overlay .images-list-cont h4 a { margin-top: 3px; margin-right: -2px !important; } -.create-vm .images-list-cont h4 a, -.create-vm .list-cont h4 a { +.create-wizard-overlay .images-list-cont h4 a, +.create-wizard-overlay .list-cont h4 a { font-size: 0.8em; font-weight: normal; margin-right: 5px; @@ -6356,34 +6506,39 @@ table.list-machines .wave { color: #FF7F2A; } -.create-vm .confirm-params { +.create-wizard-overlay .confirm-params { overflow: auto; } -.create-vm .vm-confirm .ssh.confirm-params { +.create-wizard-overlay .vm-confirm .ssh.confirm-params { max-height: 150px; } -.create-vm .personalize-cont .confirm-params { +.create-wizard-overlay .personalize-cont .confirm-params { max-height: 160px; margin-right: 10px; } -.create-vm .personalize-cont, -.create-vm .confirm-cont { - height: 250px; +.create-wizard-overlay .select-volume-details .personalize-cont { + height: 235px; +} + +.create-wizard-overlay .personalize-cont, +.create-wizard-overlay .confirm-cont { + height: 290px; } -.create-vm .image-warning p { + +.create-wizard-overlay .image-warning p { width: 80%; float: left; } -.create-vm .image-warning .untrusted-image-confirm:hover { +.create-wizard-overlay .image-warning .untrusted-image-confirm:hover { background-color: #7D674E; } -.create-vm .image-warning .untrusted-image-confirm { +.create-wizard-overlay .image-warning .untrusted-image-confirm { display: inline-block; padding: 5px; background-color: #5C4D39; @@ -6396,7 +6551,7 @@ table.list-machines .wave { text-align: center; } -.create-vm .image-warning { +.create-wizard-overlay .image-warning { display: none; background-color: #987249; color: #fff; @@ -6405,38 +6560,67 @@ table.list-machines .wave { border-top: 1px solid #AAA; } -.create-vm .create-step-cont .rename input.rename-field { +.create-wizard-overlay .create-step-cont .rename input.rename-field { font-size: 1.4em; padding: 5px; - width: 93%; - padding-left: 30px; + width: 98%; background-position: 7px center; background-repeat: no-repeat; } -.create-vm .create-step-cont .rename label { +.create-wizard-overlay.create-volume textarea.volume-description { + width: 92%; + height: 70px; +} + +.create-wizard-overlay.create-volume input.volume-name { + width: 89%; + padding: 4px; +} + +.create-wizard-overlay.create-vm .create-step-cont .rename input.rename-field { + padding-left: 30px; + width: 93%; +} + +.create-wizard-overlay .create-step-cont .rename label { display: block; } -.create-vm .create-step-cont .personalize-conts, -.create-vm .create-step-cont .confirm-conts { +.create-wizard-overlay .create-step-cont .personalize-conts.top { + margin-top: 0; +} + +.create-wizard-overlay .create-step-cont .personalize-conts, +.create-wizard-overlay .create-step-cont .confirm-conts { margin-top: 20px; } -.create-vm .create-step-cont .personalize-cont, -.create-vm .create-step-cont .confirm-cont { +.create-wizard-overlay .create-step-cont .personalize-cont, +.create-wizard-overlay .create-step-cont .confirm-cont { width: 30%; margin-right: 2%; border-right: 1px solid #A1C8DB; float: left; } -.create-vm .create-step-cont .confirm-cont ul li .title { +.create-wizard-overlay .create-step-cont .confirm-cont.wide.last { + width: 46% !important; +} +.create-wizard-overlay .create-step-cont .confirm-cont.wide { + width: 50%; +} + +.create-wizard-overlay .create-step-cont .confirm-cont ul li .title { width: 55px; float: left; } -.create-vm .create-step-cont .list-cont ul li .value { +.create-wizard-overlay .create-step-cont .list-cont ul li .project-name { + float: left !important; +} + +.create-wizard-overlay .create-step-cont .list-cont ul li .value { float: right; padding-top: 2px; display: block; @@ -6444,25 +6628,27 @@ table.list-machines .wave { text-align: right; } -.create-vm .create-step-cont .list-cont ul li.flavor-disktype .value { +.create-wizard-overlay .create-step-cont .list-cont ul li.flavor-disktype .value { width: 65px; } -.create-vm .create-step-cont .list-cont ul li.image-description .value, -.create-vm .create-step-cont .list-cont ul li.image-name .value { +.create-wizard-overlay .create-step-cont .list-cont ul li.image-description .value, +.create-wizard-overlay .create-step-cont .list-cont ul li.image-name .value { float: none; width: auto; text-align:left; width: auto; } -.create-vm .create-step-cont .list-cont ul li.image-name .value { +.create-wizard-overlay .create-step-cont .list-cont ul li.flavor-project .value, +.create-wizard-overlay .create-step-cont .list-cont ul li.image-name .value { width: 120px; text-indent: 0; margin-left: 0; + text-align: left; } -.create-vm .create-step-cont .list-cont ul li { +.create-wizard-overlay .create-step-cont .list-cont ul li { padding:0; margin:0; margin-bottom: 5px; @@ -6471,42 +6657,54 @@ table.list-machines .wave { margin-right: 10px; } -.create-vm .create-step-cont li.ssh-key-option .check, -.create-vm .create-step-cont li.list-item-option .check { +.create-wizard-overlay .create-step-cont li.ssh-key-option .check, +.create-wizard-overlay .create-step-cont li.list-item-option .check { float: right; margin-right: 5px; margin-top: 0px; } -.create-vm .create-step-cont li.ssh-key-option.selected { +.create-wizard-overlay .create-step-cont li.ssh-key-option.selected { } -.create-vm .create-step-cont li.ssh-key-option.selected:hover, -.create-vm .create-step-cont li.list-item-option.selected:hover { +.create-wizard-overlay .create-step-cont li.ssh-key-option.selected:hover, +.create-wizard-overlay .create-step-cont li.list-item-option.selected:hover { background-color: #F95; } -.create-vm .create-step-cont li.ssh-key-option:hover, -.create-vm .create-step-cont li.list-item-option:hover { +.create-wizard-overlay .create-step-cont li.ssh-key-option:hover, +.create-wizard-overlay .create-step-cont li.list-item-option:hover { background-color: #eee; } -.create-vm .create-step-cont li.ssh-key-option.selected { +.create-wizard-overlay .create-step-cont li.ssh-key-option.selected { } -.create-vm .create-step-cont li.ssh-key-option, -.create-vm .create-step-cont li.list-item-option { +.create-wizard-overlay .create-step-cont li.ssh-key-option, +.create-wizard-overlay .create-step-cont li.list-item-option { padding: 6px !important; } -.create-vm .create-step-cont .list-cont.ssh { +.create-wizard-overlay .create-step-cont .list-cont.vm h4 { + margin-right: 0; +} + +.create-wizard-overlay .create-step-cont .list-cont.vm { + width: 379px; +} + +.create-wizard-overlay .create-step-cont .list-cont.volume-size { + width: 34% +} + +.create-wizard-overlay .create-step-cont .list-cont.ssh { width: 60%; } -.create-vm .create-step-cont .list-cont.meta h4 { +.create-wizard-overlay .create-step-cont .list-cont.meta h4 { margin-right: 0; } -.create-vm .create-step-cont .list-cont.meta { +.create-wizard-overlay .create-step-cont .list-cont.last { margin-right:0; border-right: none; width: 195px; @@ -6514,7 +6712,17 @@ table.list-machines .wave { overflow-y: auto; } -.create-vm .list-cont > h4 { +.create-wizard-overlay .list-cont .list-subcont { + margin-bottom: 10px; +} + +.create-wizard-overlay .list-cont .list-subcont > h4.noborder { + border: none; + padding-bottom: 0; +} + +.create-wizard-overlay .list-cont .list-subcont > h4, +.create-wizard-overlay .list-cont > h4 { font-size: 1.2em; margin-right: 10px; border-bottom: 1px solid #A1C8DB; @@ -6522,47 +6730,47 @@ table.list-machines .wave { color: #387693; } -.create-vm .list-cont .param.image-name { +.create-wizard-overlay .list-cont .param.image-name { margin-bottom: 0 !important; border-bottom: none !important; } -.create-vm .list-cont .param h4 { +.create-wizard-overlay .list-cont .param h4 { margin-bottom: 0px !important; font-size: 1.1em !important; } -.create-vm .list-cont .param { +.create-wizard-overlay .list-cont .param { margin-bottom: 7px !important; } -.create-vm .list-cont .value { +.create-wizard-overlay .list-cont .value { font-weight: bold; } -.create-vm .list-cont .param .value { +.create-wizard-overlay .list-cont .param .value { font-size: 0.9em; } -.create-vm .list-cont .param .title { +.create-wizard-overlay .list-cont .param .title { color: #387693; } -.create-vm .list-cont .image-description { +.create-wizard-overlay .list-cont .image-description { margin-left:0; } -.create-vm .list-cont .image-description .value { +.create-wizard-overlay .list-cont .image-description .value { font-weight: normal; margin-left: 0 !important; } -.create-vm .list-cont .image-description .title { +.create-wizard-overlay .list-cont .image-description .title { display: none; font-size: 0.8em; } -.create-vm .list-cont.meta .values span { +.create-wizard-overlay .list-cont.meta .values span { display:block; float: left; margin-right: 4px; @@ -6572,23 +6780,29 @@ table.list-machines .wave { font-size: 0.9em; } -.create-vm .list-cont.meta .key { +.create-wizard-overlay .list-cont.meta .key { font-weight: bold; font-size: 0.9em; display: block; margin-bottom: 5px; } -.create-vm .meta input { +.create-wizard-overlay .meta input { font-size: 0.8em; } -.create-vm .network-select { - height: 278px; +.create-wizard-overlay .network-select { + height: 310px; overflow-y: scroll; padding-right: 10px; } +.wizard-overlay-content { + padding: 0 !important; + width: 624px !important; + overflow: hidden; +} + .vm-connect .connect-cont { margin-bottom: 20px; border-bottom: 1px solid #A1C8DB; @@ -6654,7 +6868,8 @@ input.has-errors { .icon .suspended-notice { right: 192px; - top: 54px; + top: 60px; + z-index: 500; } .suspended-notice { @@ -6793,7 +7008,7 @@ input.has-errors { .pane-view .collection-list-view .model-view .main-content .state-indicator { width: 50px; position: absolute; - top: 30px; + top: 46px; right: 5px; } @@ -6817,8 +7032,9 @@ input.has-errors { } .pane-view .collection-list-view .model-view .main-content-inner { - padding: 10px 20px; + padding: 20px 20px; padding-right: 10px; + padding-top: 20px; /*overflow: hidden;*/ } @@ -6879,6 +7095,8 @@ input.has-errors { .collection .empty-list { padding: 10px; + padding-top:6px; + padding-left: 10px; font-size: 0.8em; color: #333; } @@ -6897,13 +7115,18 @@ input.has-errors { margin-bottom: 0; } +.model-item img.logo { + width: 60px; + height: 60px; +} + .model-item .status-title { text-align: right; margin-right: 4px; margin-top: 5px; - width: 150px; + width: 170px; position: absolute; - top:-4px; + top: -4px; right: 0; } @@ -7022,11 +7245,11 @@ input.has-errors { } .model-item .status-inactive .status-indicator .indicator { - background-color: #940606; + background-color: #666666; } .model-item .status-terminated .status-indicator .indicator { - background-color: #5E1616; + background-color: #5e1616; } .model-item .status-error .status-indicator .indicator { @@ -7034,14 +7257,13 @@ input.has-errors { } .model-item .status-progress .status-indicator .indicator { - background-color: #FF7F2A; + background-color: #4085a5 !important; } .model-item .status-indicator .indicator { width: 10px; height: 11px; float: right; - background-color: #EFF7FA; margin-right: 3px; } @@ -7091,34 +7313,58 @@ input.has-errors { /* end vm sprites */ /* ips */ +#ips-list-view .model-view .main-content-inner { + padding-bottom: 25px; +} + +#ips-list-view .inline.ports.nested-model-list { + width: 360px; +} + .ip-port-view .title { width: 100%; } +.model-item.light-background .main-content-inner { + background-color:#aed2e3 !important; +} + .model-item .vm-name { font-weight: bold; } .nested-model-list { position: relative; - top: 43px; + top: 40px; overflow: visible; width: 531px; margin-left: -41px; margin-bottom: 39px; } +.items-sublist.private .nested-model-list { + top: 26px; +} + .entry.inline .nested-model-list { font-size: 0.9em; } .entry.inline .nested-model-list .model-item .outer { - width: 220px; + width: 320px; +} + +.volume-item .entry.inline .nested-model-list { + margin-left: -19px; +} + +.entry.inline .ports.nested-model-list { + top: 34px; } .entry.inline .nested-model-list { position: absolute; - top: 17px; + top: 32px; margin-left: -9px; margin-bottom: 0; width: 420px; @@ -7325,10 +7571,19 @@ input.has-errors { .network-ports-toggler { position: absolute; - top: 30px; + top: 40px; left: 0px; } +.private .model-name { + height: 17px; + margin-top: -6px; +} + +.private .network-ports-toggler { + top: 40px; +} + .model-form-actions .form-action { font-size: 1.1em !important; } @@ -7469,7 +7724,7 @@ input.has-errors { font-family: monospace; font-size: 1em; border: none; - overflow-y: hidden; + overflow-y: auto; overflow-x: hidden; height: 185px; margin: 10px 0; @@ -7559,6 +7814,11 @@ input.has-errors { cursor: pointer; } +.select-item.disabled { + background-color: #aaa !important; + color: #eeeeee; +} + .select-item.selected { background-color: #FF7F2A; } @@ -7582,6 +7842,14 @@ input.has-errors { margin-top: -5px; } +.select-item.floating-ip .project-name { + color: #777 !important; + float: right; + margin-right: -18px; + font-size: 0.8em; + margin-top: 4px; +} + .select-item.floating-ip.not-available { background-image: none; } @@ -7644,6 +7912,11 @@ input.has-errors { margin-top: 3px; } +#project-select-content .select-item .name { + width: 100% !important; + float: none; +} + .select-item .name { float: left; width: 90%; @@ -7658,3 +7931,376 @@ input.has-errors { float: left; width: 5%; } + + +#ips-create-content .form-actions { + margin-top: -23px; +} + +.project-name-cont:hover { + color: #444; +} + +.project-name-cont { + cursor: pointer; + font-weight: normal; + font-size: 11px; + color: #222; + margin-bottom: 2px; +} + +.icon .project-name-cont { + margin-bottom: 7px; +} + +.icon .machine-data { + position: relative; +} + +.icon .machine-data .project-name-cont { + padding: 0; + margin-top: -3px; +} + +#networks-list-view .project-name-cont { + margin-top: 2px; +} + +#volumes-list-view .items-sublist.system .project-name-cont, +#volumes-list-view .items-sublist.system .project-name-con:hover { + color: #999; + cursor: default; +} + + +.main-content-inner { + min-height: 70px; +} + +.select2-drop .select-item { + text-align: left; +} + +.select2-drop .select-item .quota .resource .value { + font-weight: bold; + margin-left: 1px; +} + +.select2-drop .select-item .quota .resource { + margin-right: 8px; +} + +.select2-drop .select-item .quota { + float: right; + margin:0 !important; + padding:0 !important; +} + +.create-vm .step-cont .project-select { + margin-bottom: 5px; + margin-top: -7px; +} + +.project-select .select2-chosen .quota { + display: none; +} + +#project-select-content .model-usage { + margin-bottom: 10px; +} + +#project-select-content .model-usage .key { + display: inline-block; + font-size: 0.9em; + margin-right: 5px; +} + +#project-select-content .model-usage .value { + display: inline-block; + color: #444; + letter-spacing: -1px; + font-weight: bold; + margin-right: 15px; +} + +.project.select-item .quota .resource-value { + width: 29%; + margin-right: 2%; + display: block; + float: left; + font-weight: bold; +} + +.project.select-item .quota .resource-key { + width: 13%; + display: block; + float: left; + letter-spacing: -1px; +} + +option.project.select-item.selected { + background-color: transparent; +} + +.project.select-item.selected { + background-color: #6A96AB; +} + +.project.select-item.selected .quota .resource-key { + color: #fff; +} + +.project.select-item .quota .resource-key { + color: #444; + margin-right: 5px; +} + +.project.select-item .quota .resource-value { + margin-right: 2px; + display: inline-block; +} + +.project.select-item .quota .resource-value { +} + +.project.select-item { + padding: 5px 0 0 0; + position: relative; +} + +.project.select-item.selected .quota { +} + +.project.select-item.disabled .quota { + background-color: #aaa; +} + +.project.select-item .quota { + padding: 5px 5px 5px 0; +} + +.project.select-item .quota { + margin-top: 5px; + font-size: 0.9em; + margin-left: 28px; +} + +.project.select-item .current { + position: absolute; + right: 5px; + top: 2px; + font-size: 0.9em; + padding: 4px; + display: none; + color: #FFF; +} + +.project.select-item.selected .current { + background-color: #6A96AB; +} + +.model-item.current .project.select-item .current { + background-color: #6A96AB; + display: block; +} + + +.project.select-item.current { + background-color: #C7DEE9; +} + +/* snapshots */ +.snapshot-create-form .form-field { + margin-bottom: 10px; +} + +.snapshot-create-form .form-field input { + width: 100%; +} + +.snapshot-create-desc { + width: 99.5%; + height: 90px; + border: 1px solid #aaa; + margin-top: 3px; +} + + +/* snapshots */ +.snapshot-create-form .form-field { + margin-bottom: 10px; +} + +.snapshot-create-form .form-field input { + width: 100%; +} + +.snapshot-create-desc { + width: 99.5%; + height: 90px; + border: 1px solid #aaa; + margin-top: 3px; +} + +.slider-volume-size { + margin-top: 4px; + width: 85%; + float: left; +} + +.slider-volume-size-cont { + margin-bottom: 15px; +} + +.slider-volume-size-cont .metric { + float: left; + font-weight: bold; + display: block; + margin-top: 5px; + margin-left: 4px; + color: #4085A5; +} + +.slider-volume-size-cont input { + display: block !important; + float: left; + width: 30px; + padding: 2px; + font-weight: bold; + margin-left: 4px; +} + +.slider > .dragger { + border: 1px solid #444; + width: 12px; + height: 20px; + background-color: #4085A5; +} + +.slider > .dragger:hover { + background-color: #70A9C9; +} + + +.slider > .track, .slider > .highlight-track { + background: #ccc; + border: 1px solid #aaa; + height: 7px; +} + +.slider > .highlight-track { + background-color: #8DCA09; + background: -webkit-linear-gradient(top, #8DCA09, #72A307); + background: -moz-linear-gradient(top, #8DCA09, #72A307); + background: linear-gradient(top, #8DCA09, #72A307); + + border-color: #496805; +} +/* end slider */ + +.volume-item .logo { + position: relative; + top: -5px; +} + +.volume-item .disk-template { + font-weight: bold; + color: #3582Ac; +} + +.volume-item .volume-size { + font-weight: bold; + font-size: 0.8em; + position: absolute; + width: 68px; + margin-left: -4px; + top: 67px; + text-align: center; +} + +.volume-item .volume-size span { + padding: 1px 8px; + color: #222222; + position: relative; + left: -1px; + top: -25px; + font-size: 1.4em; + text-shadow: + -1px -1px 0 rgba(239, 239, 239, 0.7), + 1px -1px 0 rgba(239, 239, 239, 0.7), + -1px 1px 0 rgba(239, 239, 239, 0.7), + 1px 1px 0 rgba(239, 239, 239, 0.7); +} + +.volume-item .vm-view-cont img { + vertical-align: middle; +} + +.volume-item .vm-view-cont { + margin-left: 10px; + margin-top: 12px; +} + +.volume-item .vms.nested-model-list .title { + font-weight: normal !important; +} + +.volume-item .vms.nested-model-list { + width: 360px; + position: relative; + top: -12px; +} + +.volume-item .toggler-wrap { + margin-top: -12px; + margin-left: 2px; +} + +.volume-item .content-cont .model-rename-view { + width: 330px; +} + +.volume-item .content-cont .edit-btn.hidden { + visibility: hidden; +} + +.volume-item .content-cont .edit-btn { + position: absolute; + right:-29px; + top: 93px; + z-index: 10000; + height: 20px; + float: right; + margin-right: 5px; +} + +.volume-item .content-cont .rename-actions { + position: absolute; + left: 286px; + top: 104px; +} + +.volume-item .content-cont textarea.readonly { + cursor: pointer; + background-color: #F2EFEF; +} + +.volume-item .content-cont textarea { + padding: 6px; + max-width: 270px; + width: 270px; + height: 100px; + margin-left: 2px; + margin-top: 5px; +} + +input.reset { + padding: 0; + margin: 0; + border: 0; + border: none; + font-size: inherit !important; + font-family: inherit !important; + color: inherit !important; + background-color: transparent; +} diff --git a/snf-cyclades-app/synnefo/ui/static/snf/css/select2-spinner.gif b/snf-cyclades-app/synnefo/ui/static/snf/css/select2-spinner.gif new file mode 100644 index 0000000000000000000000000000000000000000..5b33f7e54f4e55b6b8774d86d96895db9af044b4 Binary files /dev/null and b/snf-cyclades-app/synnefo/ui/static/snf/css/select2-spinner.gif differ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/css/select2.css b/snf-cyclades-app/synnefo/ui/static/snf/css/select2.css new file mode 100644 index 0000000000000000000000000000000000000000..cce158c9a83d72a416e4cfa0dea0d506c24d32eb --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/css/select2.css @@ -0,0 +1,681 @@ +/* +Version: 3.5.0 Timestamp: Mon Jun 16 19:29:44 EDT 2014 +*/ +.select2-container { + margin: 0; + position: relative; + display: inline-block; + /* inline-block for ie7 */ + zoom: 1; + *display: inline; + vertical-align: middle; +} + +.select2-container, +.select2-drop, +.select2-search, +.select2-search input { + /* + Force border-box so that % widths fit the parent + container without overlap because of margin/padding. + More Info : http://www.quirksmode.org/css/box.html + */ + -webkit-box-sizing: border-box; /* webkit */ + -moz-box-sizing: border-box; /* firefox */ + box-sizing: border-box; /* css3 */ +} + +.select2-container .select2-choice { + display: block; + height: 26px; + padding: 0 0 0 8px; + overflow: hidden; + position: relative; + + border: 1px solid #aaa; + white-space: nowrap; + line-height: 26px; + color: #444; + text-decoration: none; + + background-clip: padding-box; + + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + background-color: #fff; + background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eee), color-stop(0.5, #fff)); + background-image: -webkit-linear-gradient(center bottom, #eee 0%, #fff 50%); + background-image: -moz-linear-gradient(center bottom, #eee 0%, #fff 50%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#ffffff', endColorstr = '#eeeeee', GradientType = 0); + background-image: linear-gradient(to top, #eee 0%, #fff 50%); +} + +html[dir="rtl"] .select2-container .select2-choice { + padding: 0 8px 0 0; +} + +.select2-container.select2-drop-above .select2-choice { + border-bottom-color: #aaa; + + background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eee), color-stop(0.9, #fff)); + background-image: -webkit-linear-gradient(center bottom, #eee 0%, #fff 90%); + background-image: -moz-linear-gradient(center bottom, #eee 0%, #fff 90%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#eeeeee', GradientType=0); + background-image: linear-gradient(to bottom, #eee 0%, #fff 90%); +} + +.select2-container.select2-allowclear .select2-choice .select2-chosen { + margin-right: 42px; +} + +.select2-container .select2-choice > .select2-chosen { + margin-right: 26px; + display: block; + overflow: hidden; + + white-space: nowrap; + + text-overflow: ellipsis; + float: none; + width: auto; +} + +html[dir="rtl"] .select2-container .select2-choice > .select2-chosen { + margin-left: 26px; + margin-right: 0; +} + +.select2-container .select2-choice abbr { + display: none; + width: 12px; + height: 12px; + position: absolute; + right: 24px; + top: 8px; + + font-size: 1px; + text-decoration: none; + + border: 0; + background: url('select2.png') right top no-repeat; + cursor: pointer; + outline: 0; +} + +.select2-container.select2-allowclear .select2-choice abbr { + display: inline-block; +} + +.select2-container .select2-choice abbr:hover { + background-position: right -11px; + cursor: pointer; +} + +.select2-drop-mask { + border: 0; + margin: 0; + padding: 0; + position: fixed; + left: 0; + top: 0; + min-height: 100%; + min-width: 100%; + height: auto; + width: auto; + opacity: 0; + z-index: 9998; + /* styles required for IE to work */ + background-color: #fff; + filter: alpha(opacity=0); +} + +.select2-drop { + width: 100%; + margin-top: -1px; + position: absolute; + z-index: 9999; + top: 100%; + + background: #fff; + color: #000; + border: 1px solid #aaa; + border-top: 0; +} + +.select2-drop.select2-drop-above { + margin-top: 1px; + border-top: 1px solid #aaa; + border-bottom: 0; + + -webkit-box-shadow: 0 -4px 5px rgba(0, 0, 0, .15); + box-shadow: 0 -4px 5px rgba(0, 0, 0, .15); +} + +.select2-drop-active { + border: 1px solid #5897fb; + border-top: none; +} + +.select2-drop.select2-drop-above.select2-drop-active { + border-top: 1px solid #5897fb; +} + +.select2-drop-auto-width { + border-top: 1px solid #aaa; + width: auto; +} + +.select2-drop-auto-width .select2-search { + padding-top: 4px; +} + +.select2-container .select2-choice .select2-arrow { + display: inline-block; + width: 18px; + height: 100%; + position: absolute; + right: 0; + top: 0; + + border-left: 1px solid #aaa; + + background-clip: padding-box; + + background: #ccc; + background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #ccc), color-stop(0.6, #eee)); + background-image: -webkit-linear-gradient(center bottom, #ccc 0%, #eee 60%); + background-image: -moz-linear-gradient(center bottom, #ccc 0%, #eee 60%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#eeeeee', endColorstr = '#cccccc', GradientType = 0); + background-image: linear-gradient(to top, #ccc 0%, #eee 60%); +} + +html[dir="rtl"] .select2-container .select2-choice .select2-arrow { + left: 0; + right: auto; + + border-left: none; + border-right: 1px solid #aaa; +} + +.select2-container .select2-choice .select2-arrow b { + display: block; + width: 100%; + height: 100%; + background: url('select2.png') no-repeat 0 1px; +} + +html[dir="rtl"] .select2-container .select2-choice .select2-arrow b { + background-position: 2px 1px; +} + +.select2-search { + display: inline-block; + width: 100%; + min-height: 26px; + margin: 0; + padding-left: 4px; + padding-right: 4px; + + position: relative; + z-index: 10000; + + white-space: nowrap; +} + +.select2-search input { + width: 100%; + height: auto !important; + min-height: 26px; + padding: 4px 20px 4px 5px; + margin: 0; + + outline: 0; + font-family: sans-serif; + font-size: 1em; + + border: 1px solid #aaa; + + -webkit-box-shadow: none; + box-shadow: none; + + background: #fff url('select2.png') no-repeat 100% -22px; + background: url('select2.png') no-repeat 100% -22px, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, #fff), color-stop(0.99, #eee)); + background: url('select2.png') no-repeat 100% -22px, -webkit-linear-gradient(center bottom, #fff 85%, #eee 99%); + background: url('select2.png') no-repeat 100% -22px, -moz-linear-gradient(center bottom, #fff 85%, #eee 99%); + background: url('select2.png') no-repeat 100% -22px, linear-gradient(to bottom, #fff 85%, #eee 99%) 0 0; +} + +html[dir="rtl"] .select2-search input { + padding: 4px 5px 4px 20px; + + background: #fff url('select2.png') no-repeat -37px -22px; + background: url('select2.png') no-repeat -37px -22px, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, #fff), color-stop(0.99, #eee)); + background: url('select2.png') no-repeat -37px -22px, -webkit-linear-gradient(center bottom, #fff 85%, #eee 99%); + background: url('select2.png') no-repeat -37px -22px, -moz-linear-gradient(center bottom, #fff 85%, #eee 99%); + background: url('select2.png') no-repeat -37px -22px, linear-gradient(to bottom, #fff 85%, #eee 99%) 0 0; +} + +.select2-drop.select2-drop-above .select2-search input { + margin-top: 4px; +} + +.select2-search input.select2-active { + background: #fff url('select2-spinner.gif') no-repeat 100%; + background: url('select2-spinner.gif') no-repeat 100%, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, #fff), color-stop(0.99, #eee)); + background: url('select2-spinner.gif') no-repeat 100%, -webkit-linear-gradient(center bottom, #fff 85%, #eee 99%); + background: url('select2-spinner.gif') no-repeat 100%, -moz-linear-gradient(center bottom, #fff 85%, #eee 99%); + background: url('select2-spinner.gif') no-repeat 100%, linear-gradient(to bottom, #fff 85%, #eee 99%) 0 0; +} + +.select2-container-active .select2-choice, +.select2-container-active .select2-choices { + border: 1px solid #5897fb; + outline: none; + + -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, .3); + box-shadow: 0 0 5px rgba(0, 0, 0, .3); +} + +.select2-dropdown-open .select2-choice { + border-bottom-color: transparent; + -webkit-box-shadow: 0 1px 0 #fff inset; + box-shadow: 0 1px 0 #fff inset; + + background-color: #eee; + background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #fff), color-stop(0.5, #eee)); + background-image: -webkit-linear-gradient(center bottom, #fff 0%, #eee 50%); + background-image: -moz-linear-gradient(center bottom, #fff 0%, #eee 50%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#ffffff', GradientType=0); + background-image: linear-gradient(to top, #fff 0%, #eee 50%); +} + +.select2-dropdown-open.select2-drop-above .select2-choice, +.select2-dropdown-open.select2-drop-above .select2-choices { + border: 1px solid #5897fb; + border-top-color: transparent; + + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #fff), color-stop(0.5, #eee)); + background-image: -webkit-linear-gradient(center top, #fff 0%, #eee 50%); + background-image: -moz-linear-gradient(center top, #fff 0%, #eee 50%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#ffffff', GradientType=0); + background-image: linear-gradient(to bottom, #fff 0%, #eee 50%); +} + +.select2-dropdown-open .select2-choice .select2-arrow { + background: transparent; + border-left: none; + filter: none; +} +html[dir="rtl"] .select2-dropdown-open .select2-choice .select2-arrow { + border-right: none; +} + +.select2-dropdown-open .select2-choice .select2-arrow b { + background-position: -18px 1px; +} + +html[dir="rtl"] .select2-dropdown-open .select2-choice .select2-arrow b { + background-position: -16px 1px; +} + +.select2-hidden-accessible { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; +} + +/* results */ +.select2-results { + max-height: 200px; + padding: 0 0 0 4px; + margin: 4px 4px 4px 0; + position: relative; + overflow-x: hidden; + overflow-y: auto; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +html[dir="rtl"] .select2-results { + padding: 0 4px 0 0; + margin: 4px 0 4px 4px; +} + +.select2-results ul.select2-result-sub { + margin: 0; + padding-left: 0; +} + +.select2-results li { + list-style: none; + display: list-item; + background-image: none; +} + +.select2-results li.select2-result-with-children > .select2-result-label { + font-weight: bold; +} + +.select2-results .select2-result-label { + padding: 3px 7px 4px; + margin: 0; + cursor: pointer; + + min-height: 1em; + + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.select2-results-dept-1 .select2-result-label { padding-left: 20px } +.select2-results-dept-2 .select2-result-label { padding-left: 40px } +.select2-results-dept-3 .select2-result-label { padding-left: 60px } +.select2-results-dept-4 .select2-result-label { padding-left: 80px } +.select2-results-dept-5 .select2-result-label { padding-left: 100px } +.select2-results-dept-6 .select2-result-label { padding-left: 110px } +.select2-results-dept-7 .select2-result-label { padding-left: 120px } + +.select2-results .select2-highlighted { + background: #3875d7; + color: #fff; +} + +.select2-results li em { + background: #feffde; + font-style: normal; +} + +.select2-results .select2-highlighted em { + background: transparent; +} + +.select2-results .select2-highlighted ul { + background: #fff; + color: #000; +} + + +.select2-results .select2-no-results, +.select2-results .select2-searching, +.select2-results .select2-selection-limit { + background: #f4f4f4; + display: list-item; + padding-left: 5px; +} + +/* +disabled look for disabled choices in the results dropdown +*/ +.select2-results .select2-disabled.select2-highlighted { + color: #666; + background: #f4f4f4; + display: list-item; + cursor: default; +} +.select2-results .select2-disabled { + background: #f4f4f4; + display: list-item; + cursor: default; +} + +.select2-results .select2-selected { + display: none; +} + +.select2-more-results.select2-active { + background: #f4f4f4 url('select2-spinner.gif') no-repeat 100%; +} + +.select2-more-results { + background: #f4f4f4; + display: list-item; +} + +/* disabled styles */ + +.select2-container.select2-container-disabled .select2-choice { + background-color: #f4f4f4; + background-image: none; + border: 1px solid #ddd; + cursor: default; +} + +.select2-container.select2-container-disabled .select2-choice .select2-arrow { + background-color: #f4f4f4; + background-image: none; + border-left: 0; +} + +.select2-container.select2-container-disabled .select2-choice abbr { + display: none; +} + + +/* multiselect */ + +.select2-container-multi .select2-choices { + height: auto !important; + height: 1%; + margin: 0; + padding: 0 5px 0 0; + position: relative; + + border: 1px solid #aaa; + cursor: text; + overflow: hidden; + + background-color: #fff; + background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(1%, #eee), color-stop(15%, #fff)); + background-image: -webkit-linear-gradient(top, #eee 1%, #fff 15%); + background-image: -moz-linear-gradient(top, #eee 1%, #fff 15%); + background-image: linear-gradient(to bottom, #eee 1%, #fff 15%); +} + +html[dir="rtl"] .select2-container-multi .select2-choices { + padding: 0 0 0 5px; +} + +.select2-locked { + padding: 3px 5px 3px 5px !important; +} + +.select2-container-multi .select2-choices { + min-height: 26px; +} + +.select2-container-multi.select2-container-active .select2-choices { + border: 1px solid #5897fb; + outline: none; + + -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, .3); + box-shadow: 0 0 5px rgba(0, 0, 0, .3); +} +.select2-container-multi .select2-choices li { + float: left; + list-style: none; +} +html[dir="rtl"] .select2-container-multi .select2-choices li +{ + float: right; +} +.select2-container-multi .select2-choices .select2-search-field { + margin: 0; + padding: 0; + white-space: nowrap; +} + +.select2-container-multi .select2-choices .select2-search-field input { + padding: 5px; + margin: 1px 0; + + font-family: sans-serif; + font-size: 100%; + color: #666; + outline: 0; + border: 0; + -webkit-box-shadow: none; + box-shadow: none; + background: transparent !important; +} + +.select2-container-multi .select2-choices .select2-search-field input.select2-active { + background: #fff url('select2-spinner.gif') no-repeat 100% !important; +} + +.select2-default { + color: #999 !important; +} + +.select2-container-multi .select2-choices .select2-search-choice { + padding: 3px 5px 3px 18px; + margin: 3px 0 3px 5px; + position: relative; + + line-height: 13px; + color: #333; + cursor: default; + border: 1px solid #aaaaaa; + + -webkit-box-shadow: 0 0 2px #fff inset, 0 1px 0 rgba(0, 0, 0, 0.05); + box-shadow: 0 0 2px #fff inset, 0 1px 0 rgba(0, 0, 0, 0.05); + + background-clip: padding-box; + + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + background-color: #e4e4e4; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#f4f4f4', GradientType=0); + background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eee)); + background-image: -webkit-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%); + background-image: -moz-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%); + background-image: linear-gradient(to top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%); +} +html[dir="rtl"] .select2-container-multi .select2-choices .select2-search-choice +{ + margin: 3px 5px 3px 0; + padding: 3px 18px 3px 5px; +} +.select2-container-multi .select2-choices .select2-search-choice .select2-chosen { + cursor: default; +} +.select2-container-multi .select2-choices .select2-search-choice-focus { + background: #d4d4d4; +} + +.select2-search-choice-close { + display: block; + width: 12px; + height: 13px; + position: absolute; + right: 3px; + top: 4px; + + font-size: 1px; + outline: none; + background: url('select2.png') right top no-repeat; +} +html[dir="rtl"] .select2-search-choice-close { + right: auto; + left: 3px; +} + +.select2-container-multi .select2-search-choice-close { + left: 3px; +} + +html[dir="rtl"] .select2-container-multi .select2-search-choice-close { + left: auto; + right: 2px; +} + +.select2-container-multi .select2-choices .select2-search-choice .select2-search-choice-close:hover { + background-position: right -11px; +} +.select2-container-multi .select2-choices .select2-search-choice-focus .select2-search-choice-close { + background-position: right -11px; +} + +/* disabled styles */ +.select2-container-multi.select2-container-disabled .select2-choices { + background-color: #f4f4f4; + background-image: none; + border: 1px solid #ddd; + cursor: default; +} + +.select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice { + padding: 3px 5px 3px 5px; + border: 1px solid #ddd; + background-image: none; + background-color: #f4f4f4; +} + +.select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice .select2-search-choice-close { display: none; + background: none; +} +/* end multiselect */ + + +.select2-result-selectable .select2-match, +.select2-result-unselectable .select2-match { + text-decoration: underline; +} + +.select2-offscreen, .select2-offscreen:focus { + clip: rect(0 0 0 0) !important; + width: 1px !important; + height: 1px !important; + border: 0 !important; + margin: 0 !important; + padding: 0 !important; + overflow: hidden !important; + position: absolute !important; + outline: 0 !important; + left: 0px !important; + top: 0px !important; +} + +.select2-display-none { + display: none; +} + +.select2-measure-scrollbar { + position: absolute; + top: -10000px; + left: -10000px; + width: 100px; + height: 100px; + overflow: scroll; +} + +/* Retina-ize icons */ + +@media only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-resolution: 2dppx) { + .select2-search input, + .select2-search-choice-close, + .select2-container .select2-choice abbr, + .select2-container .select2-choice .select2-arrow b { + background-image: url('select2x2.png') !important; + background-repeat: no-repeat !important; + background-size: 60px 40px !important; + } + + .select2-search input { + background-position: 100% -21px !important; + } +} diff --git a/snf-cyclades-app/synnefo/ui/static/snf/css/select2.png b/snf-cyclades-app/synnefo/ui/static/snf/css/select2.png new file mode 100644 index 0000000000000000000000000000000000000000..1d804ffb99699b9e030f1010314de0970b5a000d Binary files /dev/null and b/snf-cyclades-app/synnefo/ui/static/snf/css/select2.png differ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/css/select2x2.png b/snf-cyclades-app/synnefo/ui/static/snf/css/select2x2.png new file mode 100644 index 0000000000000000000000000000000000000000..4bdd5c961d452c49dfa0789c2c7ffb82c238fc24 Binary files /dev/null and b/snf-cyclades-app/synnefo/ui/static/snf/css/select2x2.png differ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/LICENSE.txt b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/LICENSE.txt new file mode 100644 index 0000000000000000000000000000000000000000..2d0940893a29932bc8edf04de23d88fa32ad4195 --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/LICENSE.txt @@ -0,0 +1,82 @@ +noVNC is Copyright (C) 2011 Joel Martin <github@martintribe.org> + +The noVNC core library files are licensed under the MPL 2.0 (Mozilla +Public License 2.0). The noVNC core library is composed of the +Javascript code necessary for full noVNC operation. This includes (but +is not limited to): + + include/base64.js + include/des.js + include/display.js + include/input.js + include/jsunzip.js + include/keysym.js + include/logo.js + include/rfb.js + include/ui.js + include/util.js + include/vnc.js + include/websock.js + include/webutil.js + +The HTML, CSS, font and images files that included with the noVNC +source distibution (or repository) are not considered part of the +noVNC core library and are licensed under more permissive licenses. +The intent is to allow easy integration of noVNC into existing web +sites and web applications. + +The HTML, CSS, font and image files are licensed as follows: + + *.html : 2-Clause BSD license + + include/*.css : 2-Clause BSD license + + include/Orbitron* : SIL Open Font License 1.1 + (Copyright 2009 Matt McInerney) + + images/ : Creative Commons Attribution-ShareAlike + http://creativecommons.org/licenses/by-sa/3.0/ + +Some portions of noVNC are copyright to their individual authors. +Please refer to the individual source files and/or to the noVNC commit +history: https://github.com/kanaka/noVNC/commits/master + +The are several files and projects that have been incorporated into +the noVNC core library. Here is a list of those files and the original +licenses (all MPL 2.0 compatible): + + include/base64.js : MPL 2.0 + + include/des.js : Various BSD style licenses + + include/jsunzip.js : zlib/libpng license + + include/web-socket-js/ : New BSD license (3-clause). Source code at + http://github.com/gimite/web-socket-js + + include/chrome-app/tcp-stream.js + : Apache 2.0 license + + utils/websockify + utils/websocket.py : LGPL 3 + +The following license texts are included: + + docs/LICENSE.MPL-2.0 + docs/LICENSE.LGPL-3 and + docs/LICENSE.GPL-3 + docs/LICENSE.OFL-1.1 + docs/LICENSE.BSD-3-Clause (New BSD) + docs/LICENSE.BSD-2-Clause (Simplified BSD / FreeBSD) + docs/LICENSE.zlib + docs/LICENSE.Apache-2.0 + +Or alternatively the license texts may be found here: + + http://www.mozilla.org/MPL/2.0/ + http://www.gnu.org/licenses/lgpl.html and + http://www.gnu.org/licenses/gpl.html + http://scripts.sil.org/OFL + http://en.wikipedia.org/wiki/BSD_licenses + http://www.gzip.org/zlib/zlib_license.html + http://www.apache.org/licenses/LICENSE-2.0.html diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/alt.png b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/alt.png new file mode 100644 index 0000000000000000000000000000000000000000..d42af7b421b9f1193620cb4a7114f1df27886d1c Binary files /dev/null and b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/alt.png differ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/clipboard.png b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/clipboard.png new file mode 100644 index 0000000000000000000000000000000000000000..24df33c1c103755c23167c9a1d5eb51f9057060e Binary files /dev/null and b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/clipboard.png differ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/connect.png b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/connect.png new file mode 100644 index 0000000000000000000000000000000000000000..79e71adb85cbdd2da5b59d9e2c38609dd526d1be Binary files /dev/null and b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/connect.png differ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/ctrl.png b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/ctrl.png new file mode 100644 index 0000000000000000000000000000000000000000..a63b601f19d9e62a784b9ae3619792ee0e7e1a14 Binary files /dev/null and b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/ctrl.png differ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/ctrlaltdel.png b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/ctrlaltdel.png new file mode 100644 index 0000000000000000000000000000000000000000..31922e53242fe85fb1db641a15339e2c33670e5b Binary files /dev/null and b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/ctrlaltdel.png differ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/disconnect.png b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/disconnect.png new file mode 100644 index 0000000000000000000000000000000000000000..8832f5ea7e2bdde7016b9429917fd453e42aadb8 Binary files /dev/null and b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/disconnect.png differ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/drag.png b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/drag.png new file mode 100644 index 0000000000000000000000000000000000000000..433f896d67d82cb3546cd7094bb0d2543cbf0971 Binary files /dev/null and b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/drag.png differ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/esc.png b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/esc.png new file mode 100644 index 0000000000000000000000000000000000000000..ece5f7cbef684a41f4a7bb9d632e4cb2310d6b5d Binary files /dev/null and b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/esc.png differ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/favicon.ico b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c999634f0e9b5fd5250f34d13118104261346610 Binary files /dev/null and b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/favicon.ico differ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/favicon.png b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..e2bdb19436f33713fb758376329cf59486b0cdff Binary files /dev/null and b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/favicon.png differ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/keyboard.png b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/keyboard.png new file mode 100644 index 0000000000000000000000000000000000000000..f797952513b39cc75d0f0dd4d6716608e10217ed Binary files /dev/null and b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/keyboard.png differ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/mouse_left.png b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/mouse_left.png new file mode 100644 index 0000000000000000000000000000000000000000..1de7a486c76ff1b95efd7e5ee36dd6943cc6dd3a Binary files /dev/null and b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/mouse_left.png differ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/mouse_middle.png b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/mouse_middle.png new file mode 100644 index 0000000000000000000000000000000000000000..81fbd9bd375b2daa88b6bac6a7972c58b708a67b Binary files /dev/null and b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/mouse_middle.png differ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/mouse_none.png b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/mouse_none.png new file mode 100644 index 0000000000000000000000000000000000000000..93dbf5780777973578edaf7ae35f65ed01e5718e Binary files /dev/null and b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/mouse_none.png differ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/mouse_right.png b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/mouse_right.png new file mode 100644 index 0000000000000000000000000000000000000000..355b25dc9a03e6d8dc4b956dc51185b5ff8f63e9 Binary files /dev/null and b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/mouse_right.png differ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/power.png b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/power.png new file mode 100644 index 0000000000000000000000000000000000000000..f68fd0813c02a625f3fb5097353b7825f287939f Binary files /dev/null and b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/power.png differ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/screen_320x460.png b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/screen_320x460.png new file mode 100644 index 0000000000000000000000000000000000000000..172ec555c304fdcd2cd34c3f920af2cce1009406 Binary files /dev/null and b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/screen_320x460.png differ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/screen_57x57.png b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/screen_57x57.png new file mode 100644 index 0000000000000000000000000000000000000000..e2085f29fc2ca0147251b8327d06081670943f0c Binary files /dev/null and b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/screen_57x57.png differ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/screen_700x700.png b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/screen_700x700.png new file mode 100644 index 0000000000000000000000000000000000000000..ae6776853e5070b7dff48e555659d18c94263e4d Binary files /dev/null and b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/screen_700x700.png differ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/settings.png b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/settings.png new file mode 100644 index 0000000000000000000000000000000000000000..a43f5e100b3dc9b5aa30812521b036e085b44a39 Binary files /dev/null and b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/settings.png differ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/showextrakeys.png b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/showextrakeys.png new file mode 100644 index 0000000000000000000000000000000000000000..ad8e0a70d3eb7b5f4462f808ab987053fa4b0e56 Binary files /dev/null and b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/showextrakeys.png differ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/tab.png b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/tab.png new file mode 100644 index 0000000000000000000000000000000000000000..84134872a881a627f745a432a3c951254cda26cb Binary files /dev/null and b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/images/tab.png differ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/Orbitron700.ttf b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/Orbitron700.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e28729dc56b0662b5d2506ddfde5a368784f5755 Binary files /dev/null and b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/Orbitron700.ttf differ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/Orbitron700.woff b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/Orbitron700.woff new file mode 100644 index 0000000000000000000000000000000000000000..61db630cce1c18fd78c71462d82184a32af6089a Binary files /dev/null and b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/Orbitron700.woff differ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/base.css b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/base.css new file mode 100644 index 0000000000000000000000000000000000000000..d6e34942a7f158569de30f23ad02abf734f00e71 --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/base.css @@ -0,0 +1,459 @@ + +/* + * noVNC base CSS + * Copyright (C) 2012 Joel Martin + * Copyright (C) 2013 Samuel Mannehed for Cendio AB + * noVNC is licensed under the MPL 2.0 (see LICENSE.txt) + * This file is licensed under the 2-Clause BSD license (see LICENSE.txt). + */ + +body { + margin:0; + padding:0; + font-family: Helvetica; + /*Background image with light grey curve.*/ + background-color:#494949; + background-repeat:no-repeat; + background-position:right bottom; + height:100%; +} + +html { + height:100%; +} + +#noVNC_controls ul { + list-style: none; + margin: 0px; + padding: 0px; +} +#noVNC_controls li { + padding-bottom:8px; +} + +#noVNC_host { + width:150px; +} +#noVNC_port { + width: 80px; +} +#noVNC_password { + width: 150px; +} +#noVNC_encrypt { +} +#noVNC_connectTimeout { + width: 30px; +} +#noVNC_path { + width: 100px; +} +#noVNC_connect_button { + width: 110px; + float:right; +} + +#noVNC_view_drag_button { + display: none; +} +#sendCtrlAltDelButton { + display: none; +} + +#sendCtrlButton { + display: none; +} + +#noVNC_mobile_buttons { + display: none; +} + +.noVNC-buttons-left { + float: left; + z-index: 1; + position: relative; +} + +.noVNC-buttons-right { + float:right; + right: 0px; + z-index: 2; + position: absolute; +} + +#noVNC_status { + font-size: 12px; + padding-top: 4px; + height:32px; + text-align: center; + font-weight: bold; + color: #fff; +} + +#noVNC_settings_menu { + margin: 3px; + text-align: left; +} +#noVNC_settings_menu ul { + list-style: none; + margin: 0px; + padding: 0px; +} + +#noVNC_apply { + float:right; +} + +/* Do not set width/height for VNC_screen or VNC_canvas or incorrect + * scaling will occur. Canvas resizes to remote VNC settings */ +#noVNC_screen_pad { + margin: 0px; + padding: 0px; + height: 36px; +} +#noVNC_screen { + text-align: center; + display: table; + width:100%; + height:100%; + background-color:#313131; + border-bottom-right-radius: 800px 600px; + /*border-top-left-radius: 800px 600px;*/ +} + +#noVNC_container, #noVNC_canvas { + margin: 0px; + padding: 0px; +} + +#noVNC_canvas { + left: 0px; +} + +#VNC_clipboard_clear_button { + float:right; +} +#VNC_clipboard_text { + font-size: 11px; +} + +#noVNC_clipboard_clear_button { + float:right; +} + +/*Bubble contents divs*/ +#noVNC_settings { + display:none; + margin-top:73px; + right:20px; + position:fixed; +} + +#noVNC_controls { + display:none; + margin-top:73px; + right:12px; + position:fixed; +} +#noVNC_controls.top:after { + right:15px; +} + +#noVNC_description { + display:none; + position:fixed; + + margin-top:73px; + right:20px; + left:20px; + padding:15px; + color:#000; + background:#eee; /* default background for browsers without gradient support */ + + border:2px solid #E0E0E0; + -webkit-border-radius:10px; + -moz-border-radius:10px; + border-radius:10px; +} + +#noVNC_popup_status_panel { + display:none; + position: fixed; + z-index: 1; + + margin:15px; + margin-top:60px; + padding:15px; + width:auto; + + text-align:center; + font-weight:bold; + word-wrap:break-word; + color:#fff; + background:rgba(0,0,0,0.65); + + -webkit-border-radius:10px; + -moz-border-radius:10px; + border-radius:10px; +} + +#noVNC_clipboard { + display:none; + margin-top:73px; + right:30px; + position:fixed; +} +#noVNC_clipboard.top:after { + right:85px; +} + +#keyboardinput { + width:1px; + height:1px; + background-color:#fff; + color:#fff; + border:0; + position: relative; + left: -40px; + z-index: -1; +} + +/* + * Advanced Styling + */ + +.noVNC_status_normal { + background: #b2bdcd; /* Old browsers */ + background: -moz-linear-gradient(top, #b2bdcd 0%, #899cb3 49%, #7e93af 51%, #6e84a3 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#b2bdcd), color-stop(49%,#899cb3), color-stop(51%,#7e93af), color-stop(100%,#6e84a3)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* IE10+ */ + background: linear-gradient(top, #b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* W3C */ +} +.noVNC_status_error { + background: #f04040; /* Old browsers */ + background: -moz-linear-gradient(top, #f04040 0%, #899cb3 49%, #7e93af 51%, #6e84a3 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f04040), color-stop(49%,#899cb3), color-stop(51%,#7e93af), color-stop(100%,#6e84a3)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #f04040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #f04040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #f04040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* IE10+ */ + background: linear-gradient(top, #f04040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* W3C */ +} +.noVNC_status_warn { + background: #f0f040; /* Old browsers */ + background: -moz-linear-gradient(top, #f0f040 0%, #899cb3 49%, #7e93af 51%, #6e84a3 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f0f040), color-stop(49%,#899cb3), color-stop(51%,#7e93af), color-stop(100%,#6e84a3)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #f0f040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #f0f040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #f0f040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* IE10+ */ + background: linear-gradient(top, #f0f040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* W3C */ +} + +/* Control bar */ +#noVNC-control-bar { + position:fixed; + + display:block; + height:36px; + left:0; + top:0; + width:100%; + z-index:200; +} + +.noVNC_status_button { + padding: 4px 4px; + vertical-align: middle; + border:1px solid #869dbc; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + background: #b2bdcd; /* Old browsers */ + background: -moz-linear-gradient(top, #b2bdcd 0%, #899cb3 49%, #7e93af 51%, #6e84a3 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#b2bdcd), color-stop(49%,#899cb3), color-stop(51%,#7e93af), color-stop(100%,#6e84a3)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* IE10+ */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#b2bdcd', endColorstr='#6e84a3',GradientType=0 ); /* IE6-9 */ + background: linear-gradient(top, #b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* W3C */ + /*box-shadow:inset 0.4px 0.4px 0.4px #000000;*/ +} + +.noVNC_status_button_selected { + padding: 4px 4px; + vertical-align: middle; + border:1px solid #4366a9; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + background: #779ced; /* Old browsers */ + background: -moz-linear-gradient(top, #779ced 0%, #3970e0 49%, #2160dd 51%, #2463df 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#779ced), color-stop(49%,#3970e0), color-stop(51%,#2160dd), color-stop(100%,#2463df)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #779ced 0%,#3970e0 49%,#2160dd 51%,#2463df 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #779ced 0%,#3970e0 49%,#2160dd 51%,#2463df 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #779ced 0%,#3970e0 49%,#2160dd 51%,#2463df 100%); /* IE10+ */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#779ced', endColorstr='#2463df',GradientType=0 ); /* IE6-9 */ + background: linear-gradient(top, #779ced 0%,#3970e0 49%,#2160dd 51%,#2463df 100%); /* W3C */ + /*box-shadow:inset 0.4px 0.4px 0.4px #000000;*/ +} + + +/*Settings Bubble*/ +.triangle-right { + position:relative; + padding:15px; + margin:1em 0 3em; + color:#fff; + background:#fff; /* default background for browsers without gradient support */ + /* css3 */ + /*background:-webkit-gradient(linear, 0 0, 0 100%, from(#2e88c4), to(#075698)); + background:-moz-linear-gradient(#2e88c4, #075698); + background:-o-linear-gradient(#2e88c4, #075698); + background:linear-gradient(#2e88c4, #075698);*/ + -webkit-border-radius:10px; + -moz-border-radius:10px; + border-radius:10px; + color:#000; + border:2px solid #E0E0E0; +} + +.triangle-right.top:after { + border-color: transparent #E0E0E0; + border-width: 20px 20px 0 0; + bottom: auto; + left: auto; + right: 50px; + top: -20px; +} + +.triangle-right:after { + content:""; + position:absolute; + bottom:-20px; /* value = - border-top-width - border-bottom-width */ + left:50px; /* controls horizontal position */ + border-width:20px 0 0 20px; /* vary these values to change the angle of the vertex */ + border-style:solid; + border-color:#E0E0E0 transparent; + /* reduce the damage in FF3.0 */ + display:block; + width:0; +} + +.triangle-right.top:after { + top:-40px; /* value = - border-top-width - border-bottom-width */ + right:50px; /* controls horizontal position */ + bottom:auto; + left:auto; + border-width:40px 40px 0 0; /* vary these values to change the angle of the vertex */ + border-color:transparent #E0E0E0; +} + +/*Default noVNC logo.*/ +/* From: http://fonts.googleapis.com/css?family=Orbitron:700 */ +@font-face { + font-family: 'Orbitron'; + font-style: normal; + font-weight: 700; + src: local('?'), url('Orbitron700.woff') format('woff'), + url('Orbitron700.ttf') format('truetype'); +} + +#noVNC_logo { + margin-top: 170px; + margin-left: 10px; + color:yellow; + text-align:left; + font-family: 'Orbitron', 'OrbitronTTF', sans-serif; + line-height:90%; + text-shadow: + 5px 5px 0 #000, + -1px -1px 0 #000, + 1px -1px 0 #000, + -1px 1px 0 #000, + 1px 1px 0 #000; +} + + +#noVNC_logo span{ + color:green; +} + +/* ---------------------------------------- + * Media sizing + * ---------------------------------------- + */ + + +.noVNC_status_button { + font-size: 12px; +} + +#noVNC_clipboard_text { + width: 500px; +} + +#noVNC_logo { + font-size: 180px; +} + +.noVNC-buttons-left { + padding-left: 10px; +} + +.noVNC-buttons-right { + padding-right: 10px; +} + +#noVNC_status { + z-index: 0; + position: absolute; + width: 100%; +} + +@media screen and (max-width: 640px){ + .noVNC_status_button { + font-size: 10px; + } + .noVNC-buttons-left { + padding-left: 0px; + } + .noVNC-buttons-right { + padding-right: 0px; + } + #noVNC_status { + z-index: 1; + position: relative; + width: auto; + float: left; + } +} + +@media screen and (min-width: 481px) and (max-width: 640px) { + #noVNC_clipboard_text { + width: 410px; + } + #noVNC_logo { + font-size: 150px; + } +} + +@media screen and (min-width: 321px) and (max-width: 480px) { + #noVNC_clipboard_text { + width: 250px; + } + #noVNC_logo { + font-size: 110px; + } +} + +@media screen and (max-width: 320px) { + .noVNC_status_button { + font-size: 9px; + } + #noVNC_clipboard_text { + width: 220px; + } + #noVNC_logo { + font-size: 90px; + } +} diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/base64.js b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/base64.js new file mode 100644 index 0000000000000000000000000000000000000000..5a6890ad29562652050fae6a7c43d08a2913403e --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/base64.js @@ -0,0 +1,115 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// From: http://hg.mozilla.org/mozilla-central/raw-file/ec10630b1a54/js/src/devtools/jint/sunspider/string-base64.js + +/*jslint white: false, bitwise: false, plusplus: false */ +/*global console */ + +var Base64 = { + +/* Convert data (an array of integers) to a Base64 string. */ +toBase64Table : 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='.split(''), +base64Pad : '=', + +encode: function (data) { + "use strict"; + var result = ''; + var toBase64Table = Base64.toBase64Table; + var length = data.length + var lengthpad = (length%3); + var i = 0, j = 0; + // Convert every three bytes to 4 ascii characters. + /* BEGIN LOOP */ + for (i = 0; i < (length - 2); i += 3) { + result += toBase64Table[data[i] >> 2]; + result += toBase64Table[((data[i] & 0x03) << 4) + (data[i+1] >> 4)]; + result += toBase64Table[((data[i+1] & 0x0f) << 2) + (data[i+2] >> 6)]; + result += toBase64Table[data[i+2] & 0x3f]; + } + /* END LOOP */ + + // Convert the remaining 1 or 2 bytes, pad out to 4 characters. + if (lengthpad === 2) { + j = length - lengthpad; + result += toBase64Table[data[j] >> 2]; + result += toBase64Table[((data[j] & 0x03) << 4) + (data[j+1] >> 4)]; + result += toBase64Table[(data[j+1] & 0x0f) << 2]; + result += toBase64Table[64]; + } else if (lengthpad === 1) { + j = length - lengthpad; + result += toBase64Table[data[j] >> 2]; + result += toBase64Table[(data[j] & 0x03) << 4]; + result += toBase64Table[64]; + result += toBase64Table[64]; + } + + return result; +}, + +/* Convert Base64 data to a string */ +toBinaryTable : [ + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63, + 52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14, + 15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1, + -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40, + 41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1 +], + +decode: function (data, offset) { + "use strict"; + offset = typeof(offset) !== 'undefined' ? offset : 0; + var toBinaryTable = Base64.toBinaryTable; + var base64Pad = Base64.base64Pad; + var result, result_length, idx, i, c, padding; + var leftbits = 0; // number of bits decoded, but yet to be appended + var leftdata = 0; // bits decoded, but yet to be appended + var data_length = data.indexOf('=') - offset; + + if (data_length < 0) { data_length = data.length - offset; } + + /* Every four characters is 3 resulting numbers */ + result_length = (data_length >> 2) * 3 + Math.floor((data_length%4)/1.5); + result = new Array(result_length); + + // Convert one by one. + /* BEGIN LOOP */ + for (idx = 0, i = offset; i < data.length; i++) { + c = toBinaryTable[data.charCodeAt(i) & 0x7f]; + padding = (data.charAt(i) === base64Pad); + // Skip illegal characters and whitespace + if (c === -1) { + console.error("Illegal character code " + data.charCodeAt(i) + " at position " + i); + continue; + } + + // Collect data into leftdata, update bitcount + leftdata = (leftdata << 6) | c; + leftbits += 6; + + // If we have 8 or more bits, append 8 bits to the result + if (leftbits >= 8) { + leftbits -= 8; + // Append if not padding. + if (!padding) { + result[idx++] = (leftdata >> leftbits) & 0xff; + } + leftdata &= (1 << leftbits) - 1; + } + } + /* END LOOP */ + + // If there are any bits left, the base64 string was corrupted + if (leftbits) { + throw {name: 'Base64-Error', + message: 'Corrupted base64 string'}; + } + + return result; +} + +}; /* End of Base64 namespace */ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/black.css b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/black.css new file mode 100644 index 0000000000000000000000000000000000000000..7d940c5af8e326f14cc98499d646c10d89c8c3dc --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/black.css @@ -0,0 +1,71 @@ +/* + * noVNC black CSS + * Copyright (C) 2012 Joel Martin + * Copyright (C) 2013 Samuel Mannehed for Cendio AB + * noVNC is licensed under the MPL 2.0 (see LICENSE.txt) + * This file is licensed under the 2-Clause BSD license (see LICENSE.txt). + */ + +#keyboardinput { + background-color:#000; +} + +.noVNC_status_normal { + background: #4c4c4c; /* Old browsers */ + background: -moz-linear-gradient(top, #4c4c4c 0%, #2c2c2c 50%, #000000 51%, #131313 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#4c4c4c), color-stop(50%,#2c2c2c), color-stop(51%,#000000), color-stop(100%,#131313)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #4c4c4c 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #4c4c4c 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #4c4c4c 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* IE10+ */ + background: linear-gradient(top, #4c4c4c 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* W3C */ +} +.noVNC_status_error { + background: #f04040; /* Old browsers */ + background: -moz-linear-gradient(top, #f04040 0%, #2c2c2c 50%, #000000 51%, #131313 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f04040), color-stop(50%,#2c2c2c), color-stop(51%,#000000), color-stop(100%,#131313)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #f04040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #f04040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #f04040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* IE10+ */ + background: linear-gradient(top, #f04040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* W3C */ +} +.noVNC_status_warn { + background: #f0f040; /* Old browsers */ + background: -moz-linear-gradient(top, #f0f040 0%, #2c2c2c 50%, #000000 51%, #131313 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f0f040), color-stop(50%,#2c2c2c), color-stop(51%,#000000), color-stop(100%,#131313)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #f0f040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #f0f040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #f0f040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* IE10+ */ + background: linear-gradient(top, #f0f040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* W3C */ +} + +.triangle-right { + border:2px solid #fff; + background:#000; + color:#fff; +} + +.noVNC_status_button { + font-size: 12px; + vertical-align: middle; + border:1px solid #4c4c4c; + + background: #4c4c4c; /* Old browsers */ + background: -moz-linear-gradient(top, #4c4c4c 0%, #2c2c2c 50%, #000000 51%, #131313 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#4c4c4c), color-stop(50%,#2c2c2c), color-stop(51%,#000000), color-stop(100%,#131313)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #4c4c4c 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #4c4c4c 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #4c4c4c 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* IE10+ */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#4c4c4c', endColorstr='#131313',GradientType=0 ); /* IE6-9 */ + background: linear-gradient(top, #4c4c4c 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* W3C */ +} + +.noVNC_status_button_selected { + background: #9dd53a; /* Old browsers */ + background: -moz-linear-gradient(top, #9dd53a 0%, #a1d54f 50%, #80c217 51%, #7cbc0a 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#9dd53a), color-stop(50%,#a1d54f), color-stop(51%,#80c217), color-stop(100%,#7cbc0a)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #9dd53a 0%,#a1d54f 50%,#80c217 51%,#7cbc0a 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #9dd53a 0%,#a1d54f 50%,#80c217 51%,#7cbc0a 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #9dd53a 0%,#a1d54f 50%,#80c217 51%,#7cbc0a 100%); /* IE10+ */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#9dd53a', endColorstr='#7cbc0a',GradientType=0 ); /* IE6-9 */ + background: linear-gradient(top, #9dd53a 0%,#a1d54f 50%,#80c217 51%,#7cbc0a 100%); /* W3C */ +} diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/blue.css b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/blue.css new file mode 100644 index 0000000000000000000000000000000000000000..b2a0adcc92a893aea75ffd708d0d8cdfd484d370 --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/blue.css @@ -0,0 +1,64 @@ +/* + * noVNC blue CSS + * Copyright (C) 2012 Joel Martin + * Copyright (C) 2013 Samuel Mannehed for Cendio AB + * noVNC is licensed under the MPL 2.0 (see LICENSE.txt) + * This file is licensed under the 2-Clause BSD license (see LICENSE.txt). + */ + +.noVNC_status_normal { + background-color:#04073d; + background-image: -webkit-gradient( + linear, + left bottom, + left top, + color-stop(0.54, rgb(10,15,79)), + color-stop(0.5, rgb(4,7,61)) + ); + background-image: -moz-linear-gradient( + center bottom, + rgb(10,15,79) 54%, + rgb(4,7,61) 50% + ); +} +.noVNC_status_error { + background-color:#f04040; + background-image: -webkit-gradient( + linear, + left bottom, + left top, + color-stop(0.54, rgb(240,64,64)), + color-stop(0.5, rgb(4,7,61)) + ); + background-image: -moz-linear-gradient( + center bottom, + rgb(4,7,61) 54%, + rgb(249,64,64) 50% + ); +} +.noVNC_status_warn { + background-color:#f0f040; + background-image: -webkit-gradient( + linear, + left bottom, + left top, + color-stop(0.54, rgb(240,240,64)), + color-stop(0.5, rgb(4,7,61)) + ); + background-image: -moz-linear-gradient( + center bottom, + rgb(4,7,61) 54%, + rgb(240,240,64) 50% + ); +} + +.triangle-right { + border:2px solid #fff; + background:#04073d; + color:#fff; +} + +#keyboardinput { + background-color:#04073d; +} + diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/chrome-app/tcp-client.js b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/chrome-app/tcp-client.js new file mode 100644 index 0000000000000000000000000000000000000000..b8c125f5c328be39ac502b82ae85e4ada7a64c43 --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/chrome-app/tcp-client.js @@ -0,0 +1,321 @@ +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Author: Boris Smus (smus@chromium.org) +*/ + +(function(exports) { + + // Define some local variables here. + var socket = chrome.socket || chrome.experimental.socket; + var dns = chrome.experimental.dns; + + /** + * Creates an instance of the client + * + * @param {String} host The remote host to connect to + * @param {Number} port The port to connect to at the remote host + */ + function TcpClient(host, port, pollInterval) { + this.host = host; + this.port = port; + this.pollInterval = pollInterval || 15; + + // Callback functions. + this.callbacks = { + connect: null, // Called when socket is connected. + disconnect: null, // Called when socket is disconnected. + recvBuffer: null, // Called (as ArrayBuffer) when client receives data from server. + recvString: null, // Called (as string) when client receives data from server. + sent: null // Called when client sends data to server. + }; + + // Socket. + this.socketId = null; + this.isConnected = false; + + log('initialized tcp client'); + } + + /** + * Connects to the TCP socket, and creates an open socket. + * + * @see http://developer.chrome.com/trunk/apps/socket.html#method-create + * @param {Function} callback The function to call on connection + */ + TcpClient.prototype.connect = function(callback) { + // First resolve the hostname to an IP. + dns.resolve(this.host, function(result) { + this.addr = result.address; + socket.create('tcp', {}, this._onCreate.bind(this)); + + // Register connect callback. + this.callbacks.connect = callback; + }.bind(this)); + }; + + /** + * Sends an arraybuffer/view down the wire to the remote side + * + * @see http://developer.chrome.com/trunk/apps/socket.html#method-write + * @param {String} msg The arraybuffer/view to send + * @param {Function} callback The function to call when the message has sent + */ + TcpClient.prototype.sendBuffer = function(buf, callback) { + if (buf.buffer) { + buf = buf.buffer; + } + + /* + // Debug + var bytes = [], u8 = new Uint8Array(buf); + for (var i = 0; i < u8.length; i++) { + bytes.push(u8[i]); + } + log("sending bytes: " + (bytes.join(','))); + */ + + socket.write(this.socketId, buf, this._onWriteComplete.bind(this)); + + // Register sent callback. + this.callbacks.sent = callback; + }; + + /** + * Sends a string down the wire to the remote side + * + * @see http://developer.chrome.com/trunk/apps/socket.html#method-write + * @param {String} msg The string to send + * @param {Function} callback The function to call when the message has sent + */ + TcpClient.prototype.sendString = function(msg, callback) { + /* + // Debug + log("sending string: " + msg); + */ + + this._stringToArrayBuffer(msg, function(arrayBuffer) { + socket.write(this.socketId, arrayBuffer, this._onWriteComplete.bind(this)); + }.bind(this)); + + // Register sent callback. + this.callbacks.sent = callback; + }; + + /** + * Sets the callback for when a message is received + * + * @param {Function} callback The function to call when a message has arrived + * @param {String} type The callback argument type: "arraybuffer" or "string" + */ + TcpClient.prototype.addResponseListener = function(callback, type) { + if (typeof type === "undefined") { + type = "arraybuffer"; + } + // Register received callback. + if (type === "string") { + this.callbacks.recvString = callback; + } else { + this.callbacks.recvBuffer = callback; + } + }; + + /** + * Sets the callback for when the socket disconnects + * + * @param {Function} callback The function to call when the socket disconnects + * @param {String} type The callback argument type: "arraybuffer" or "string" + */ + TcpClient.prototype.addDisconnectListener = function(callback) { + // Register disconnect callback. + this.callbacks.disconnect = callback; + }; + + /** + * Disconnects from the remote side + * + * @see http://developer.chrome.com/trunk/apps/socket.html#method-disconnect + */ + TcpClient.prototype.disconnect = function() { + if (this.isConnected) { + this.isConnected = false; + socket.disconnect(this.socketId); + if (this.callbacks.disconnect) { + this.callbacks.disconnect(); + } + log('socket disconnected'); + } + }; + + /** + * The callback function used for when we attempt to have Chrome + * create a socket. If the socket is successfully created + * we go ahead and connect to the remote side. + * + * @private + * @see http://developer.chrome.com/trunk/apps/socket.html#method-connect + * @param {Object} createInfo The socket details + */ + TcpClient.prototype._onCreate = function(createInfo) { + this.socketId = createInfo.socketId; + if (this.socketId > 0) { + socket.connect(this.socketId, this.addr, this.port, this._onConnectComplete.bind(this)); + } else { + error('Unable to create socket'); + } + }; + + /** + * The callback function used for when we attempt to have Chrome + * connect to the remote side. If a successful connection is + * made then polling starts to check for data to read + * + * @private + * @param {Number} resultCode Indicates whether the connection was successful + */ + TcpClient.prototype._onConnectComplete = function(resultCode) { + // Start polling for reads. + this.isConnected = true; + setTimeout(this._periodicallyRead.bind(this), this.pollInterval); + + if (this.callbacks.connect) { + log('connect complete'); + this.callbacks.connect(); + } + log('onConnectComplete'); + }; + + /** + * Checks for new data to read from the socket + * + * @see http://developer.chrome.com/trunk/apps/socket.html#method-read + */ + TcpClient.prototype._periodicallyRead = function() { + var that = this; + socket.getInfo(this.socketId, function (info) { + if (info.connected) { + setTimeout(that._periodicallyRead.bind(that), that.pollInterval); + socket.read(that.socketId, null, that._onDataRead.bind(that)); + } else if (that.isConnected) { + log('socket disconnect detected'); + that.disconnect(); + } + }); + }; + + /** + * Callback function for when data has been read from the socket. + * Converts the array buffer that is read in to a string + * and sends it on for further processing by passing it to + * the previously assigned callback function. + * + * @private + * @see TcpClient.prototype.addResponseListener + * @param {Object} readInfo The incoming message + */ + TcpClient.prototype._onDataRead = function(readInfo) { + // Call received callback if there's data in the response. + if (readInfo.resultCode > 0) { + log('onDataRead'); + + /* + // Debug + var bytes = [], u8 = new Uint8Array(readInfo.data); + for (var i = 0; i < u8.length; i++) { + bytes.push(u8[i]); + } + log("received bytes: " + (bytes.join(','))); + */ + + if (this.callbacks.recvBuffer) { + // Return raw ArrayBuffer directly. + this.callbacks.recvBuffer(readInfo.data); + } + if (this.callbacks.recvString) { + // Convert ArrayBuffer to string. + this._arrayBufferToString(readInfo.data, function(str) { + this.callbacks.recvString(str); + }.bind(this)); + } + + // Trigger another read right away + setTimeout(this._periodicallyRead.bind(this), 0); + } + }; + + /** + * Callback for when data has been successfully + * written to the socket. + * + * @private + * @param {Object} writeInfo The outgoing message + */ + TcpClient.prototype._onWriteComplete = function(writeInfo) { + log('onWriteComplete'); + // Call sent callback. + if (this.callbacks.sent) { + this.callbacks.sent(writeInfo); + } + }; + + /** + * Converts an array buffer to a string + * + * @private + * @param {ArrayBuffer} buf The buffer to convert + * @param {Function} callback The function to call when conversion is complete + */ + TcpClient.prototype._arrayBufferToString = function(buf, callback) { + var bb = new Blob([new Uint8Array(buf)]); + var f = new FileReader(); + f.onload = function(e) { + callback(e.target.result); + }; + f.readAsText(bb); + }; + + /** + * Converts a string to an array buffer + * + * @private + * @param {String} str The string to convert + * @param {Function} callback The function to call when conversion is complete + */ + TcpClient.prototype._stringToArrayBuffer = function(str, callback) { + var bb = new Blob([str]); + var f = new FileReader(); + f.onload = function(e) { + callback(e.target.result); + }; + f.readAsArrayBuffer(bb); + }; + + /** + * Wrapper function for logging + */ + function log(msg) { + console.log(msg); + } + + /** + * Wrapper function for error logging + */ + function error(msg) { + console.error(msg); + } + + exports.TcpClient = TcpClient; + +})(window); diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/des.js b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/des.js new file mode 100644 index 0000000000000000000000000000000000000000..1f952851e391200709934cd0dba293c803b5e2b8 --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/des.js @@ -0,0 +1,273 @@ +/* + * Ported from Flashlight VNC ActionScript implementation: + * http://www.wizhelp.com/flashlight-vnc/ + * + * Full attribution follows: + * + * ------------------------------------------------------------------------- + * + * This DES class has been extracted from package Acme.Crypto for use in VNC. + * The unnecessary odd parity code has been removed. + * + * These changes are: + * Copyright (C) 1999 AT&T Laboratories Cambridge. All Rights Reserved. + * + * This software 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. + * + + * DesCipher - the DES encryption method + * + * The meat of this code is by Dave Zimmerman <dzimm@widget.com>, and is: + * + * Copyright (c) 1996 Widget Workshop, Inc. All Rights Reserved. + * + * Permission to use, copy, modify, and distribute this software + * and its documentation for NON-COMMERCIAL or COMMERCIAL purposes and + * without fee is hereby granted, provided that this copyright notice is kept + * intact. + * + * WIDGET WORKSHOP MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY + * OF THE SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + * TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE, OR NON-INFRINGEMENT. WIDGET WORKSHOP SHALL NOT BE LIABLE + * FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR + * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. + * + * THIS SOFTWARE IS NOT DESIGNED OR INTENDED FOR USE OR RESALE AS ON-LINE + * CONTROL EQUIPMENT IN HAZARDOUS ENVIRONMENTS REQUIRING FAIL-SAFE + * PERFORMANCE, SUCH AS IN THE OPERATION OF NUCLEAR FACILITIES, AIRCRAFT + * NAVIGATION OR COMMUNICATION SYSTEMS, AIR TRAFFIC CONTROL, DIRECT LIFE + * SUPPORT MACHINES, OR WEAPONS SYSTEMS, IN WHICH THE FAILURE OF THE + * SOFTWARE COULD LEAD DIRECTLY TO DEATH, PERSONAL INJURY, OR SEVERE + * PHYSICAL OR ENVIRONMENTAL DAMAGE ("HIGH RISK ACTIVITIES"). WIDGET WORKSHOP + * SPECIFICALLY DISCLAIMS ANY EXPRESS OR IMPLIED WARRANTY OF FITNESS FOR + * HIGH RISK ACTIVITIES. + * + * + * The rest is: + * + * Copyright (C) 1996 by Jef Poskanzer <jef@acme.com>. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * Visit the ACME Labs Java page for up-to-date versions of this and other + * fine Java utilities: http://www.acme.com/java/ + */ + +"use strict"; +/*jslint white: false, bitwise: false, plusplus: false */ + +function DES(passwd) { + +// Tables, permutations, S-boxes, etc. +var PC2 = [13,16,10,23, 0, 4, 2,27,14, 5,20, 9,22,18,11, 3, + 25, 7,15, 6,26,19,12, 1,40,51,30,36,46,54,29,39, + 50,44,32,47,43,48,38,55,33,52,45,41,49,35,28,31 ], + totrot = [ 1, 2, 4, 6, 8,10,12,14,15,17,19,21,23,25,27,28], + z = 0x0, a,b,c,d,e,f, SP1,SP2,SP3,SP4,SP5,SP6,SP7,SP8, + keys = []; + +a=1<<16; b=1<<24; c=a|b; d=1<<2; e=1<<10; f=d|e; +SP1 = [c|e,z|z,a|z,c|f,c|d,a|f,z|d,a|z,z|e,c|e,c|f,z|e,b|f,c|d,b|z,z|d, + z|f,b|e,b|e,a|e,a|e,c|z,c|z,b|f,a|d,b|d,b|d,a|d,z|z,z|f,a|f,b|z, + a|z,c|f,z|d,c|z,c|e,b|z,b|z,z|e,c|d,a|z,a|e,b|d,z|e,z|d,b|f,a|f, + c|f,a|d,c|z,b|f,b|d,z|f,a|f,c|e,z|f,b|e,b|e,z|z,a|d,a|e,z|z,c|d]; +a=1<<20; b=1<<31; c=a|b; d=1<<5; e=1<<15; f=d|e; +SP2 = [c|f,b|e,z|e,a|f,a|z,z|d,c|d,b|f,b|d,c|f,c|e,b|z,b|e,a|z,z|d,c|d, + a|e,a|d,b|f,z|z,b|z,z|e,a|f,c|z,a|d,b|d,z|z,a|e,z|f,c|e,c|z,z|f, + z|z,a|f,c|d,a|z,b|f,c|z,c|e,z|e,c|z,b|e,z|d,c|f,a|f,z|d,z|e,b|z, + z|f,c|e,a|z,b|d,a|d,b|f,b|d,a|d,a|e,z|z,b|e,z|f,b|z,c|d,c|f,a|e]; +a=1<<17; b=1<<27; c=a|b; d=1<<3; e=1<<9; f=d|e; +SP3 = [z|f,c|e,z|z,c|d,b|e,z|z,a|f,b|e,a|d,b|d,b|d,a|z,c|f,a|d,c|z,z|f, + b|z,z|d,c|e,z|e,a|e,c|z,c|d,a|f,b|f,a|e,a|z,b|f,z|d,c|f,z|e,b|z, + c|e,b|z,a|d,z|f,a|z,c|e,b|e,z|z,z|e,a|d,c|f,b|e,b|d,z|e,z|z,c|d, + b|f,a|z,b|z,c|f,z|d,a|f,a|e,b|d,c|z,b|f,z|f,c|z,a|f,z|d,c|d,a|e]; +a=1<<13; b=1<<23; c=a|b; d=1<<0; e=1<<7; f=d|e; +SP4 = [c|d,a|f,a|f,z|e,c|e,b|f,b|d,a|d,z|z,c|z,c|z,c|f,z|f,z|z,b|e,b|d, + z|d,a|z,b|z,c|d,z|e,b|z,a|d,a|e,b|f,z|d,a|e,b|e,a|z,c|e,c|f,z|f, + b|e,b|d,c|z,c|f,z|f,z|z,z|z,c|z,a|e,b|e,b|f,z|d,c|d,a|f,a|f,z|e, + c|f,z|f,z|d,a|z,b|d,a|d,c|e,b|f,a|d,a|e,b|z,c|d,z|e,b|z,a|z,c|e]; +a=1<<25; b=1<<30; c=a|b; d=1<<8; e=1<<19; f=d|e; +SP5 = [z|d,a|f,a|e,c|d,z|e,z|d,b|z,a|e,b|f,z|e,a|d,b|f,c|d,c|e,z|f,b|z, + a|z,b|e,b|e,z|z,b|d,c|f,c|f,a|d,c|e,b|d,z|z,c|z,a|f,a|z,c|z,z|f, + z|e,c|d,z|d,a|z,b|z,a|e,c|d,b|f,a|d,b|z,c|e,a|f,b|f,z|d,a|z,c|e, + c|f,z|f,c|z,c|f,a|e,z|z,b|e,c|z,z|f,a|d,b|d,z|e,z|z,b|e,a|f,b|d]; +a=1<<22; b=1<<29; c=a|b; d=1<<4; e=1<<14; f=d|e; +SP6 = [b|d,c|z,z|e,c|f,c|z,z|d,c|f,a|z,b|e,a|f,a|z,b|d,a|d,b|e,b|z,z|f, + z|z,a|d,b|f,z|e,a|e,b|f,z|d,c|d,c|d,z|z,a|f,c|e,z|f,a|e,c|e,b|z, + b|e,z|d,c|d,a|e,c|f,a|z,z|f,b|d,a|z,b|e,b|z,z|f,b|d,c|f,a|e,c|z, + a|f,c|e,z|z,c|d,z|d,z|e,c|z,a|f,z|e,a|d,b|f,z|z,c|e,b|z,a|d,b|f]; +a=1<<21; b=1<<26; c=a|b; d=1<<1; e=1<<11; f=d|e; +SP7 = [a|z,c|d,b|f,z|z,z|e,b|f,a|f,c|e,c|f,a|z,z|z,b|d,z|d,b|z,c|d,z|f, + b|e,a|f,a|d,b|e,b|d,c|z,c|e,a|d,c|z,z|e,z|f,c|f,a|e,z|d,b|z,a|e, + b|z,a|e,a|z,b|f,b|f,c|d,c|d,z|d,a|d,b|z,b|e,a|z,c|e,z|f,a|f,c|e, + z|f,b|d,c|f,c|z,a|e,z|z,z|d,c|f,z|z,a|f,c|z,z|e,b|d,b|e,z|e,a|d]; +a=1<<18; b=1<<28; c=a|b; d=1<<6; e=1<<12; f=d|e; +SP8 = [b|f,z|e,a|z,c|f,b|z,b|f,z|d,b|z,a|d,c|z,c|f,a|e,c|e,a|f,z|e,z|d, + c|z,b|d,b|e,z|f,a|e,a|d,c|d,c|e,z|f,z|z,z|z,c|d,b|d,b|e,a|f,a|z, + a|f,a|z,c|e,z|e,z|d,c|d,z|e,a|f,b|e,z|d,b|d,c|z,c|d,b|z,a|z,b|f, + z|z,c|f,a|d,b|d,c|z,b|e,b|f,z|z,c|f,a|e,a|e,z|f,z|f,a|d,b|z,c|e]; + +// Set the key. +function setKeys(keyBlock) { + var i, j, l, m, n, o, pc1m = [], pcr = [], kn = [], + raw0, raw1, rawi, KnLi; + + for (j = 0, l = 56; j < 56; ++j, l-=8) { + l += l<-5 ? 65 : l<-3 ? 31 : l<-1 ? 63 : l===27 ? 35 : 0; // PC1 + m = l & 0x7; + pc1m[j] = ((keyBlock[l >>> 3] & (1<<m)) !== 0) ? 1: 0; + } + + for (i = 0; i < 16; ++i) { + m = i << 1; + n = m + 1; + kn[m] = kn[n] = 0; + for (o=28; o<59; o+=28) { + for (j = o-28; j < o; ++j) { + l = j + totrot[i]; + if (l < o) { + pcr[j] = pc1m[l]; + } else { + pcr[j] = pc1m[l - 28]; + } + } + } + for (j = 0; j < 24; ++j) { + if (pcr[PC2[j]] !== 0) { + kn[m] |= 1<<(23-j); + } + if (pcr[PC2[j + 24]] !== 0) { + kn[n] |= 1<<(23-j); + } + } + } + + // cookey + for (i = 0, rawi = 0, KnLi = 0; i < 16; ++i) { + raw0 = kn[rawi++]; + raw1 = kn[rawi++]; + keys[KnLi] = (raw0 & 0x00fc0000) << 6; + keys[KnLi] |= (raw0 & 0x00000fc0) << 10; + keys[KnLi] |= (raw1 & 0x00fc0000) >>> 10; + keys[KnLi] |= (raw1 & 0x00000fc0) >>> 6; + ++KnLi; + keys[KnLi] = (raw0 & 0x0003f000) << 12; + keys[KnLi] |= (raw0 & 0x0000003f) << 16; + keys[KnLi] |= (raw1 & 0x0003f000) >>> 4; + keys[KnLi] |= (raw1 & 0x0000003f); + ++KnLi; + } +} + +// Encrypt 8 bytes of text +function enc8(text) { + var i = 0, b = text.slice(), fval, keysi = 0, + l, r, x; // left, right, accumulator + + // Squash 8 bytes to 2 ints + l = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++]; + r = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++]; + + x = ((l >>> 4) ^ r) & 0x0f0f0f0f; + r ^= x; + l ^= (x << 4); + x = ((l >>> 16) ^ r) & 0x0000ffff; + r ^= x; + l ^= (x << 16); + x = ((r >>> 2) ^ l) & 0x33333333; + l ^= x; + r ^= (x << 2); + x = ((r >>> 8) ^ l) & 0x00ff00ff; + l ^= x; + r ^= (x << 8); + r = (r << 1) | ((r >>> 31) & 1); + x = (l ^ r) & 0xaaaaaaaa; + l ^= x; + r ^= x; + l = (l << 1) | ((l >>> 31) & 1); + + for (i = 0; i < 8; ++i) { + x = (r << 28) | (r >>> 4); + x ^= keys[keysi++]; + fval = SP7[x & 0x3f]; + fval |= SP5[(x >>> 8) & 0x3f]; + fval |= SP3[(x >>> 16) & 0x3f]; + fval |= SP1[(x >>> 24) & 0x3f]; + x = r ^ keys[keysi++]; + fval |= SP8[x & 0x3f]; + fval |= SP6[(x >>> 8) & 0x3f]; + fval |= SP4[(x >>> 16) & 0x3f]; + fval |= SP2[(x >>> 24) & 0x3f]; + l ^= fval; + x = (l << 28) | (l >>> 4); + x ^= keys[keysi++]; + fval = SP7[x & 0x3f]; + fval |= SP5[(x >>> 8) & 0x3f]; + fval |= SP3[(x >>> 16) & 0x3f]; + fval |= SP1[(x >>> 24) & 0x3f]; + x = l ^ keys[keysi++]; + fval |= SP8[x & 0x0000003f]; + fval |= SP6[(x >>> 8) & 0x3f]; + fval |= SP4[(x >>> 16) & 0x3f]; + fval |= SP2[(x >>> 24) & 0x3f]; + r ^= fval; + } + + r = (r << 31) | (r >>> 1); + x = (l ^ r) & 0xaaaaaaaa; + l ^= x; + r ^= x; + l = (l << 31) | (l >>> 1); + x = ((l >>> 8) ^ r) & 0x00ff00ff; + r ^= x; + l ^= (x << 8); + x = ((l >>> 2) ^ r) & 0x33333333; + r ^= x; + l ^= (x << 2); + x = ((r >>> 16) ^ l) & 0x0000ffff; + l ^= x; + r ^= (x << 16); + x = ((r >>> 4) ^ l) & 0x0f0f0f0f; + l ^= x; + r ^= (x << 4); + + // Spread ints to bytes + x = [r, l]; + for (i = 0; i < 8; i++) { + b[i] = (x[i>>>2] >>> (8*(3 - (i%4)))) % 256; + if (b[i] < 0) { b[i] += 256; } // unsigned + } + return b; +} + +// Encrypt 16 bytes of text using passwd as key +function encrypt(t) { + return enc8(t.slice(0,8)).concat(enc8(t.slice(8,16))); +} + +setKeys(passwd); // Setup keys +return {'encrypt': encrypt}; // Public interface + +} // function DES diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/display.js b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/display.js new file mode 100644 index 0000000000000000000000000000000000000000..9f2d6b89b80c4e5afcda9c267300a6a76b3e1c12 --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/display.js @@ -0,0 +1,770 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2012 Joel Martin + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +/*jslint browser: true, white: false, bitwise: false */ +/*global Util, Base64, changeCursor */ + +function Display(defaults) { +"use strict"; + +var that = {}, // Public API methods + conf = {}, // Configuration attributes + + // Private Display namespace variables + c_ctx = null, + c_forceCanvas = false, + + // Queued drawing actions for in-order rendering + renderQ = [], + + // Predefine function variables (jslint) + imageDataGet, rgbImageData, bgrxImageData, cmapImageData, + setFillColor, rescale, scan_renderQ, + + // The full frame buffer (logical canvas) size + fb_width = 0, + fb_height = 0, + // The visible "physical canvas" viewport + viewport = {'x': 0, 'y': 0, 'w' : 0, 'h' : 0 }, + cleanRect = {'x1': 0, 'y1': 0, 'x2': -1, 'y2': -1}, + + c_prevStyle = "", + tile = null, + tile16x16 = null, + tile_x = 0, + tile_y = 0; + + +// Configuration attributes +Util.conf_defaults(conf, that, defaults, [ + ['target', 'wo', 'dom', null, 'Canvas element for rendering'], + ['context', 'ro', 'raw', null, 'Canvas 2D context for rendering (read-only)'], + ['logo', 'rw', 'raw', null, 'Logo to display when cleared: {"width": width, "height": height, "data": data}'], + ['true_color', 'rw', 'bool', true, 'Use true-color pixel data'], + ['colourMap', 'rw', 'arr', [], 'Colour map array (when not true-color)'], + ['scale', 'rw', 'float', 1.0, 'Display area scale factor 0.0 - 1.0'], + ['viewport', 'rw', 'bool', false, 'Use a viewport set with viewportChange()'], + ['width', 'rw', 'int', null, 'Display area width'], + ['height', 'rw', 'int', null, 'Display area height'], + + ['render_mode', 'ro', 'str', '', 'Canvas rendering mode (read-only)'], + + ['prefer_js', 'rw', 'str', null, 'Prefer Javascript over canvas methods'], + ['cursor_uri', 'rw', 'raw', null, 'Can we render cursor using data URI'] + ]); + +// Override some specific getters/setters +that.get_context = function () { return c_ctx; }; + +that.set_scale = function(scale) { rescale(scale); }; + +that.set_width = function (val) { that.resize(val, fb_height); }; +that.get_width = function() { return fb_width; }; + +that.set_height = function (val) { that.resize(fb_width, val); }; +that.get_height = function() { return fb_height; }; + + + +// +// Private functions +// + +// Create the public API interface +function constructor() { + Util.Debug(">> Display.constructor"); + + var c, func, i, curDat, curSave, + has_imageData = false, UE = Util.Engine; + + if (! conf.target) { throw("target must be set"); } + + if (typeof conf.target === 'string') { + throw("target must be a DOM element"); + } + + c = conf.target; + + if (! c.getContext) { throw("no getContext method"); } + + if (! c_ctx) { c_ctx = c.getContext('2d'); } + + Util.Debug("User Agent: " + navigator.userAgent); + if (UE.gecko) { Util.Debug("Browser: gecko " + UE.gecko); } + if (UE.webkit) { Util.Debug("Browser: webkit " + UE.webkit); } + if (UE.trident) { Util.Debug("Browser: trident " + UE.trident); } + if (UE.presto) { Util.Debug("Browser: presto " + UE.presto); } + + that.clear(); + + // Check canvas features + if ('createImageData' in c_ctx) { + conf.render_mode = "canvas rendering"; + } else { + throw("Canvas does not support createImageData"); + } + if (conf.prefer_js === null) { + Util.Info("Prefering javascript operations"); + conf.prefer_js = true; + } + + // Initialize cached tile imageData + tile16x16 = c_ctx.createImageData(16, 16); + + /* + * Determine browser support for setting the cursor via data URI + * scheme + */ + curDat = []; + for (i=0; i < 8 * 8 * 4; i += 1) { + curDat.push(255); + } + try { + curSave = c.style.cursor; + changeCursor(conf.target, curDat, curDat, 2, 2, 8, 8); + if (c.style.cursor) { + if (conf.cursor_uri === null) { + conf.cursor_uri = true; + } + Util.Info("Data URI scheme cursor supported"); + } else { + if (conf.cursor_uri === null) { + conf.cursor_uri = false; + } + Util.Warn("Data URI scheme cursor not supported"); + } + c.style.cursor = curSave; + } catch (exc2) { + Util.Error("Data URI scheme cursor test exception: " + exc2); + conf.cursor_uri = false; + } + + Util.Debug("<< Display.constructor"); + return that ; +} + +rescale = function(factor) { + var c, tp, x, y, + properties = ['transform', 'WebkitTransform', 'MozTransform', null]; + c = conf.target; + tp = properties.shift(); + while (tp) { + if (typeof c.style[tp] !== 'undefined') { + break; + } + tp = properties.shift(); + } + + if (tp === null) { + Util.Debug("No scaling support"); + return; + } + + + if (typeof(factor) === "undefined") { + factor = conf.scale; + } else if (factor > 1.0) { + factor = 1.0; + } else if (factor < 0.1) { + factor = 0.1; + } + + if (conf.scale === factor) { + //Util.Debug("Display already scaled to '" + factor + "'"); + return; + } + + conf.scale = factor; + x = c.width - c.width * factor; + y = c.height - c.height * factor; + c.style[tp] = "scale(" + conf.scale + ") translate(-" + x + "px, -" + y + "px)"; +}; + +setFillColor = function(color) { + var bgr, newStyle; + if (conf.true_color) { + bgr = color; + } else { + bgr = conf.colourMap[color[0]]; + } + newStyle = "rgb(" + bgr[2] + "," + bgr[1] + "," + bgr[0] + ")"; + if (newStyle !== c_prevStyle) { + c_ctx.fillStyle = newStyle; + c_prevStyle = newStyle; + } +}; + + +// +// Public API interface functions +// + +// Shift and/or resize the visible viewport +that.viewportChange = function(deltaX, deltaY, width, height) { + var c = conf.target, v = viewport, cr = cleanRect, + saveImg = null, saveStyle, x1, y1, vx2, vy2, w, h; + + if (!conf.viewport) { + Util.Debug("Setting viewport to full display region"); + deltaX = -v.w; // Clamped later if out of bounds + deltaY = -v.h; // Clamped later if out of bounds + width = fb_width; + height = fb_height; + } + + if (typeof(deltaX) === "undefined") { deltaX = 0; } + if (typeof(deltaY) === "undefined") { deltaY = 0; } + if (typeof(width) === "undefined") { width = v.w; } + if (typeof(height) === "undefined") { height = v.h; } + + // Size change + + if (width > fb_width) { width = fb_width; } + if (height > fb_height) { height = fb_height; } + + if ((v.w !== width) || (v.h !== height)) { + // Change width + if ((width < v.w) && (cr.x2 > v.x + width -1)) { + cr.x2 = v.x + width - 1; + } + v.w = width; + + // Change height + if ((height < v.h) && (cr.y2 > v.y + height -1)) { + cr.y2 = v.y + height - 1; + } + v.h = height; + + + if (v.w > 0 && v.h > 0 && c.width > 0 && c.height > 0) { + saveImg = c_ctx.getImageData(0, 0, + (c.width < v.w) ? c.width : v.w, + (c.height < v.h) ? c.height : v.h); + } + + c.width = v.w; + c.height = v.h; + + if (saveImg) { + c_ctx.putImageData(saveImg, 0, 0); + } + } + + vx2 = v.x + v.w - 1; + vy2 = v.y + v.h - 1; + + + // Position change + + if ((deltaX < 0) && ((v.x + deltaX) < 0)) { + deltaX = - v.x; + } + if ((vx2 + deltaX) >= fb_width) { + deltaX -= ((vx2 + deltaX) - fb_width + 1); + } + + if ((v.y + deltaY) < 0) { + deltaY = - v.y; + } + if ((vy2 + deltaY) >= fb_height) { + deltaY -= ((vy2 + deltaY) - fb_height + 1); + } + + if ((deltaX === 0) && (deltaY === 0)) { + //Util.Debug("skipping viewport change"); + return; + } + Util.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY); + + v.x += deltaX; + vx2 += deltaX; + v.y += deltaY; + vy2 += deltaY; + + // Update the clean rectangle + if (v.x > cr.x1) { + cr.x1 = v.x; + } + if (vx2 < cr.x2) { + cr.x2 = vx2; + } + if (v.y > cr.y1) { + cr.y1 = v.y; + } + if (vy2 < cr.y2) { + cr.y2 = vy2; + } + + if (deltaX < 0) { + // Shift viewport left, redraw left section + x1 = 0; + w = - deltaX; + } else { + // Shift viewport right, redraw right section + x1 = v.w - deltaX; + w = deltaX; + } + if (deltaY < 0) { + // Shift viewport up, redraw top section + y1 = 0; + h = - deltaY; + } else { + // Shift viewport down, redraw bottom section + y1 = v.h - deltaY; + h = deltaY; + } + + // Copy the valid part of the viewport to the shifted location + saveStyle = c_ctx.fillStyle; + c_ctx.fillStyle = "rgb(255,255,255)"; + if (deltaX !== 0) { + //that.copyImage(0, 0, -deltaX, 0, v.w, v.h); + //that.fillRect(x1, 0, w, v.h, [255,255,255]); + c_ctx.drawImage(c, 0, 0, v.w, v.h, -deltaX, 0, v.w, v.h); + c_ctx.fillRect(x1, 0, w, v.h); + } + if (deltaY !== 0) { + //that.copyImage(0, 0, 0, -deltaY, v.w, v.h); + //that.fillRect(0, y1, v.w, h, [255,255,255]); + c_ctx.drawImage(c, 0, 0, v.w, v.h, 0, -deltaY, v.w, v.h); + c_ctx.fillRect(0, y1, v.w, h); + } + c_ctx.fillStyle = saveStyle; +}; + + +// Return a map of clean and dirty areas of the viewport and reset the +// tracking of clean and dirty areas. +// +// Returns: {'cleanBox': {'x': x, 'y': y, 'w': w, 'h': h}, +// 'dirtyBoxes': [{'x': x, 'y': y, 'w': w, 'h': h}, ...]} +that.getCleanDirtyReset = function() { + var v = viewport, c = cleanRect, cleanBox, dirtyBoxes = [], + vx2 = v.x + v.w - 1, vy2 = v.y + v.h - 1; + + + // Copy the cleanRect + cleanBox = {'x': c.x1, 'y': c.y1, + 'w': c.x2 - c.x1 + 1, 'h': c.y2 - c.y1 + 1}; + + if ((c.x1 >= c.x2) || (c.y1 >= c.y2)) { + // Whole viewport is dirty + dirtyBoxes.push({'x': v.x, 'y': v.y, 'w': v.w, 'h': v.h}); + } else { + // Redraw dirty regions + if (v.x < c.x1) { + // left side dirty region + dirtyBoxes.push({'x': v.x, 'y': v.y, + 'w': c.x1 - v.x + 1, 'h': v.h}); + } + if (vx2 > c.x2) { + // right side dirty region + dirtyBoxes.push({'x': c.x2 + 1, 'y': v.y, + 'w': vx2 - c.x2, 'h': v.h}); + } + if (v.y < c.y1) { + // top/middle dirty region + dirtyBoxes.push({'x': c.x1, 'y': v.y, + 'w': c.x2 - c.x1 + 1, 'h': c.y1 - v.y}); + } + if (vy2 > c.y2) { + // bottom/middle dirty region + dirtyBoxes.push({'x': c.x1, 'y': c.y2 + 1, + 'w': c.x2 - c.x1 + 1, 'h': vy2 - c.y2}); + } + } + + // Reset the cleanRect to the whole viewport + cleanRect = {'x1': v.x, 'y1': v.y, + 'x2': v.x + v.w - 1, 'y2': v.y + v.h - 1}; + + return {'cleanBox': cleanBox, 'dirtyBoxes': dirtyBoxes}; +}; + +// Translate viewport coordinates to absolute coordinates +that.absX = function(x) { + return x + viewport.x; +}; +that.absY = function(y) { + return y + viewport.y; +}; + + +that.resize = function(width, height) { + c_prevStyle = ""; + + fb_width = width; + fb_height = height; + + rescale(conf.scale); + that.viewportChange(); +}; + +that.clear = function() { + + if (conf.logo) { + that.resize(conf.logo.width, conf.logo.height); + that.blitStringImage(conf.logo.data, 0, 0); + } else { + that.resize(640, 20); + c_ctx.clearRect(0, 0, viewport.w, viewport.h); + } + + renderQ = []; + + // No benefit over default ("source-over") in Chrome and firefox + //c_ctx.globalCompositeOperation = "copy"; +}; + +that.fillRect = function(x, y, width, height, color) { + setFillColor(color); + c_ctx.fillRect(x - viewport.x, y - viewport.y, width, height); +}; + +that.copyImage = function(old_x, old_y, new_x, new_y, w, h) { + var x1 = old_x - viewport.x, y1 = old_y - viewport.y, + x2 = new_x - viewport.x, y2 = new_y - viewport.y; + c_ctx.drawImage(conf.target, x1, y1, w, h, x2, y2, w, h); +}; + + +// Start updating a tile +that.startTile = function(x, y, width, height, color) { + var data, bgr, red, green, blue, i; + tile_x = x; + tile_y = y; + if ((width === 16) && (height === 16)) { + tile = tile16x16; + } else { + tile = c_ctx.createImageData(width, height); + } + data = tile.data; + if (conf.prefer_js) { + if (conf.true_color) { + bgr = color; + } else { + bgr = conf.colourMap[color[0]]; + } + red = bgr[2]; + green = bgr[1]; + blue = bgr[0]; + for (i = 0; i < (width * height * 4); i+=4) { + data[i ] = red; + data[i + 1] = green; + data[i + 2] = blue; + data[i + 3] = 255; + } + } else { + that.fillRect(x, y, width, height, color); + } +}; + +// Update sub-rectangle of the current tile +that.subTile = function(x, y, w, h, color) { + var data, p, bgr, red, green, blue, width, j, i, xend, yend; + if (conf.prefer_js) { + data = tile.data; + width = tile.width; + if (conf.true_color) { + bgr = color; + } else { + bgr = conf.colourMap[color[0]]; + } + red = bgr[2]; + green = bgr[1]; + blue = bgr[0]; + xend = x + w; + yend = y + h; + for (j = y; j < yend; j += 1) { + for (i = x; i < xend; i += 1) { + p = (i + (j * width) ) * 4; + data[p ] = red; + data[p + 1] = green; + data[p + 2] = blue; + data[p + 3] = 255; + } + } + } else { + that.fillRect(tile_x + x, tile_y + y, w, h, color); + } +}; + +// Draw the current tile to the screen +that.finishTile = function() { + if (conf.prefer_js) { + c_ctx.putImageData(tile, tile_x - viewport.x, tile_y - viewport.y); + } + // else: No-op, if not prefer_js then already done by setSubTile +}; + +rgbImageData = function(x, y, vx, vy, width, height, arr, offset) { + var img, i, j, data; + /* + if ((x - v.x >= v.w) || (y - v.y >= v.h) || + (x - v.x + width < 0) || (y - v.y + height < 0)) { + // Skipping because outside of viewport + return; + } + */ + img = c_ctx.createImageData(width, height); + data = img.data; + for (i=0, j=offset; i < (width * height * 4); i=i+4, j=j+3) { + data[i ] = arr[j ]; + data[i + 1] = arr[j + 1]; + data[i + 2] = arr[j + 2]; + data[i + 3] = 255; // Set Alpha + } + c_ctx.putImageData(img, x - vx, y - vy); +}; + +bgrxImageData = function(x, y, vx, vy, width, height, arr, offset) { + var img, i, j, data; + /* + if ((x - v.x >= v.w) || (y - v.y >= v.h) || + (x - v.x + width < 0) || (y - v.y + height < 0)) { + // Skipping because outside of viewport + return; + } + */ + img = c_ctx.createImageData(width, height); + data = img.data; + for (i=0, j=offset; i < (width * height * 4); i=i+4, j=j+4) { + data[i ] = arr[j + 2]; + data[i + 1] = arr[j + 1]; + data[i + 2] = arr[j ]; + data[i + 3] = 255; // Set Alpha + } + c_ctx.putImageData(img, x - vx, y - vy); +}; + +cmapImageData = function(x, y, vx, vy, width, height, arr, offset) { + var img, i, j, data, bgr, cmap; + img = c_ctx.createImageData(width, height); + data = img.data; + cmap = conf.colourMap; + for (i=0, j=offset; i < (width * height * 4); i+=4, j+=1) { + bgr = cmap[arr[j]]; + data[i ] = bgr[2]; + data[i + 1] = bgr[1]; + data[i + 2] = bgr[0]; + data[i + 3] = 255; // Set Alpha + } + c_ctx.putImageData(img, x - vx, y - vy); +}; + +that.blitImage = function(x, y, width, height, arr, offset) { + if (conf.true_color) { + bgrxImageData(x, y, viewport.x, viewport.y, width, height, arr, offset); + } else { + cmapImageData(x, y, viewport.x, viewport.y, width, height, arr, offset); + } +}; + +that.blitRgbImage = function(x, y, width, height, arr, offset) { + if (conf.true_color) { + rgbImageData(x, y, viewport.x, viewport.y, width, height, arr, offset); + } else { + // prolly wrong... + cmapImageData(x, y, viewport.x, viewport.y, width, height, arr, offset); + } +}; + +that.blitStringImage = function(str, x, y) { + var img = new Image(); + img.onload = function () { + c_ctx.drawImage(img, x - viewport.x, y - viewport.y); + }; + img.src = str; +}; + +// Wrap ctx.drawImage but relative to viewport +that.drawImage = function(img, x, y) { + c_ctx.drawImage(img, x - viewport.x, y - viewport.y); +}; + +that.renderQ_push = function(action) { + renderQ.push(action); + if (renderQ.length === 1) { + // If this can be rendered immediately it will be, otherwise + // the scanner will start polling the queue (every + // requestAnimationFrame interval) + scan_renderQ(); + } +}; + +scan_renderQ = function() { + var a, ready = true; + while (ready && renderQ.length > 0) { + a = renderQ[0]; + switch (a.type) { + case 'copy': + that.copyImage(a.old_x, a.old_y, a.x, a.y, a.width, a.height); + break; + case 'fill': + that.fillRect(a.x, a.y, a.width, a.height, a.color); + break; + case 'blit': + that.blitImage(a.x, a.y, a.width, a.height, a.data, 0); + break; + case 'blitRgb': + that.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0); + break; + case 'img': + if (a.img.complete) { + that.drawImage(a.img, a.x, a.y); + } else { + // We need to wait for this image to 'load' + // to keep things in-order + ready = false; + } + break; + } + if (ready) { + a = renderQ.shift(); + } + } + if (renderQ.length > 0) { + requestAnimFrame(scan_renderQ); + } +}; + + +that.changeCursor = function(pixels, mask, hotx, hoty, w, h) { + if (conf.cursor_uri === false) { + Util.Warn("changeCursor called but no cursor data URI support"); + return; + } + + if (conf.true_color) { + changeCursor(conf.target, pixels, mask, hotx, hoty, w, h); + } else { + changeCursor(conf.target, pixels, mask, hotx, hoty, w, h, conf.colourMap); + } +}; + +that.defaultCursor = function() { + conf.target.style.cursor = "default"; +}; + +return constructor(); // Return the public API interface + +} // End of Display() + + +/* Set CSS cursor property using data URI encoded cursor file */ +function changeCursor(target, pixels, mask, hotx, hoty, w0, h0, cmap) { + "use strict"; + var cur = [], rgb, IHDRsz, RGBsz, ANDsz, XORsz, url, idx, alpha, x, y; + //Util.Debug(">> changeCursor, x: " + hotx + ", y: " + hoty + ", w0: " + w0 + ", h0: " + h0); + + var w = w0; + var h = h0; + if (h < w) + h = w; // increase h to make it square + else + w = h; // increace w to make it square + + // Push multi-byte little-endian values + cur.push16le = function (num) { + this.push((num ) & 0xFF, + (num >> 8) & 0xFF ); + }; + cur.push32le = function (num) { + this.push((num ) & 0xFF, + (num >> 8) & 0xFF, + (num >> 16) & 0xFF, + (num >> 24) & 0xFF ); + }; + + IHDRsz = 40; + RGBsz = w * h * 4; + XORsz = Math.ceil( (w * h) / 8.0 ); + ANDsz = Math.ceil( (w * h) / 8.0 ); + + // Main header + cur.push16le(0); // 0: Reserved + cur.push16le(2); // 2: .CUR type + cur.push16le(1); // 4: Number of images, 1 for non-animated ico + + // Cursor #1 header (ICONDIRENTRY) + cur.push(w); // 6: width + cur.push(h); // 7: height + cur.push(0); // 8: colors, 0 -> true-color + cur.push(0); // 9: reserved + cur.push16le(hotx); // 10: hotspot x coordinate + cur.push16le(hoty); // 12: hotspot y coordinate + cur.push32le(IHDRsz + RGBsz + XORsz + ANDsz); + // 14: cursor data byte size + cur.push32le(22); // 18: offset of cursor data in the file + + + // Cursor #1 InfoHeader (ICONIMAGE/BITMAPINFO) + cur.push32le(IHDRsz); // 22: Infoheader size + cur.push32le(w); // 26: Cursor width + cur.push32le(h*2); // 30: XOR+AND height + cur.push16le(1); // 34: number of planes + cur.push16le(32); // 36: bits per pixel + cur.push32le(0); // 38: Type of compression + + cur.push32le(XORsz + ANDsz); // 43: Size of Image + // Gimp leaves this as 0 + + cur.push32le(0); // 46: reserved + cur.push32le(0); // 50: reserved + cur.push32le(0); // 54: reserved + cur.push32le(0); // 58: reserved + + // 62: color data (RGBQUAD icColors[]) + for (y = h-1; y >= 0; y -= 1) { + for (x = 0; x < w; x += 1) { + if (x >= w0 || y >= h0) { + cur.push(0); // blue + cur.push(0); // green + cur.push(0); // red + cur.push(0); // alpha + } else { + idx = y * Math.ceil(w0 / 8) + Math.floor(x/8); + alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0; + if (cmap) { + idx = (w0 * y) + x; + rgb = cmap[pixels[idx]]; + cur.push(rgb[2]); // blue + cur.push(rgb[1]); // green + cur.push(rgb[0]); // red + cur.push(alpha); // alpha + } else { + idx = ((w0 * y) + x) * 4; + cur.push(pixels[idx + 2]); // blue + cur.push(pixels[idx + 1]); // green + cur.push(pixels[idx ]); // red + cur.push(alpha); // alpha + } + } + } + } + + // XOR/bitmask data (BYTE icXOR[]) + // (ignored, just needs to be right size) + for (y = 0; y < h; y += 1) { + for (x = 0; x < Math.ceil(w / 8); x += 1) { + cur.push(0x00); + } + } + + // AND/bitmask data (BYTE icAND[]) + // (ignored, just needs to be right size) + for (y = 0; y < h; y += 1) { + for (x = 0; x < Math.ceil(w / 8); x += 1) { + cur.push(0x00); + } + } + + url = "data:image/x-icon;base64," + Base64.encode(cur); + target.style.cursor = "url(" + url + ") " + hotx + " " + hoty + ", default"; + //Util.Debug("<< changeCursor, cur.length: " + cur.length); +} diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/extra.css b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/extra.css new file mode 100644 index 0000000000000000000000000000000000000000..67b73626ea965ed0ea9eb05d4c7065530f7545af --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/extra.css @@ -0,0 +1,198 @@ + +#noVNC_status { + color: #222; + text-align: left; + line-height: 24px; + height: 24px; + padding: 0; + padding-left: 145px; + position:static; + width: auto; + font-weight: normal; + font-size:12px; +} + +#noVNC_clipboard { + display: none; +} +.noVNC_extra_keys { + float: right; +} +#noVNC_mobile_buttons { + display:block; +} + +#noVNC-control-bar { + position: static; + height: auto; + background: transparent; + +} +#noVNC_mobile_buttons { + display: inline-block; +} + +#noVNC_screen { + border-bottom-right-radius: 0; +} + +.noVNC-buttons-left, +.noVNC-buttons-right{ + padding: 0; + position: static; +} + +.noVNC-buttons-right label { + font-size: 12px; + top: -2px; + position:relative; +} + +.noVNC_status_button { + background: #4F5255; + padding:2px; + opacity: 0.8; + border-radius: 0; + height: 15px; +} +.noVNC_status_button:hover { + opacity: 1; +} +.noVNC_status_button_selected { + background: #F24E53; + border-radius: 0; + padding:2px; + height: 15px; +} + +.myKeyboardInput { + position: absolute; + left: -5000px; +} +.console .actions-bar { + background: #74aec9; + padding: 0 10px; + border-top: 1px solid #c7dfe9; +} +.console .help-text { + background: #74aec9; + font-size: 12px; + color: #222; + line-height:100%; + height: auto; +} +.console .help-text span { + display: block; + border-top: 1px solid #c7dfe9; + padding: 6px 10px 7px 155px; +} + +.console .help-text a { + float: right; + font-weight: bold; + text-decoration: none; + color: #4085a5; + position: relative; + left:-5px; +} +.console .help-text a:hover { + color: #fff; +} +#noVNC_logo { + visibility: hidden; +} + +#console-header { + background: #4085a5; + height: auto; + min-height: 31px; + margin: 0; + padding: 4px 10px 4px 155px; + line-height: 31px; + position: relative; +} +.console .console-header-logo { + display: block; + position: absolute; + width: 130px; + height: 30px; + top: 4px; + left:10px; + position: absolute; + padding: 0; + margin: 0; +} + +.console .console-header-logo a { + text-decoration: none; +} + +.console .console-header-logo img { + max-width: 130px; + max-height: 31px; + position: absolute; + left: 0; + bottom: 0; +} + +.console .console-header-logo span { + font-size: 20px; + color: #fff; + font-family: "Courier New"; + font-weight: bold; +} + +.console .console-info { + position: static; + margin: 0; + display: inline-block; + font-size: 12px; +} +.console .console-info div { + line-height: 130%; +} + +.ipv6-text, .ipv4-text { + font-size: 12px; + +} +#sendEnterButton { + height: 21px; + color: #fff; +} +.keyboardInputInitiator { + margin: 0 0 0 -10px; + height: 18px; + position: relative; + top: -1px; +} +.console-body { + background-image: none; +} +.console input[type="checkbox"] { + vertical-align: bottom; +} +.host-ip span { + white-space: nowrap; +} + +#showKeyboard, +#clipboardButton, +#sendEnterButton, +#noVNC_mouse_button0, +#noVNC_mouse_button1, +#noVNC_mouse_button2, +#noVNC_mouse_button3, +#noVNC_mouse_button4, +#disconnectButton, +#noVNC_ctrl_box, +#noVNC_alt_box, +#ctrl_label, +#alt_label, +#connectButton +{ + position: absolute; + left: -2000px; + top: -2000px; + z-index: -1; +} diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/input.js b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/input.js new file mode 100644 index 0000000000000000000000000000000000000000..8f0c650fc33f2897885e807f4b8b748b86878196 --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/input.js @@ -0,0 +1,1980 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2012 Joel Martin + * Copyright (C) 2013 Samuel Mannehed for Cendio AB + * Licensed under MPL 2.0 or any later version (see LICENSE.txt) + */ + +/*jslint browser: true, white: false, bitwise: false */ +/*global window, Util */ + + +// +// Keyboard event handler +// + +function Keyboard(defaults) { +"use strict"; + +var that = {}, // Public API methods + conf = {}, // Configuration attributes + + keyDownList = []; // List of depressed keys + // (even if they are happy) + +// Configuration attributes +Util.conf_defaults(conf, that, defaults, [ + ['target', 'wo', 'dom', document, 'DOM element that captures keyboard input'], + ['focused', 'rw', 'bool', true, 'Capture and send key events'], + + ['onKeyPress', 'rw', 'func', null, 'Handler for key press/release'] + ]); + + +// +// Private functions +// + +// From the event keyCode return the keysym value for keys that need +// to be suppressed otherwise they may trigger unintended browser +// actions +function getKeysymSpecial(evt) { + var keysym = null; + + switch ( evt.keyCode ) { + // These generate a keyDown and keyPress in Firefox and Opera + case 8 : keysym = 0xFF08; break; // BACKSPACE + case 13 : keysym = 0xFF0D; break; // ENTER + + // This generates a keyDown and keyPress in Opera + case 9 : keysym = 0xFF09; break; // TAB + default : break; + } + + if (evt.type === 'keydown') { + switch ( evt.keyCode ) { + case 27 : keysym = 0xFF1B; break; // ESCAPE + case 46 : keysym = 0xFFFF; break; // DELETE + + case 36 : keysym = 0xFF50; break; // HOME + case 35 : keysym = 0xFF57; break; // END + case 33 : keysym = 0xFF55; break; // PAGE_UP + case 34 : keysym = 0xFF56; break; // PAGE_DOWN + case 45 : keysym = 0xFF63; break; // INSERT + // '-' during keyPress + case 37 : keysym = 0xFF51; break; // LEFT + case 38 : keysym = 0xFF52; break; // UP + case 39 : keysym = 0xFF53; break; // RIGHT + case 40 : keysym = 0xFF54; break; // DOWN + case 16 : keysym = 0xFFE1; break; // SHIFT + case 17 : keysym = 0xFFE3; break; // CONTROL + //case 18 : keysym = 0xFFE7; break; // Left Meta (Mac Option) + case 18 : keysym = 0xFFE9; break; // Left ALT (Mac Command) + + case 112 : keysym = 0xFFBE; break; // F1 + case 113 : keysym = 0xFFBF; break; // F2 + case 114 : keysym = 0xFFC0; break; // F3 + case 115 : keysym = 0xFFC1; break; // F4 + case 116 : keysym = 0xFFC2; break; // F5 + case 117 : keysym = 0xFFC3; break; // F6 + case 118 : keysym = 0xFFC4; break; // F7 + case 119 : keysym = 0xFFC5; break; // F8 + case 120 : keysym = 0xFFC6; break; // F9 + case 121 : keysym = 0xFFC7; break; // F10 + case 122 : keysym = 0xFFC8; break; // F11 + case 123 : keysym = 0xFFC9; break; // F12 + + case 225 : keysym = 0xFE03; break; // AltGr + case 91 : keysym = 0xFFEC; break; // Super_R (Win Key) + case 93 : keysym = 0xFF67; break; // Menu (Win Menu) + + default : break; + } + } + + if ((!keysym) && (evt.ctrlKey || evt.altKey)) { + if ((typeof(evt.which) !== "undefined") && (evt.which > 0)) { + keysym = evt.which; + } else { + // IE9 always + // Firefox and Opera when ctrl/alt + special + Util.Warn("which not set, using keyCode"); + keysym = evt.keyCode; + } + + /* Remap symbols */ + switch (keysym) { + case 186 : keysym = 59; break; // ; (IE) + case 187 : keysym = 61; break; // = (IE) + case 188 : keysym = 44; break; // , (Mozilla, IE) + case 109 : // - (Mozilla, Opera) + if (Util.Engine.gecko || Util.Engine.presto) { + keysym = 45; } + break; + case 173 : // - (Mozilla) + if (Util.Engine.gecko) { + keysym = 45; } + break; + case 189 : keysym = 45; break; // - (IE) + case 190 : keysym = 46; break; // . (Mozilla, IE) + case 191 : keysym = 47; break; // / (Mozilla, IE) + case 192 : keysym = 96; break; // ` (Mozilla, IE) + case 219 : keysym = 91; break; // [ (Mozilla, IE) + case 220 : keysym = 92; break; // \ (Mozilla, IE) + case 221 : keysym = 93; break; // ] (Mozilla, IE) + case 222 : keysym = 39; break; // ' (Mozilla, IE) + } + + /* Remap shifted and unshifted keys */ + if (!!evt.shiftKey) { + switch (keysym) { + case 48 : keysym = 41 ; break; // ) (shifted 0) + case 49 : keysym = 33 ; break; // ! (shifted 1) + case 50 : keysym = 64 ; break; // @ (shifted 2) + case 51 : keysym = 35 ; break; // # (shifted 3) + case 52 : keysym = 36 ; break; // $ (shifted 4) + case 53 : keysym = 37 ; break; // % (shifted 5) + case 54 : keysym = 94 ; break; // ^ (shifted 6) + case 55 : keysym = 38 ; break; // & (shifted 7) + case 56 : keysym = 42 ; break; // * (shifted 8) + case 57 : keysym = 40 ; break; // ( (shifted 9) + + case 59 : keysym = 58 ; break; // : (shifted `) + case 61 : keysym = 43 ; break; // + (shifted ;) + case 44 : keysym = 60 ; break; // < (shifted ,) + case 45 : keysym = 95 ; break; // _ (shifted -) + case 46 : keysym = 62 ; break; // > (shifted .) + case 47 : keysym = 63 ; break; // ? (shifted /) + case 96 : keysym = 126; break; // ~ (shifted `) + case 91 : keysym = 123; break; // { (shifted [) + case 92 : keysym = 124; break; // | (shifted \) + case 93 : keysym = 125; break; // } (shifted ]) + case 39 : keysym = 34 ; break; // " (shifted ') + } + } else if ((keysym >= 65) && (keysym <=90)) { + /* Remap unshifted A-Z */ + keysym += 32; + } else if (evt.keyLocation === 3) { + // numpad keys + switch (keysym) { + case 96 : keysym = 48; break; // 0 + case 97 : keysym = 49; break; // 1 + case 98 : keysym = 50; break; // 2 + case 99 : keysym = 51; break; // 3 + case 100: keysym = 52; break; // 4 + case 101: keysym = 53; break; // 5 + case 102: keysym = 54; break; // 6 + case 103: keysym = 55; break; // 7 + case 104: keysym = 56; break; // 8 + case 105: keysym = 57; break; // 9 + case 109: keysym = 45; break; // - + case 110: keysym = 46; break; // . + case 111: keysym = 47; break; // / + } + } + } + + return keysym; +} + +/* Translate DOM keyPress event to keysym value */ +function getKeysym(evt) { + var keysym, msg; + + if (typeof(evt.which) !== "undefined") { + // WebKit, Firefox, Opera + keysym = evt.which; + } else { + // IE9 + Util.Warn("which not set, using keyCode"); + keysym = evt.keyCode; + } + + if ((keysym > 255) && (keysym < 0xFF00)) { + msg = "Mapping character code " + keysym; + // Map Unicode outside Latin 1 to X11 keysyms + keysym = unicodeTable[keysym]; + if (typeof(keysym) === 'undefined') { + keysym = 0; + } + Util.Debug(msg + " to " + keysym); + } + + return keysym; +} + +function show_keyDownList(kind) { + var c; + var msg = "keyDownList (" + kind + "):\n"; + for (c = 0; c < keyDownList.length; c++) { + msg = msg + " " + c + " - keyCode: " + keyDownList[c].keyCode + + " - which: " + keyDownList[c].which + "\n"; + } + Util.Debug(msg); +} + +function copyKeyEvent(evt) { + var members = ['type', 'keyCode', 'charCode', 'which', + 'altKey', 'ctrlKey', 'shiftKey', + 'keyLocation', 'keyIdentifier'], i, obj = {}; + for (i = 0; i < members.length; i++) { + if (typeof(evt[members[i]]) !== "undefined") { + obj[members[i]] = evt[members[i]]; + } + } + return obj; +} + +function pushKeyEvent(fevt) { + keyDownList.push(fevt); +} + +function getKeyEvent(keyCode, pop) { + var i, fevt = null; + for (i = keyDownList.length-1; i >= 0; i--) { + if (keyDownList[i].keyCode === keyCode) { + if ((typeof(pop) !== "undefined") && (pop)) { + fevt = keyDownList.splice(i, 1)[0]; + } else { + fevt = keyDownList[i]; + } + break; + } + } + return fevt; +} + +function ignoreKeyEvent(evt) { + // Blarg. Some keys have a different keyCode on keyDown vs keyUp + if (evt.keyCode === 229) { + // French AZERTY keyboard dead key. + // Lame thing is that the respective keyUp is 219 so we can't + // properly ignore the keyUp event + return true; + } + return false; +} + + +// +// Key Event Handling: +// +// There are several challenges when dealing with key events: +// - The meaning and use of keyCode, charCode and which depends on +// both the browser and the event type (keyDown/Up vs keyPress). +// - We cannot automatically determine the keyboard layout +// - The keyDown and keyUp events have a keyCode value that has not +// been translated by modifier keys. +// - The keyPress event has a translated (for layout and modifiers) +// character code but the attribute containing it differs. keyCode +// contains the translated value in WebKit (Chrome/Safari), Opera +// 11 and IE9. charCode contains the value in WebKit and Firefox. +// The which attribute contains the value on WebKit, Firefox and +// Opera 11. +// - The keyDown/Up keyCode value indicates (sort of) the physical +// key was pressed but only for standard US layout. On a US +// keyboard, the '-' and '_' characters are on the same key and +// generate a keyCode value of 189. But on an AZERTY keyboard even +// though they are different physical keys they both still +// generate a keyCode of 189! +// - To prevent a key event from propagating to the browser and +// causing unwanted default actions (such as closing a tab, +// opening a menu, shifting focus, etc) we must suppress this +// event in both keyDown and keyPress because not all key strokes +// generate on a keyPress event. Also, in WebKit and IE9 +// suppressing the keyDown prevents a keyPress but other browsers +// still generated a keyPress even if keyDown is suppressed. +// +// For safe key events, we wait until the keyPress event before +// reporting a key down event. For unsafe key events, we report a key +// down event when the keyDown event fires and we suppress any further +// actions (including keyPress). +// +// In order to report a key up event that matches what we reported +// for the key down event, we keep a list of keys that are currently +// down. When the keyDown event happens, we add the key event to the +// list. If it is a safe key event, then we update the which attribute +// in the most recent item on the list when we received a keyPress +// event (keyPress should immediately follow keyDown). When we +// received a keyUp event we search for the event on the list with +// a matching keyCode and we report the character code using the value +// in the 'which' attribute that was stored with that key. +// + +function onKeyDown(e) { + if (! conf.focused) { + return true; + } + var fevt = null, evt = (e ? e : window.event), + keysym = null, suppress = false; + //Util.Debug("onKeyDown kC:" + evt.keyCode + " cC:" + evt.charCode + " w:" + evt.which); + + fevt = copyKeyEvent(evt); + + keysym = getKeysymSpecial(evt); + // Save keysym decoding for use in keyUp + fevt.keysym = keysym; + if (keysym) { + // If it is a key or key combination that might trigger + // browser behaviors or it has no corresponding keyPress + // event, then send it immediately + if (conf.onKeyPress && !ignoreKeyEvent(evt)) { + Util.Debug("onKeyPress down, keysym: " + keysym + + " (onKeyDown key: " + evt.keyCode + + ", which: " + evt.which + ")"); + conf.onKeyPress(keysym, 1, evt); + } + suppress = true; + } + + if (! ignoreKeyEvent(evt)) { + // Add it to the list of depressed keys + pushKeyEvent(fevt); + //show_keyDownList('down'); + } + + if (suppress) { + // Suppress bubbling/default actions + Util.stopEvent(e); + return false; + } else { + // Allow the event to bubble and become a keyPress event which + // will have the character code translated + return true; + } +} + +function onKeyPress(e) { + if (! conf.focused) { + return true; + } + var evt = (e ? e : window.event), + kdlen = keyDownList.length, keysym = null; + //Util.Debug("onKeyPress kC:" + evt.keyCode + " cC:" + evt.charCode + " w:" + evt.which); + + if (((evt.which !== "undefined") && (evt.which === 0)) || + (getKeysymSpecial(evt))) { + // Firefox and Opera generate a keyPress event even if keyDown + // is suppressed. But the keys we want to suppress will have + // either: + // - the which attribute set to 0 + // - getKeysymSpecial() will identify it + Util.Debug("Ignoring special key in keyPress"); + Util.stopEvent(e); + return false; + } + + keysym = getKeysym(evt); + + // Modify the the which attribute in the depressed keys list so + // that the keyUp event will be able to have the character code + // translation available. + if (kdlen > 0) { + keyDownList[kdlen-1].keysym = keysym; + } else { + Util.Warn("keyDownList empty when keyPress triggered"); + } + + //show_keyDownList('press'); + + // Send the translated keysym + if (conf.onKeyPress && (keysym > 0)) { + Util.Debug("onKeyPress down, keysym: " + keysym + + " (onKeyPress key: " + evt.keyCode + + ", which: " + evt.which + ")"); + conf.onKeyPress(keysym, 1, evt); + } + + // Stop keypress events just in case + Util.stopEvent(e); + return false; +} + +function onKeyUp(e) { + if (! conf.focused) { + return true; + } + var fevt = null, evt = (e ? e : window.event), keysym; + //Util.Debug("onKeyUp kC:" + evt.keyCode + " cC:" + evt.charCode + " w:" + evt.which); + + fevt = getKeyEvent(evt.keyCode, true); + + if (fevt) { + keysym = fevt.keysym; + } else { + Util.Warn("Key event (keyCode = " + evt.keyCode + + ") not found on keyDownList"); + keysym = 0; + } + + //show_keyDownList('up'); + + if (conf.onKeyPress && (keysym > 0)) { + //Util.Debug("keyPress up, keysym: " + keysym + + // " (key: " + evt.keyCode + ", which: " + evt.which + ")"); + Util.Debug("onKeyPress up, keysym: " + keysym + + " (onKeyPress key: " + evt.keyCode + + ", which: " + evt.which + ")"); + conf.onKeyPress(keysym, 0, evt); + } + Util.stopEvent(e); + return false; +} + +function allKeysUp() { + Util.Debug(">> Keyboard.allKeysUp"); + if (keyDownList.length > 0) { + Util.Info("Releasing pressed/down keys"); + } + var i, keysym, fevt = null; + for (i = keyDownList.length-1; i >= 0; i--) { + fevt = keyDownList.splice(i, 1)[0]; + keysym = fevt.keysym; + if (conf.onKeyPress && (keysym > 0)) { + Util.Debug("allKeysUp, keysym: " + keysym + + " (keyCode: " + fevt.keyCode + + ", which: " + fevt.which + ")"); + conf.onKeyPress(keysym, 0, fevt); + } + } + Util.Debug("<< Keyboard.allKeysUp"); + return; +} + +// +// Public API interface functions +// + +that.grab = function() { + //Util.Debug(">> Keyboard.grab"); + var c = conf.target; + + Util.addEvent(c, 'keydown', onKeyDown); + Util.addEvent(c, 'keyup', onKeyUp); + Util.addEvent(c, 'keypress', onKeyPress); + + // Release (key up) if window loses focus + Util.addEvent(window, 'blur', allKeysUp); + + //Util.Debug("<< Keyboard.grab"); +}; + +that.ungrab = function() { + //Util.Debug(">> Keyboard.ungrab"); + var c = conf.target; + + Util.removeEvent(c, 'keydown', onKeyDown); + Util.removeEvent(c, 'keyup', onKeyUp); + Util.removeEvent(c, 'keypress', onKeyPress); + Util.removeEvent(window, 'blur', allKeysUp); + + // Release (key up) all keys that are in a down state + allKeysUp(); + + //Util.Debug(">> Keyboard.ungrab"); +}; + +return that; // Return the public API interface + +} // End of Keyboard() + + +// +// Mouse event handler +// + +function Mouse(defaults) { +"use strict"; + +var that = {}, // Public API methods + conf = {}, // Configuration attributes + mouseCaptured = false; + +var doubleClickTimer = null, + lastTouchPos = null; + +// Configuration attributes +Util.conf_defaults(conf, that, defaults, [ + ['target', 'ro', 'dom', document, 'DOM element that captures mouse input'], + ['focused', 'rw', 'bool', true, 'Capture and send mouse clicks/movement'], + ['scale', 'rw', 'float', 1.0, 'Viewport scale factor 0.0 - 1.0'], + + ['onMouseButton', 'rw', 'func', null, 'Handler for mouse button click/release'], + ['onMouseMove', 'rw', 'func', null, 'Handler for mouse movement'], + ['touchButton', 'rw', 'int', 1, 'Button mask (1, 2, 4) for touch devices (0 means ignore clicks)'] + ]); + +function captureMouse() { + // capturing the mouse ensures we get the mouseup event + if (conf.target.setCapture) { + conf.target.setCapture(); + } + + // some browsers give us mouseup events regardless, + // so if we never captured the mouse, we can disregard the event + mouseCaptured = true; +} + +function releaseMouse() { + if (conf.target.releaseCapture) { + conf.target.releaseCapture(); + } + mouseCaptured = false; +} +// +// Private functions +// + +function resetDoubleClickTimer() { + doubleClickTimer = null; +} + +function onMouseButton(e, down) { + var evt, pos, bmask; + if (! conf.focused) { + return true; + } + evt = (e ? e : window.event); + pos = Util.getEventPosition(e, conf.target, conf.scale); + + if (e.touches || e.changedTouches) { + // Touch device + + // When two touches occur within 500 ms of each other and are + // closer than 20 pixels together a double click is triggered. + if (down == 1) { + if (doubleClickTimer == null) { + lastTouchPos = pos; + } else { + clearTimeout(doubleClickTimer); + + // When the distance between the two touches is small enough + // force the position of the latter touch to the position of + // the first. + + var xs = lastTouchPos.x - pos.x; + var ys = lastTouchPos.y - pos.y; + var d = Math.sqrt((xs * xs) + (ys * ys)); + + // The goal is to trigger on a certain physical width, the + // devicePixelRatio brings us a bit closer but is not optimal. + if (d < 20 * window.devicePixelRatio) { + pos = lastTouchPos; + } + } + doubleClickTimer = setTimeout(resetDoubleClickTimer, 500); + } + bmask = conf.touchButton; + // If bmask is set + } else if (evt.which) { + /* everything except IE */ + bmask = 1 << evt.button; + } else { + /* IE including 9 */ + bmask = (evt.button & 0x1) + // Left + (evt.button & 0x2) * 2 + // Right + (evt.button & 0x4) / 2; // Middle + } + //Util.Debug("mouse " + pos.x + "," + pos.y + " down: " + down + + // " bmask: " + bmask + "(evt.button: " + evt.button + ")"); + if (conf.onMouseButton) { + Util.Debug("onMouseButton " + (down ? "down" : "up") + + ", x: " + pos.x + ", y: " + pos.y + ", bmask: " + bmask); + conf.onMouseButton(pos.x, pos.y, down, bmask); + } + Util.stopEvent(e); + return false; +} + +function onMouseDown(e) { + captureMouse(); + onMouseButton(e, 1); +} + +function onMouseUp(e) { + if (!mouseCaptured) { + return; + } + + onMouseButton(e, 0); + releaseMouse(); +} + +function onMouseWheel(e) { + var evt, pos, bmask, wheelData; + if (! conf.focused) { + return true; + } + evt = (e ? e : window.event); + pos = Util.getEventPosition(e, conf.target, conf.scale); + wheelData = evt.detail ? evt.detail * -1 : evt.wheelDelta / 40; + if (wheelData > 0) { + bmask = 1 << 3; + } else { + bmask = 1 << 4; + } + //Util.Debug('mouse scroll by ' + wheelData + ':' + pos.x + "," + pos.y); + if (conf.onMouseButton) { + conf.onMouseButton(pos.x, pos.y, 1, bmask); + conf.onMouseButton(pos.x, pos.y, 0, bmask); + } + Util.stopEvent(e); + return false; +} + +function onMouseMove(e) { + var evt, pos; + if (! conf.focused) { + return true; + } + evt = (e ? e : window.event); + pos = Util.getEventPosition(e, conf.target, conf.scale); + //Util.Debug('mouse ' + evt.which + '/' + evt.button + ' up:' + pos.x + "," + pos.y); + if (conf.onMouseMove) { + conf.onMouseMove(pos.x, pos.y); + } + Util.stopEvent(e); + return false; +} + +function onMouseDisable(e) { + var evt, pos; + if (! conf.focused) { + return true; + } + evt = (e ? e : window.event); + pos = Util.getEventPosition(e, conf.target, conf.scale); + /* Stop propagation if inside canvas area */ + if ((pos.realx >= 0) && (pos.realy >= 0) && + (pos.realx < conf.target.offsetWidth) && + (pos.realy < conf.target.offsetHeight)) { + //Util.Debug("mouse event disabled"); + Util.stopEvent(e); + return false; + } + //Util.Debug("mouse event not disabled"); + return true; +} + +// +// Public API interface functions +// + +that.grab = function() { + //Util.Debug(">> Mouse.grab"); + var c = conf.target; + + if ('ontouchstart' in document.documentElement) { + Util.addEvent(c, 'touchstart', onMouseDown); + Util.addEvent(window, 'touchend', onMouseUp); + Util.addEvent(c, 'touchend', onMouseUp); + Util.addEvent(c, 'touchmove', onMouseMove); + } else { + Util.addEvent(c, 'mousedown', onMouseDown); + Util.addEvent(window, 'mouseup', onMouseUp); + Util.addEvent(c, 'mouseup', onMouseUp); + Util.addEvent(c, 'mousemove', onMouseMove); + Util.addEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel', + onMouseWheel); + } + + /* Work around right and middle click browser behaviors */ + Util.addEvent(document, 'click', onMouseDisable); + Util.addEvent(document.body, 'contextmenu', onMouseDisable); + + //Util.Debug("<< Mouse.grab"); +}; + +that.ungrab = function() { + //Util.Debug(">> Mouse.ungrab"); + var c = conf.target; + + if ('ontouchstart' in document.documentElement) { + Util.removeEvent(c, 'touchstart', onMouseDown); + Util.removeEvent(window, 'touchend', onMouseUp); + Util.removeEvent(c, 'touchend', onMouseUp); + Util.removeEvent(c, 'touchmove', onMouseMove); + } else { + Util.removeEvent(c, 'mousedown', onMouseDown); + Util.removeEvent(window, 'mouseup', onMouseUp); + Util.removeEvent(c, 'mouseup', onMouseUp); + Util.removeEvent(c, 'mousemove', onMouseMove); + Util.removeEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel', + onMouseWheel); + } + + /* Work around right and middle click browser behaviors */ + Util.removeEvent(document, 'click', onMouseDisable); + Util.removeEvent(document.body, 'contextmenu', onMouseDisable); + + //Util.Debug(">> Mouse.ungrab"); +}; + +return that; // Return the public API interface + +} // End of Mouse() + + +/* + * Browser keypress to X11 keysym for Unicode characters > U+00FF + */ +unicodeTable = { + 0x0104 : 0x01a1, + 0x02D8 : 0x01a2, + 0x0141 : 0x01a3, + 0x013D : 0x01a5, + 0x015A : 0x01a6, + 0x0160 : 0x01a9, + 0x015E : 0x01aa, + 0x0164 : 0x01ab, + 0x0179 : 0x01ac, + 0x017D : 0x01ae, + 0x017B : 0x01af, + 0x0105 : 0x01b1, + 0x02DB : 0x01b2, + 0x0142 : 0x01b3, + 0x013E : 0x01b5, + 0x015B : 0x01b6, + 0x02C7 : 0x01b7, + 0x0161 : 0x01b9, + 0x015F : 0x01ba, + 0x0165 : 0x01bb, + 0x017A : 0x01bc, + 0x02DD : 0x01bd, + 0x017E : 0x01be, + 0x017C : 0x01bf, + 0x0154 : 0x01c0, + 0x0102 : 0x01c3, + 0x0139 : 0x01c5, + 0x0106 : 0x01c6, + 0x010C : 0x01c8, + 0x0118 : 0x01ca, + 0x011A : 0x01cc, + 0x010E : 0x01cf, + 0x0110 : 0x01d0, + 0x0143 : 0x01d1, + 0x0147 : 0x01d2, + 0x0150 : 0x01d5, + 0x0158 : 0x01d8, + 0x016E : 0x01d9, + 0x0170 : 0x01db, + 0x0162 : 0x01de, + 0x0155 : 0x01e0, + 0x0103 : 0x01e3, + 0x013A : 0x01e5, + 0x0107 : 0x01e6, + 0x010D : 0x01e8, + 0x0119 : 0x01ea, + 0x011B : 0x01ec, + 0x010F : 0x01ef, + 0x0111 : 0x01f0, + 0x0144 : 0x01f1, + 0x0148 : 0x01f2, + 0x0151 : 0x01f5, + 0x0171 : 0x01fb, + 0x0159 : 0x01f8, + 0x016F : 0x01f9, + 0x0163 : 0x01fe, + 0x02D9 : 0x01ff, + 0x0126 : 0x02a1, + 0x0124 : 0x02a6, + 0x0130 : 0x02a9, + 0x011E : 0x02ab, + 0x0134 : 0x02ac, + 0x0127 : 0x02b1, + 0x0125 : 0x02b6, + 0x0131 : 0x02b9, + 0x011F : 0x02bb, + 0x0135 : 0x02bc, + 0x010A : 0x02c5, + 0x0108 : 0x02c6, + 0x0120 : 0x02d5, + 0x011C : 0x02d8, + 0x016C : 0x02dd, + 0x015C : 0x02de, + 0x010B : 0x02e5, + 0x0109 : 0x02e6, + 0x0121 : 0x02f5, + 0x011D : 0x02f8, + 0x016D : 0x02fd, + 0x015D : 0x02fe, + 0x0138 : 0x03a2, + 0x0156 : 0x03a3, + 0x0128 : 0x03a5, + 0x013B : 0x03a6, + 0x0112 : 0x03aa, + 0x0122 : 0x03ab, + 0x0166 : 0x03ac, + 0x0157 : 0x03b3, + 0x0129 : 0x03b5, + 0x013C : 0x03b6, + 0x0113 : 0x03ba, + 0x0123 : 0x03bb, + 0x0167 : 0x03bc, + 0x014A : 0x03bd, + 0x014B : 0x03bf, + 0x0100 : 0x03c0, + 0x012E : 0x03c7, + 0x0116 : 0x03cc, + 0x012A : 0x03cf, + 0x0145 : 0x03d1, + 0x014C : 0x03d2, + 0x0136 : 0x03d3, + 0x0172 : 0x03d9, + 0x0168 : 0x03dd, + 0x016A : 0x03de, + 0x0101 : 0x03e0, + 0x012F : 0x03e7, + 0x0117 : 0x03ec, + 0x012B : 0x03ef, + 0x0146 : 0x03f1, + 0x014D : 0x03f2, + 0x0137 : 0x03f3, + 0x0173 : 0x03f9, + 0x0169 : 0x03fd, + 0x016B : 0x03fe, + 0x1E02 : 0x1001e02, + 0x1E03 : 0x1001e03, + 0x1E0A : 0x1001e0a, + 0x1E80 : 0x1001e80, + 0x1E82 : 0x1001e82, + 0x1E0B : 0x1001e0b, + 0x1EF2 : 0x1001ef2, + 0x1E1E : 0x1001e1e, + 0x1E1F : 0x1001e1f, + 0x1E40 : 0x1001e40, + 0x1E41 : 0x1001e41, + 0x1E56 : 0x1001e56, + 0x1E81 : 0x1001e81, + 0x1E57 : 0x1001e57, + 0x1E83 : 0x1001e83, + 0x1E60 : 0x1001e60, + 0x1EF3 : 0x1001ef3, + 0x1E84 : 0x1001e84, + 0x1E85 : 0x1001e85, + 0x1E61 : 0x1001e61, + 0x0174 : 0x1000174, + 0x1E6A : 0x1001e6a, + 0x0176 : 0x1000176, + 0x0175 : 0x1000175, + 0x1E6B : 0x1001e6b, + 0x0177 : 0x1000177, + 0x0152 : 0x13bc, + 0x0153 : 0x13bd, + 0x0178 : 0x13be, + 0x203E : 0x047e, + 0x3002 : 0x04a1, + 0x300C : 0x04a2, + 0x300D : 0x04a3, + 0x3001 : 0x04a4, + 0x30FB : 0x04a5, + 0x30F2 : 0x04a6, + 0x30A1 : 0x04a7, + 0x30A3 : 0x04a8, + 0x30A5 : 0x04a9, + 0x30A7 : 0x04aa, + 0x30A9 : 0x04ab, + 0x30E3 : 0x04ac, + 0x30E5 : 0x04ad, + 0x30E7 : 0x04ae, + 0x30C3 : 0x04af, + 0x30FC : 0x04b0, + 0x30A2 : 0x04b1, + 0x30A4 : 0x04b2, + 0x30A6 : 0x04b3, + 0x30A8 : 0x04b4, + 0x30AA : 0x04b5, + 0x30AB : 0x04b6, + 0x30AD : 0x04b7, + 0x30AF : 0x04b8, + 0x30B1 : 0x04b9, + 0x30B3 : 0x04ba, + 0x30B5 : 0x04bb, + 0x30B7 : 0x04bc, + 0x30B9 : 0x04bd, + 0x30BB : 0x04be, + 0x30BD : 0x04bf, + 0x30BF : 0x04c0, + 0x30C1 : 0x04c1, + 0x30C4 : 0x04c2, + 0x30C6 : 0x04c3, + 0x30C8 : 0x04c4, + 0x30CA : 0x04c5, + 0x30CB : 0x04c6, + 0x30CC : 0x04c7, + 0x30CD : 0x04c8, + 0x30CE : 0x04c9, + 0x30CF : 0x04ca, + 0x30D2 : 0x04cb, + 0x30D5 : 0x04cc, + 0x30D8 : 0x04cd, + 0x30DB : 0x04ce, + 0x30DE : 0x04cf, + 0x30DF : 0x04d0, + 0x30E0 : 0x04d1, + 0x30E1 : 0x04d2, + 0x30E2 : 0x04d3, + 0x30E4 : 0x04d4, + 0x30E6 : 0x04d5, + 0x30E8 : 0x04d6, + 0x30E9 : 0x04d7, + 0x30EA : 0x04d8, + 0x30EB : 0x04d9, + 0x30EC : 0x04da, + 0x30ED : 0x04db, + 0x30EF : 0x04dc, + 0x30F3 : 0x04dd, + 0x309B : 0x04de, + 0x309C : 0x04df, + 0x06F0 : 0x10006f0, + 0x06F1 : 0x10006f1, + 0x06F2 : 0x10006f2, + 0x06F3 : 0x10006f3, + 0x06F4 : 0x10006f4, + 0x06F5 : 0x10006f5, + 0x06F6 : 0x10006f6, + 0x06F7 : 0x10006f7, + 0x06F8 : 0x10006f8, + 0x06F9 : 0x10006f9, + 0x066A : 0x100066a, + 0x0670 : 0x1000670, + 0x0679 : 0x1000679, + 0x067E : 0x100067e, + 0x0686 : 0x1000686, + 0x0688 : 0x1000688, + 0x0691 : 0x1000691, + 0x060C : 0x05ac, + 0x06D4 : 0x10006d4, + 0x0660 : 0x1000660, + 0x0661 : 0x1000661, + 0x0662 : 0x1000662, + 0x0663 : 0x1000663, + 0x0664 : 0x1000664, + 0x0665 : 0x1000665, + 0x0666 : 0x1000666, + 0x0667 : 0x1000667, + 0x0668 : 0x1000668, + 0x0669 : 0x1000669, + 0x061B : 0x05bb, + 0x061F : 0x05bf, + 0x0621 : 0x05c1, + 0x0622 : 0x05c2, + 0x0623 : 0x05c3, + 0x0624 : 0x05c4, + 0x0625 : 0x05c5, + 0x0626 : 0x05c6, + 0x0627 : 0x05c7, + 0x0628 : 0x05c8, + 0x0629 : 0x05c9, + 0x062A : 0x05ca, + 0x062B : 0x05cb, + 0x062C : 0x05cc, + 0x062D : 0x05cd, + 0x062E : 0x05ce, + 0x062F : 0x05cf, + 0x0630 : 0x05d0, + 0x0631 : 0x05d1, + 0x0632 : 0x05d2, + 0x0633 : 0x05d3, + 0x0634 : 0x05d4, + 0x0635 : 0x05d5, + 0x0636 : 0x05d6, + 0x0637 : 0x05d7, + 0x0638 : 0x05d8, + 0x0639 : 0x05d9, + 0x063A : 0x05da, + 0x0640 : 0x05e0, + 0x0641 : 0x05e1, + 0x0642 : 0x05e2, + 0x0643 : 0x05e3, + 0x0644 : 0x05e4, + 0x0645 : 0x05e5, + 0x0646 : 0x05e6, + 0x0647 : 0x05e7, + 0x0648 : 0x05e8, + 0x0649 : 0x05e9, + 0x064A : 0x05ea, + 0x064B : 0x05eb, + 0x064C : 0x05ec, + 0x064D : 0x05ed, + 0x064E : 0x05ee, + 0x064F : 0x05ef, + 0x0650 : 0x05f0, + 0x0651 : 0x05f1, + 0x0652 : 0x05f2, + 0x0653 : 0x1000653, + 0x0654 : 0x1000654, + 0x0655 : 0x1000655, + 0x0698 : 0x1000698, + 0x06A4 : 0x10006a4, + 0x06A9 : 0x10006a9, + 0x06AF : 0x10006af, + 0x06BA : 0x10006ba, + 0x06BE : 0x10006be, + 0x06CC : 0x10006cc, + 0x06D2 : 0x10006d2, + 0x06C1 : 0x10006c1, + 0x0492 : 0x1000492, + 0x0493 : 0x1000493, + 0x0496 : 0x1000496, + 0x0497 : 0x1000497, + 0x049A : 0x100049a, + 0x049B : 0x100049b, + 0x049C : 0x100049c, + 0x049D : 0x100049d, + 0x04A2 : 0x10004a2, + 0x04A3 : 0x10004a3, + 0x04AE : 0x10004ae, + 0x04AF : 0x10004af, + 0x04B0 : 0x10004b0, + 0x04B1 : 0x10004b1, + 0x04B2 : 0x10004b2, + 0x04B3 : 0x10004b3, + 0x04B6 : 0x10004b6, + 0x04B7 : 0x10004b7, + 0x04B8 : 0x10004b8, + 0x04B9 : 0x10004b9, + 0x04BA : 0x10004ba, + 0x04BB : 0x10004bb, + 0x04D8 : 0x10004d8, + 0x04D9 : 0x10004d9, + 0x04E2 : 0x10004e2, + 0x04E3 : 0x10004e3, + 0x04E8 : 0x10004e8, + 0x04E9 : 0x10004e9, + 0x04EE : 0x10004ee, + 0x04EF : 0x10004ef, + 0x0452 : 0x06a1, + 0x0453 : 0x06a2, + 0x0451 : 0x06a3, + 0x0454 : 0x06a4, + 0x0455 : 0x06a5, + 0x0456 : 0x06a6, + 0x0457 : 0x06a7, + 0x0458 : 0x06a8, + 0x0459 : 0x06a9, + 0x045A : 0x06aa, + 0x045B : 0x06ab, + 0x045C : 0x06ac, + 0x0491 : 0x06ad, + 0x045E : 0x06ae, + 0x045F : 0x06af, + 0x2116 : 0x06b0, + 0x0402 : 0x06b1, + 0x0403 : 0x06b2, + 0x0401 : 0x06b3, + 0x0404 : 0x06b4, + 0x0405 : 0x06b5, + 0x0406 : 0x06b6, + 0x0407 : 0x06b7, + 0x0408 : 0x06b8, + 0x0409 : 0x06b9, + 0x040A : 0x06ba, + 0x040B : 0x06bb, + 0x040C : 0x06bc, + 0x0490 : 0x06bd, + 0x040E : 0x06be, + 0x040F : 0x06bf, + 0x044E : 0x06c0, + 0x0430 : 0x06c1, + 0x0431 : 0x06c2, + 0x0446 : 0x06c3, + 0x0434 : 0x06c4, + 0x0435 : 0x06c5, + 0x0444 : 0x06c6, + 0x0433 : 0x06c7, + 0x0445 : 0x06c8, + 0x0438 : 0x06c9, + 0x0439 : 0x06ca, + 0x043A : 0x06cb, + 0x043B : 0x06cc, + 0x043C : 0x06cd, + 0x043D : 0x06ce, + 0x043E : 0x06cf, + 0x043F : 0x06d0, + 0x044F : 0x06d1, + 0x0440 : 0x06d2, + 0x0441 : 0x06d3, + 0x0442 : 0x06d4, + 0x0443 : 0x06d5, + 0x0436 : 0x06d6, + 0x0432 : 0x06d7, + 0x044C : 0x06d8, + 0x044B : 0x06d9, + 0x0437 : 0x06da, + 0x0448 : 0x06db, + 0x044D : 0x06dc, + 0x0449 : 0x06dd, + 0x0447 : 0x06de, + 0x044A : 0x06df, + 0x042E : 0x06e0, + 0x0410 : 0x06e1, + 0x0411 : 0x06e2, + 0x0426 : 0x06e3, + 0x0414 : 0x06e4, + 0x0415 : 0x06e5, + 0x0424 : 0x06e6, + 0x0413 : 0x06e7, + 0x0425 : 0x06e8, + 0x0418 : 0x06e9, + 0x0419 : 0x06ea, + 0x041A : 0x06eb, + 0x041B : 0x06ec, + 0x041C : 0x06ed, + 0x041D : 0x06ee, + 0x041E : 0x06ef, + 0x041F : 0x06f0, + 0x042F : 0x06f1, + 0x0420 : 0x06f2, + 0x0421 : 0x06f3, + 0x0422 : 0x06f4, + 0x0423 : 0x06f5, + 0x0416 : 0x06f6, + 0x0412 : 0x06f7, + 0x042C : 0x06f8, + 0x042B : 0x06f9, + 0x0417 : 0x06fa, + 0x0428 : 0x06fb, + 0x042D : 0x06fc, + 0x0429 : 0x06fd, + 0x0427 : 0x06fe, + 0x042A : 0x06ff, + 0x0386 : 0x07a1, + 0x0388 : 0x07a2, + 0x0389 : 0x07a3, + 0x038A : 0x07a4, + 0x03AA : 0x07a5, + 0x038C : 0x07a7, + 0x038E : 0x07a8, + 0x03AB : 0x07a9, + 0x038F : 0x07ab, + 0x0385 : 0x07ae, + 0x2015 : 0x07af, + 0x03AC : 0x07b1, + 0x03AD : 0x07b2, + 0x03AE : 0x07b3, + 0x03AF : 0x07b4, + 0x03CA : 0x07b5, + 0x0390 : 0x07b6, + 0x03CC : 0x07b7, + 0x03CD : 0x07b8, + 0x03CB : 0x07b9, + 0x03B0 : 0x07ba, + 0x03CE : 0x07bb, + 0x0391 : 0x07c1, + 0x0392 : 0x07c2, + 0x0393 : 0x07c3, + 0x0394 : 0x07c4, + 0x0395 : 0x07c5, + 0x0396 : 0x07c6, + 0x0397 : 0x07c7, + 0x0398 : 0x07c8, + 0x0399 : 0x07c9, + 0x039A : 0x07ca, + 0x039B : 0x07cb, + 0x039C : 0x07cc, + 0x039D : 0x07cd, + 0x039E : 0x07ce, + 0x039F : 0x07cf, + 0x03A0 : 0x07d0, + 0x03A1 : 0x07d1, + 0x03A3 : 0x07d2, + 0x03A4 : 0x07d4, + 0x03A5 : 0x07d5, + 0x03A6 : 0x07d6, + 0x03A7 : 0x07d7, + 0x03A8 : 0x07d8, + 0x03A9 : 0x07d9, + 0x03B1 : 0x07e1, + 0x03B2 : 0x07e2, + 0x03B3 : 0x07e3, + 0x03B4 : 0x07e4, + 0x03B5 : 0x07e5, + 0x03B6 : 0x07e6, + 0x03B7 : 0x07e7, + 0x03B8 : 0x07e8, + 0x03B9 : 0x07e9, + 0x03BA : 0x07ea, + 0x03BB : 0x07eb, + 0x03BC : 0x07ec, + 0x03BD : 0x07ed, + 0x03BE : 0x07ee, + 0x03BF : 0x07ef, + 0x03C0 : 0x07f0, + 0x03C1 : 0x07f1, + 0x03C3 : 0x07f2, + 0x03C2 : 0x07f3, + 0x03C4 : 0x07f4, + 0x03C5 : 0x07f5, + 0x03C6 : 0x07f6, + 0x03C7 : 0x07f7, + 0x03C8 : 0x07f8, + 0x03C9 : 0x07f9, + 0x23B7 : 0x08a1, + 0x2320 : 0x08a4, + 0x2321 : 0x08a5, + 0x23A1 : 0x08a7, + 0x23A3 : 0x08a8, + 0x23A4 : 0x08a9, + 0x23A6 : 0x08aa, + 0x239B : 0x08ab, + 0x239D : 0x08ac, + 0x239E : 0x08ad, + 0x23A0 : 0x08ae, + 0x23A8 : 0x08af, + 0x23AC : 0x08b0, + 0x2264 : 0x08bc, + 0x2260 : 0x08bd, + 0x2265 : 0x08be, + 0x222B : 0x08bf, + 0x2234 : 0x08c0, + 0x221D : 0x08c1, + 0x221E : 0x08c2, + 0x2207 : 0x08c5, + 0x223C : 0x08c8, + 0x2243 : 0x08c9, + 0x21D4 : 0x08cd, + 0x21D2 : 0x08ce, + 0x2261 : 0x08cf, + //0x221A : 0x08d6, + 0x2282 : 0x08da, + 0x2283 : 0x08db, + 0x2229 : 0x08dc, + 0x222A : 0x08dd, + 0x2227 : 0x08de, + 0x2228 : 0x08df, + //0x2202 : 0x08ef, + 0x0192 : 0x08f6, + 0x2190 : 0x08fb, + 0x2191 : 0x08fc, + 0x2192 : 0x08fd, + 0x2193 : 0x08fe, + 0x25C6 : 0x09e0, + 0x2592 : 0x09e1, + 0x2409 : 0x09e2, + 0x240C : 0x09e3, + 0x240D : 0x09e4, + 0x240A : 0x09e5, + 0x2424 : 0x09e8, + 0x240B : 0x09e9, + 0x2518 : 0x09ea, + 0x2510 : 0x09eb, + 0x250C : 0x09ec, + 0x2514 : 0x09ed, + 0x253C : 0x09ee, + 0x23BA : 0x09ef, + 0x23BB : 0x09f0, + 0x2500 : 0x09f1, + 0x23BC : 0x09f2, + 0x23BD : 0x09f3, + 0x251C : 0x09f4, + 0x2524 : 0x09f5, + 0x2534 : 0x09f6, + 0x252C : 0x09f7, + 0x2502 : 0x09f8, + 0x2003 : 0x0aa1, + 0x2002 : 0x0aa2, + 0x2004 : 0x0aa3, + 0x2005 : 0x0aa4, + 0x2007 : 0x0aa5, + 0x2008 : 0x0aa6, + 0x2009 : 0x0aa7, + 0x200A : 0x0aa8, + 0x2014 : 0x0aa9, + 0x2013 : 0x0aaa, + 0x2026 : 0x0aae, + 0x2025 : 0x0aaf, + 0x2153 : 0x0ab0, + 0x2154 : 0x0ab1, + 0x2155 : 0x0ab2, + 0x2156 : 0x0ab3, + 0x2157 : 0x0ab4, + 0x2158 : 0x0ab5, + 0x2159 : 0x0ab6, + 0x215A : 0x0ab7, + 0x2105 : 0x0ab8, + 0x2012 : 0x0abb, + 0x215B : 0x0ac3, + 0x215C : 0x0ac4, + 0x215D : 0x0ac5, + 0x215E : 0x0ac6, + 0x2122 : 0x0ac9, + 0x2018 : 0x0ad0, + 0x2019 : 0x0ad1, + 0x201C : 0x0ad2, + 0x201D : 0x0ad3, + 0x211E : 0x0ad4, + 0x2032 : 0x0ad6, + 0x2033 : 0x0ad7, + 0x271D : 0x0ad9, + 0x2663 : 0x0aec, + 0x2666 : 0x0aed, + 0x2665 : 0x0aee, + 0x2720 : 0x0af0, + 0x2020 : 0x0af1, + 0x2021 : 0x0af2, + 0x2713 : 0x0af3, + 0x2717 : 0x0af4, + 0x266F : 0x0af5, + 0x266D : 0x0af6, + 0x2642 : 0x0af7, + 0x2640 : 0x0af8, + 0x260E : 0x0af9, + 0x2315 : 0x0afa, + 0x2117 : 0x0afb, + 0x2038 : 0x0afc, + 0x201A : 0x0afd, + 0x201E : 0x0afe, + 0x22A4 : 0x0bc2, + 0x230A : 0x0bc4, + 0x2218 : 0x0bca, + 0x2395 : 0x0bcc, + 0x22A5 : 0x0bce, + 0x25CB : 0x0bcf, + 0x2308 : 0x0bd3, + 0x22A3 : 0x0bdc, + 0x22A2 : 0x0bfc, + 0x2017 : 0x0cdf, + 0x05D0 : 0x0ce0, + 0x05D1 : 0x0ce1, + 0x05D2 : 0x0ce2, + 0x05D3 : 0x0ce3, + 0x05D4 : 0x0ce4, + 0x05D5 : 0x0ce5, + 0x05D6 : 0x0ce6, + 0x05D7 : 0x0ce7, + 0x05D8 : 0x0ce8, + 0x05D9 : 0x0ce9, + 0x05DA : 0x0cea, + 0x05DB : 0x0ceb, + 0x05DC : 0x0cec, + 0x05DD : 0x0ced, + 0x05DE : 0x0cee, + 0x05DF : 0x0cef, + 0x05E0 : 0x0cf0, + 0x05E1 : 0x0cf1, + 0x05E2 : 0x0cf2, + 0x05E3 : 0x0cf3, + 0x05E4 : 0x0cf4, + 0x05E5 : 0x0cf5, + 0x05E6 : 0x0cf6, + 0x05E7 : 0x0cf7, + 0x05E8 : 0x0cf8, + 0x05E9 : 0x0cf9, + 0x05EA : 0x0cfa, + 0x0E01 : 0x0da1, + 0x0E02 : 0x0da2, + 0x0E03 : 0x0da3, + 0x0E04 : 0x0da4, + 0x0E05 : 0x0da5, + 0x0E06 : 0x0da6, + 0x0E07 : 0x0da7, + 0x0E08 : 0x0da8, + 0x0E09 : 0x0da9, + 0x0E0A : 0x0daa, + 0x0E0B : 0x0dab, + 0x0E0C : 0x0dac, + 0x0E0D : 0x0dad, + 0x0E0E : 0x0dae, + 0x0E0F : 0x0daf, + 0x0E10 : 0x0db0, + 0x0E11 : 0x0db1, + 0x0E12 : 0x0db2, + 0x0E13 : 0x0db3, + 0x0E14 : 0x0db4, + 0x0E15 : 0x0db5, + 0x0E16 : 0x0db6, + 0x0E17 : 0x0db7, + 0x0E18 : 0x0db8, + 0x0E19 : 0x0db9, + 0x0E1A : 0x0dba, + 0x0E1B : 0x0dbb, + 0x0E1C : 0x0dbc, + 0x0E1D : 0x0dbd, + 0x0E1E : 0x0dbe, + 0x0E1F : 0x0dbf, + 0x0E20 : 0x0dc0, + 0x0E21 : 0x0dc1, + 0x0E22 : 0x0dc2, + 0x0E23 : 0x0dc3, + 0x0E24 : 0x0dc4, + 0x0E25 : 0x0dc5, + 0x0E26 : 0x0dc6, + 0x0E27 : 0x0dc7, + 0x0E28 : 0x0dc8, + 0x0E29 : 0x0dc9, + 0x0E2A : 0x0dca, + 0x0E2B : 0x0dcb, + 0x0E2C : 0x0dcc, + 0x0E2D : 0x0dcd, + 0x0E2E : 0x0dce, + 0x0E2F : 0x0dcf, + 0x0E30 : 0x0dd0, + 0x0E31 : 0x0dd1, + 0x0E32 : 0x0dd2, + 0x0E33 : 0x0dd3, + 0x0E34 : 0x0dd4, + 0x0E35 : 0x0dd5, + 0x0E36 : 0x0dd6, + 0x0E37 : 0x0dd7, + 0x0E38 : 0x0dd8, + 0x0E39 : 0x0dd9, + 0x0E3A : 0x0dda, + 0x0E3F : 0x0ddf, + 0x0E40 : 0x0de0, + 0x0E41 : 0x0de1, + 0x0E42 : 0x0de2, + 0x0E43 : 0x0de3, + 0x0E44 : 0x0de4, + 0x0E45 : 0x0de5, + 0x0E46 : 0x0de6, + 0x0E47 : 0x0de7, + 0x0E48 : 0x0de8, + 0x0E49 : 0x0de9, + 0x0E4A : 0x0dea, + 0x0E4B : 0x0deb, + 0x0E4C : 0x0dec, + 0x0E4D : 0x0ded, + 0x0E50 : 0x0df0, + 0x0E51 : 0x0df1, + 0x0E52 : 0x0df2, + 0x0E53 : 0x0df3, + 0x0E54 : 0x0df4, + 0x0E55 : 0x0df5, + 0x0E56 : 0x0df6, + 0x0E57 : 0x0df7, + 0x0E58 : 0x0df8, + 0x0E59 : 0x0df9, + 0x0587 : 0x1000587, + 0x0589 : 0x1000589, + 0x055D : 0x100055d, + 0x058A : 0x100058a, + 0x055C : 0x100055c, + 0x055B : 0x100055b, + 0x055E : 0x100055e, + 0x0531 : 0x1000531, + 0x0561 : 0x1000561, + 0x0532 : 0x1000532, + 0x0562 : 0x1000562, + 0x0533 : 0x1000533, + 0x0563 : 0x1000563, + 0x0534 : 0x1000534, + 0x0564 : 0x1000564, + 0x0535 : 0x1000535, + 0x0565 : 0x1000565, + 0x0536 : 0x1000536, + 0x0566 : 0x1000566, + 0x0537 : 0x1000537, + 0x0567 : 0x1000567, + 0x0538 : 0x1000538, + 0x0568 : 0x1000568, + 0x0539 : 0x1000539, + 0x0569 : 0x1000569, + 0x053A : 0x100053a, + 0x056A : 0x100056a, + 0x053B : 0x100053b, + 0x056B : 0x100056b, + 0x053C : 0x100053c, + 0x056C : 0x100056c, + 0x053D : 0x100053d, + 0x056D : 0x100056d, + 0x053E : 0x100053e, + 0x056E : 0x100056e, + 0x053F : 0x100053f, + 0x056F : 0x100056f, + 0x0540 : 0x1000540, + 0x0570 : 0x1000570, + 0x0541 : 0x1000541, + 0x0571 : 0x1000571, + 0x0542 : 0x1000542, + 0x0572 : 0x1000572, + 0x0543 : 0x1000543, + 0x0573 : 0x1000573, + 0x0544 : 0x1000544, + 0x0574 : 0x1000574, + 0x0545 : 0x1000545, + 0x0575 : 0x1000575, + 0x0546 : 0x1000546, + 0x0576 : 0x1000576, + 0x0547 : 0x1000547, + 0x0577 : 0x1000577, + 0x0548 : 0x1000548, + 0x0578 : 0x1000578, + 0x0549 : 0x1000549, + 0x0579 : 0x1000579, + 0x054A : 0x100054a, + 0x057A : 0x100057a, + 0x054B : 0x100054b, + 0x057B : 0x100057b, + 0x054C : 0x100054c, + 0x057C : 0x100057c, + 0x054D : 0x100054d, + 0x057D : 0x100057d, + 0x054E : 0x100054e, + 0x057E : 0x100057e, + 0x054F : 0x100054f, + 0x057F : 0x100057f, + 0x0550 : 0x1000550, + 0x0580 : 0x1000580, + 0x0551 : 0x1000551, + 0x0581 : 0x1000581, + 0x0552 : 0x1000552, + 0x0582 : 0x1000582, + 0x0553 : 0x1000553, + 0x0583 : 0x1000583, + 0x0554 : 0x1000554, + 0x0584 : 0x1000584, + 0x0555 : 0x1000555, + 0x0585 : 0x1000585, + 0x0556 : 0x1000556, + 0x0586 : 0x1000586, + 0x055A : 0x100055a, + 0x10D0 : 0x10010d0, + 0x10D1 : 0x10010d1, + 0x10D2 : 0x10010d2, + 0x10D3 : 0x10010d3, + 0x10D4 : 0x10010d4, + 0x10D5 : 0x10010d5, + 0x10D6 : 0x10010d6, + 0x10D7 : 0x10010d7, + 0x10D8 : 0x10010d8, + 0x10D9 : 0x10010d9, + 0x10DA : 0x10010da, + 0x10DB : 0x10010db, + 0x10DC : 0x10010dc, + 0x10DD : 0x10010dd, + 0x10DE : 0x10010de, + 0x10DF : 0x10010df, + 0x10E0 : 0x10010e0, + 0x10E1 : 0x10010e1, + 0x10E2 : 0x10010e2, + 0x10E3 : 0x10010e3, + 0x10E4 : 0x10010e4, + 0x10E5 : 0x10010e5, + 0x10E6 : 0x10010e6, + 0x10E7 : 0x10010e7, + 0x10E8 : 0x10010e8, + 0x10E9 : 0x10010e9, + 0x10EA : 0x10010ea, + 0x10EB : 0x10010eb, + 0x10EC : 0x10010ec, + 0x10ED : 0x10010ed, + 0x10EE : 0x10010ee, + 0x10EF : 0x10010ef, + 0x10F0 : 0x10010f0, + 0x10F1 : 0x10010f1, + 0x10F2 : 0x10010f2, + 0x10F3 : 0x10010f3, + 0x10F4 : 0x10010f4, + 0x10F5 : 0x10010f5, + 0x10F6 : 0x10010f6, + 0x1E8A : 0x1001e8a, + 0x012C : 0x100012c, + 0x01B5 : 0x10001b5, + 0x01E6 : 0x10001e6, + 0x01D2 : 0x10001d1, + 0x019F : 0x100019f, + 0x1E8B : 0x1001e8b, + 0x012D : 0x100012d, + 0x01B6 : 0x10001b6, + 0x01E7 : 0x10001e7, + //0x01D2 : 0x10001d2, + 0x0275 : 0x1000275, + 0x018F : 0x100018f, + 0x0259 : 0x1000259, + 0x1E36 : 0x1001e36, + 0x1E37 : 0x1001e37, + 0x1EA0 : 0x1001ea0, + 0x1EA1 : 0x1001ea1, + 0x1EA2 : 0x1001ea2, + 0x1EA3 : 0x1001ea3, + 0x1EA4 : 0x1001ea4, + 0x1EA5 : 0x1001ea5, + 0x1EA6 : 0x1001ea6, + 0x1EA7 : 0x1001ea7, + 0x1EA8 : 0x1001ea8, + 0x1EA9 : 0x1001ea9, + 0x1EAA : 0x1001eaa, + 0x1EAB : 0x1001eab, + 0x1EAC : 0x1001eac, + 0x1EAD : 0x1001ead, + 0x1EAE : 0x1001eae, + 0x1EAF : 0x1001eaf, + 0x1EB0 : 0x1001eb0, + 0x1EB1 : 0x1001eb1, + 0x1EB2 : 0x1001eb2, + 0x1EB3 : 0x1001eb3, + 0x1EB4 : 0x1001eb4, + 0x1EB5 : 0x1001eb5, + 0x1EB6 : 0x1001eb6, + 0x1EB7 : 0x1001eb7, + 0x1EB8 : 0x1001eb8, + 0x1EB9 : 0x1001eb9, + 0x1EBA : 0x1001eba, + 0x1EBB : 0x1001ebb, + 0x1EBC : 0x1001ebc, + 0x1EBD : 0x1001ebd, + 0x1EBE : 0x1001ebe, + 0x1EBF : 0x1001ebf, + 0x1EC0 : 0x1001ec0, + 0x1EC1 : 0x1001ec1, + 0x1EC2 : 0x1001ec2, + 0x1EC3 : 0x1001ec3, + 0x1EC4 : 0x1001ec4, + 0x1EC5 : 0x1001ec5, + 0x1EC6 : 0x1001ec6, + 0x1EC7 : 0x1001ec7, + 0x1EC8 : 0x1001ec8, + 0x1EC9 : 0x1001ec9, + 0x1ECA : 0x1001eca, + 0x1ECB : 0x1001ecb, + 0x1ECC : 0x1001ecc, + 0x1ECD : 0x1001ecd, + 0x1ECE : 0x1001ece, + 0x1ECF : 0x1001ecf, + 0x1ED0 : 0x1001ed0, + 0x1ED1 : 0x1001ed1, + 0x1ED2 : 0x1001ed2, + 0x1ED3 : 0x1001ed3, + 0x1ED4 : 0x1001ed4, + 0x1ED5 : 0x1001ed5, + 0x1ED6 : 0x1001ed6, + 0x1ED7 : 0x1001ed7, + 0x1ED8 : 0x1001ed8, + 0x1ED9 : 0x1001ed9, + 0x1EDA : 0x1001eda, + 0x1EDB : 0x1001edb, + 0x1EDC : 0x1001edc, + 0x1EDD : 0x1001edd, + 0x1EDE : 0x1001ede, + 0x1EDF : 0x1001edf, + 0x1EE0 : 0x1001ee0, + 0x1EE1 : 0x1001ee1, + 0x1EE2 : 0x1001ee2, + 0x1EE3 : 0x1001ee3, + 0x1EE4 : 0x1001ee4, + 0x1EE5 : 0x1001ee5, + 0x1EE6 : 0x1001ee6, + 0x1EE7 : 0x1001ee7, + 0x1EE8 : 0x1001ee8, + 0x1EE9 : 0x1001ee9, + 0x1EEA : 0x1001eea, + 0x1EEB : 0x1001eeb, + 0x1EEC : 0x1001eec, + 0x1EED : 0x1001eed, + 0x1EEE : 0x1001eee, + 0x1EEF : 0x1001eef, + 0x1EF0 : 0x1001ef0, + 0x1EF1 : 0x1001ef1, + 0x1EF4 : 0x1001ef4, + 0x1EF5 : 0x1001ef5, + 0x1EF6 : 0x1001ef6, + 0x1EF7 : 0x1001ef7, + 0x1EF8 : 0x1001ef8, + 0x1EF9 : 0x1001ef9, + 0x01A0 : 0x10001a0, + 0x01A1 : 0x10001a1, + 0x01AF : 0x10001af, + 0x01B0 : 0x10001b0, + 0x20A0 : 0x10020a0, + 0x20A1 : 0x10020a1, + 0x20A2 : 0x10020a2, + 0x20A3 : 0x10020a3, + 0x20A4 : 0x10020a4, + 0x20A5 : 0x10020a5, + 0x20A6 : 0x10020a6, + 0x20A7 : 0x10020a7, + 0x20A8 : 0x10020a8, + 0x20A9 : 0x10020a9, + 0x20AA : 0x10020aa, + 0x20AB : 0x10020ab, + 0x20AC : 0x20ac, + 0x2070 : 0x1002070, + 0x2074 : 0x1002074, + 0x2075 : 0x1002075, + 0x2076 : 0x1002076, + 0x2077 : 0x1002077, + 0x2078 : 0x1002078, + 0x2079 : 0x1002079, + 0x2080 : 0x1002080, + 0x2081 : 0x1002081, + 0x2082 : 0x1002082, + 0x2083 : 0x1002083, + 0x2084 : 0x1002084, + 0x2085 : 0x1002085, + 0x2086 : 0x1002086, + 0x2087 : 0x1002087, + 0x2088 : 0x1002088, + 0x2089 : 0x1002089, + 0x2202 : 0x1002202, + 0x2205 : 0x1002205, + 0x2208 : 0x1002208, + 0x2209 : 0x1002209, + 0x220B : 0x100220B, + 0x221A : 0x100221A, + 0x221B : 0x100221B, + 0x221C : 0x100221C, + 0x222C : 0x100222C, + 0x222D : 0x100222D, + 0x2235 : 0x1002235, + 0x2245 : 0x1002248, + 0x2247 : 0x1002247, + 0x2262 : 0x1002262, + 0x2263 : 0x1002263, + 0x2800 : 0x1002800, + 0x2801 : 0x1002801, + 0x2802 : 0x1002802, + 0x2803 : 0x1002803, + 0x2804 : 0x1002804, + 0x2805 : 0x1002805, + 0x2806 : 0x1002806, + 0x2807 : 0x1002807, + 0x2808 : 0x1002808, + 0x2809 : 0x1002809, + 0x280a : 0x100280a, + 0x280b : 0x100280b, + 0x280c : 0x100280c, + 0x280d : 0x100280d, + 0x280e : 0x100280e, + 0x280f : 0x100280f, + 0x2810 : 0x1002810, + 0x2811 : 0x1002811, + 0x2812 : 0x1002812, + 0x2813 : 0x1002813, + 0x2814 : 0x1002814, + 0x2815 : 0x1002815, + 0x2816 : 0x1002816, + 0x2817 : 0x1002817, + 0x2818 : 0x1002818, + 0x2819 : 0x1002819, + 0x281a : 0x100281a, + 0x281b : 0x100281b, + 0x281c : 0x100281c, + 0x281d : 0x100281d, + 0x281e : 0x100281e, + 0x281f : 0x100281f, + 0x2820 : 0x1002820, + 0x2821 : 0x1002821, + 0x2822 : 0x1002822, + 0x2823 : 0x1002823, + 0x2824 : 0x1002824, + 0x2825 : 0x1002825, + 0x2826 : 0x1002826, + 0x2827 : 0x1002827, + 0x2828 : 0x1002828, + 0x2829 : 0x1002829, + 0x282a : 0x100282a, + 0x282b : 0x100282b, + 0x282c : 0x100282c, + 0x282d : 0x100282d, + 0x282e : 0x100282e, + 0x282f : 0x100282f, + 0x2830 : 0x1002830, + 0x2831 : 0x1002831, + 0x2832 : 0x1002832, + 0x2833 : 0x1002833, + 0x2834 : 0x1002834, + 0x2835 : 0x1002835, + 0x2836 : 0x1002836, + 0x2837 : 0x1002837, + 0x2838 : 0x1002838, + 0x2839 : 0x1002839, + 0x283a : 0x100283a, + 0x283b : 0x100283b, + 0x283c : 0x100283c, + 0x283d : 0x100283d, + 0x283e : 0x100283e, + 0x283f : 0x100283f, + 0x2840 : 0x1002840, + 0x2841 : 0x1002841, + 0x2842 : 0x1002842, + 0x2843 : 0x1002843, + 0x2844 : 0x1002844, + 0x2845 : 0x1002845, + 0x2846 : 0x1002846, + 0x2847 : 0x1002847, + 0x2848 : 0x1002848, + 0x2849 : 0x1002849, + 0x284a : 0x100284a, + 0x284b : 0x100284b, + 0x284c : 0x100284c, + 0x284d : 0x100284d, + 0x284e : 0x100284e, + 0x284f : 0x100284f, + 0x2850 : 0x1002850, + 0x2851 : 0x1002851, + 0x2852 : 0x1002852, + 0x2853 : 0x1002853, + 0x2854 : 0x1002854, + 0x2855 : 0x1002855, + 0x2856 : 0x1002856, + 0x2857 : 0x1002857, + 0x2858 : 0x1002858, + 0x2859 : 0x1002859, + 0x285a : 0x100285a, + 0x285b : 0x100285b, + 0x285c : 0x100285c, + 0x285d : 0x100285d, + 0x285e : 0x100285e, + 0x285f : 0x100285f, + 0x2860 : 0x1002860, + 0x2861 : 0x1002861, + 0x2862 : 0x1002862, + 0x2863 : 0x1002863, + 0x2864 : 0x1002864, + 0x2865 : 0x1002865, + 0x2866 : 0x1002866, + 0x2867 : 0x1002867, + 0x2868 : 0x1002868, + 0x2869 : 0x1002869, + 0x286a : 0x100286a, + 0x286b : 0x100286b, + 0x286c : 0x100286c, + 0x286d : 0x100286d, + 0x286e : 0x100286e, + 0x286f : 0x100286f, + 0x2870 : 0x1002870, + 0x2871 : 0x1002871, + 0x2872 : 0x1002872, + 0x2873 : 0x1002873, + 0x2874 : 0x1002874, + 0x2875 : 0x1002875, + 0x2876 : 0x1002876, + 0x2877 : 0x1002877, + 0x2878 : 0x1002878, + 0x2879 : 0x1002879, + 0x287a : 0x100287a, + 0x287b : 0x100287b, + 0x287c : 0x100287c, + 0x287d : 0x100287d, + 0x287e : 0x100287e, + 0x287f : 0x100287f, + 0x2880 : 0x1002880, + 0x2881 : 0x1002881, + 0x2882 : 0x1002882, + 0x2883 : 0x1002883, + 0x2884 : 0x1002884, + 0x2885 : 0x1002885, + 0x2886 : 0x1002886, + 0x2887 : 0x1002887, + 0x2888 : 0x1002888, + 0x2889 : 0x1002889, + 0x288a : 0x100288a, + 0x288b : 0x100288b, + 0x288c : 0x100288c, + 0x288d : 0x100288d, + 0x288e : 0x100288e, + 0x288f : 0x100288f, + 0x2890 : 0x1002890, + 0x2891 : 0x1002891, + 0x2892 : 0x1002892, + 0x2893 : 0x1002893, + 0x2894 : 0x1002894, + 0x2895 : 0x1002895, + 0x2896 : 0x1002896, + 0x2897 : 0x1002897, + 0x2898 : 0x1002898, + 0x2899 : 0x1002899, + 0x289a : 0x100289a, + 0x289b : 0x100289b, + 0x289c : 0x100289c, + 0x289d : 0x100289d, + 0x289e : 0x100289e, + 0x289f : 0x100289f, + 0x28a0 : 0x10028a0, + 0x28a1 : 0x10028a1, + 0x28a2 : 0x10028a2, + 0x28a3 : 0x10028a3, + 0x28a4 : 0x10028a4, + 0x28a5 : 0x10028a5, + 0x28a6 : 0x10028a6, + 0x28a7 : 0x10028a7, + 0x28a8 : 0x10028a8, + 0x28a9 : 0x10028a9, + 0x28aa : 0x10028aa, + 0x28ab : 0x10028ab, + 0x28ac : 0x10028ac, + 0x28ad : 0x10028ad, + 0x28ae : 0x10028ae, + 0x28af : 0x10028af, + 0x28b0 : 0x10028b0, + 0x28b1 : 0x10028b1, + 0x28b2 : 0x10028b2, + 0x28b3 : 0x10028b3, + 0x28b4 : 0x10028b4, + 0x28b5 : 0x10028b5, + 0x28b6 : 0x10028b6, + 0x28b7 : 0x10028b7, + 0x28b8 : 0x10028b8, + 0x28b9 : 0x10028b9, + 0x28ba : 0x10028ba, + 0x28bb : 0x10028bb, + 0x28bc : 0x10028bc, + 0x28bd : 0x10028bd, + 0x28be : 0x10028be, + 0x28bf : 0x10028bf, + 0x28c0 : 0x10028c0, + 0x28c1 : 0x10028c1, + 0x28c2 : 0x10028c2, + 0x28c3 : 0x10028c3, + 0x28c4 : 0x10028c4, + 0x28c5 : 0x10028c5, + 0x28c6 : 0x10028c6, + 0x28c7 : 0x10028c7, + 0x28c8 : 0x10028c8, + 0x28c9 : 0x10028c9, + 0x28ca : 0x10028ca, + 0x28cb : 0x10028cb, + 0x28cc : 0x10028cc, + 0x28cd : 0x10028cd, + 0x28ce : 0x10028ce, + 0x28cf : 0x10028cf, + 0x28d0 : 0x10028d0, + 0x28d1 : 0x10028d1, + 0x28d2 : 0x10028d2, + 0x28d3 : 0x10028d3, + 0x28d4 : 0x10028d4, + 0x28d5 : 0x10028d5, + 0x28d6 : 0x10028d6, + 0x28d7 : 0x10028d7, + 0x28d8 : 0x10028d8, + 0x28d9 : 0x10028d9, + 0x28da : 0x10028da, + 0x28db : 0x10028db, + 0x28dc : 0x10028dc, + 0x28dd : 0x10028dd, + 0x28de : 0x10028de, + 0x28df : 0x10028df, + 0x28e0 : 0x10028e0, + 0x28e1 : 0x10028e1, + 0x28e2 : 0x10028e2, + 0x28e3 : 0x10028e3, + 0x28e4 : 0x10028e4, + 0x28e5 : 0x10028e5, + 0x28e6 : 0x10028e6, + 0x28e7 : 0x10028e7, + 0x28e8 : 0x10028e8, + 0x28e9 : 0x10028e9, + 0x28ea : 0x10028ea, + 0x28eb : 0x10028eb, + 0x28ec : 0x10028ec, + 0x28ed : 0x10028ed, + 0x28ee : 0x10028ee, + 0x28ef : 0x10028ef, + 0x28f0 : 0x10028f0, + 0x28f1 : 0x10028f1, + 0x28f2 : 0x10028f2, + 0x28f3 : 0x10028f3, + 0x28f4 : 0x10028f4, + 0x28f5 : 0x10028f5, + 0x28f6 : 0x10028f6, + 0x28f7 : 0x10028f7, + 0x28f8 : 0x10028f8, + 0x28f9 : 0x10028f9, + 0x28fa : 0x10028fa, + 0x28fb : 0x10028fb, + 0x28fc : 0x10028fc, + 0x28fd : 0x10028fd, + 0x28fe : 0x10028fe, + 0x28ff : 0x10028ff +}; diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/jsunzip.js b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/jsunzip.js new file mode 100755 index 0000000000000000000000000000000000000000..8968f866aa35d5ad1217d530c471caf3a962a646 --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/jsunzip.js @@ -0,0 +1,676 @@ +/* + * JSUnzip + * + * Copyright (c) 2011 by Erik Moller + * All Rights Reserved + * + * This software is provided 'as-is', without any express + * or implied warranty. In no event will the authors be + * held liable for any damages arising from the use of + * this software. + * + * Permission is granted to anyone to use this software + * for any purpose, including commercial applications, + * and to alter it and redistribute it freely, subject to + * the following restrictions: + * + * 1. The origin of this software must not be + * misrepresented; you must not claim that you + * wrote the original software. If you use this + * software in a product, an acknowledgment in + * the product documentation would be appreciated + * but is not required. + * + * 2. Altered source versions must be plainly marked + * as such, and must not be misrepresented as + * being the original software. + * + * 3. This notice may not be removed or altered from + * any source distribution. + */ + +var tinf; + +function JSUnzip() { + + this.getInt = function(offset, size) { + switch (size) { + case 4: + return (this.data.charCodeAt(offset + 3) & 0xff) << 24 | + (this.data.charCodeAt(offset + 2) & 0xff) << 16 | + (this.data.charCodeAt(offset + 1) & 0xff) << 8 | + (this.data.charCodeAt(offset + 0) & 0xff); + break; + case 2: + return (this.data.charCodeAt(offset + 1) & 0xff) << 8 | + (this.data.charCodeAt(offset + 0) & 0xff); + break; + default: + return this.data.charCodeAt(offset) & 0xff; + break; + } + }; + + this.getDOSDate = function(dosdate, dostime) { + var day = dosdate & 0x1f; + var month = ((dosdate >> 5) & 0xf) - 1; + var year = 1980 + ((dosdate >> 9) & 0x7f) + var second = (dostime & 0x1f) * 2; + var minute = (dostime >> 5) & 0x3f; + hour = (dostime >> 11) & 0x1f; + return new Date(year, month, day, hour, minute, second); + } + + this.open = function(data) { + this.data = data; + this.files = []; + + if (this.data.length < 22) + return { 'status' : false, 'error' : 'Invalid data' }; + var endOfCentralDirectory = this.data.length - 22; + while (endOfCentralDirectory >= 0 && this.getInt(endOfCentralDirectory, 4) != 0x06054b50) + --endOfCentralDirectory; + if (endOfCentralDirectory < 0) + return { 'status' : false, 'error' : 'Invalid data' }; + if (this.getInt(endOfCentralDirectory + 4, 2) != 0 || this.getInt(endOfCentralDirectory + 6, 2) != 0) + return { 'status' : false, 'error' : 'No multidisk support' }; + + var entriesInThisDisk = this.getInt(endOfCentralDirectory + 8, 2); + var centralDirectoryOffset = this.getInt(endOfCentralDirectory + 16, 4); + var globalCommentLength = this.getInt(endOfCentralDirectory + 20, 2); + this.comment = this.data.slice(endOfCentralDirectory + 22, endOfCentralDirectory + 22 + globalCommentLength); + + var fileOffset = centralDirectoryOffset; + + for (var i = 0; i < entriesInThisDisk; ++i) { + if (this.getInt(fileOffset + 0, 4) != 0x02014b50) + return { 'status' : false, 'error' : 'Invalid data' }; + if (this.getInt(fileOffset + 6, 2) > 20) + return { 'status' : false, 'error' : 'Unsupported version' }; + if (this.getInt(fileOffset + 8, 2) & 1) + return { 'status' : false, 'error' : 'Encryption not implemented' }; + + var compressionMethod = this.getInt(fileOffset + 10, 2); + if (compressionMethod != 0 && compressionMethod != 8) + return { 'status' : false, 'error' : 'Unsupported compression method' }; + + var lastModFileTime = this.getInt(fileOffset + 12, 2); + var lastModFileDate = this.getInt(fileOffset + 14, 2); + var lastModifiedDate = this.getDOSDate(lastModFileDate, lastModFileTime); + + var crc = this.getInt(fileOffset + 16, 4); + // TODO: crc + + var compressedSize = this.getInt(fileOffset + 20, 4); + var uncompressedSize = this.getInt(fileOffset + 24, 4); + + var fileNameLength = this.getInt(fileOffset + 28, 2); + var extraFieldLength = this.getInt(fileOffset + 30, 2); + var fileCommentLength = this.getInt(fileOffset + 32, 2); + + var relativeOffsetOfLocalHeader = this.getInt(fileOffset + 42, 4); + + var fileName = this.data.slice(fileOffset + 46, fileOffset + 46 + fileNameLength); + var fileComment = this.data.slice(fileOffset + 46 + fileNameLength + extraFieldLength, fileOffset + 46 + fileNameLength + extraFieldLength + fileCommentLength); + + if (this.getInt(relativeOffsetOfLocalHeader + 0, 4) != 0x04034b50) + return { 'status' : false, 'error' : 'Invalid data' }; + var localFileNameLength = this.getInt(relativeOffsetOfLocalHeader + 26, 2); + var localExtraFieldLength = this.getInt(relativeOffsetOfLocalHeader + 28, 2); + var localFileContent = relativeOffsetOfLocalHeader + 30 + localFileNameLength + localExtraFieldLength; + + this.files[fileName] = + { + 'fileComment' : fileComment, + 'compressionMethod' : compressionMethod, + 'compressedSize' : compressedSize, + 'uncompressedSize' : uncompressedSize, + 'localFileContent' : localFileContent, + 'lastModifiedDate' : lastModifiedDate + }; + + fileOffset += 46 + fileNameLength + extraFieldLength + fileCommentLength; + } + return { 'status' : true } + }; + + + this.read = function(fileName) { + var fileInfo = this.files[fileName]; + if (fileInfo) { + if (fileInfo.compressionMethod == 8) { + if (!tinf) { + tinf = new TINF(); + tinf.init(); + } + var result = tinf.uncompress(this.data, fileInfo.localFileContent); + if (result.status == tinf.OK) + return { 'status' : true, 'data' : result.data }; + else + return { 'status' : false, 'error' : result.error }; + } else { + return { 'status' : true, 'data' : this.data.slice(fileInfo.localFileContent, fileInfo.localFileContent + fileInfo.uncompressedSize) }; + } + } + return { 'status' : false, 'error' : "File '" + fileName + "' doesn't exist in zip" }; + }; + +}; + + + +/* + * tinflate - tiny inflate + * + * Copyright (c) 2003 by Joergen Ibsen / Jibz + * All Rights Reserved + * + * http://www.ibsensoftware.com/ + * + * This software is provided 'as-is', without any express + * or implied warranty. In no event will the authors be + * held liable for any damages arising from the use of + * this software. + * + * Permission is granted to anyone to use this software + * for any purpose, including commercial applications, + * and to alter it and redistribute it freely, subject to + * the following restrictions: + * + * 1. The origin of this software must not be + * misrepresented; you must not claim that you + * wrote the original software. If you use this + * software in a product, an acknowledgment in + * the product documentation would be appreciated + * but is not required. + * + * 2. Altered source versions must be plainly marked + * as such, and must not be misrepresented as + * being the original software. + * + * 3. This notice may not be removed or altered from + * any source distribution. + */ + +/* + * tinflate javascript port by Erik Moller in May 2011. + * emoller@opera.com + * + * read_bits() patched by mike@imidio.com to allow + * reading more then 8 bits (needed in some zlib streams) + */ + +"use strict"; + +function TINF() { + +this.OK = 0; +this.DATA_ERROR = (-3); +this.WINDOW_SIZE = 32768; + +/* ------------------------------ * + * -- internal data structures -- * + * ------------------------------ */ + +this.TREE = function() { + this.table = new Array(16); /* table of code length counts */ + this.trans = new Array(288); /* code -> symbol translation table */ +}; + +this.DATA = function(that) { + this.source = ''; + this.sourceIndex = 0; + this.tag = 0; + this.bitcount = 0; + + this.dest = []; + + this.history = []; + + this.ltree = new that.TREE(); /* dynamic length/symbol tree */ + this.dtree = new that.TREE(); /* dynamic distance tree */ +}; + +/* --------------------------------------------------- * + * -- uninitialized global data (static structures) -- * + * --------------------------------------------------- */ + +this.sltree = new this.TREE(); /* fixed length/symbol tree */ +this.sdtree = new this.TREE(); /* fixed distance tree */ + +/* extra bits and base tables for length codes */ +this.length_bits = new Array(30); +this.length_base = new Array(30); + +/* extra bits and base tables for distance codes */ +this.dist_bits = new Array(30); +this.dist_base = new Array(30); + +/* special ordering of code length codes */ +this.clcidx = [ + 16, 17, 18, 0, 8, 7, 9, 6, + 10, 5, 11, 4, 12, 3, 13, 2, + 14, 1, 15 +]; + +/* ----------------------- * + * -- utility functions -- * + * ----------------------- */ + +/* build extra bits and base tables */ +this.build_bits_base = function(bits, base, delta, first) +{ + var i, sum; + + /* build bits table */ + for (i = 0; i < delta; ++i) bits[i] = 0; + for (i = 0; i < 30 - delta; ++i) bits[i + delta] = Math.floor(i / delta); + + /* build base table */ + for (sum = first, i = 0; i < 30; ++i) + { + base[i] = sum; + sum += 1 << bits[i]; + } +} + +/* build the fixed huffman trees */ +this.build_fixed_trees = function(lt, dt) +{ + var i; + + /* build fixed length tree */ + for (i = 0; i < 7; ++i) lt.table[i] = 0; + + lt.table[7] = 24; + lt.table[8] = 152; + lt.table[9] = 112; + + for (i = 0; i < 24; ++i) lt.trans[i] = 256 + i; + for (i = 0; i < 144; ++i) lt.trans[24 + i] = i; + for (i = 0; i < 8; ++i) lt.trans[24 + 144 + i] = 280 + i; + for (i = 0; i < 112; ++i) lt.trans[24 + 144 + 8 + i] = 144 + i; + + /* build fixed distance tree */ + for (i = 0; i < 5; ++i) dt.table[i] = 0; + + dt.table[5] = 32; + + for (i = 0; i < 32; ++i) dt.trans[i] = i; +} + +/* given an array of code lengths, build a tree */ +this.build_tree = function(t, lengths, loffset, num) +{ + var offs = new Array(16); + var i, sum; + + /* clear code length count table */ + for (i = 0; i < 16; ++i) t.table[i] = 0; + + /* scan symbol lengths, and sum code length counts */ + for (i = 0; i < num; ++i) t.table[lengths[loffset + i]]++; + + t.table[0] = 0; + + /* compute offset table for distribution sort */ + for (sum = 0, i = 0; i < 16; ++i) + { + offs[i] = sum; + sum += t.table[i]; + } + + /* create code->symbol translation table (symbols sorted by code) */ + for (i = 0; i < num; ++i) + { + if (lengths[loffset + i]) t.trans[offs[lengths[loffset + i]]++] = i; + } +} + +/* ---------------------- * + * -- decode functions -- * + * ---------------------- */ + +/* get one bit from source stream */ +this.getbit = function(d) +{ + var bit; + + /* check if tag is empty */ + if (!d.bitcount--) + { + /* load next tag */ + d.tag = d.source[d.sourceIndex++] & 0xff; + d.bitcount = 7; + } + + /* shift bit out of tag */ + bit = d.tag & 0x01; + d.tag >>= 1; + + return bit; +} + +/* read a num bit value from a stream and add base */ +function read_bits_direct(source, bitcount, tag, idx, num) +{ + var val = 0; + while (bitcount < 24) { + tag = tag | (source[idx++] & 0xff) << bitcount; + bitcount += 8; + } + val = tag & (0xffff >> (16 - num)); + tag >>= num; + bitcount -= num; + return [bitcount, tag, idx, val]; +} +this.read_bits = function(d, num, base) +{ + if (!num) + return base; + + var ret = read_bits_direct(d.source, d.bitcount, d.tag, d.sourceIndex, num); + d.bitcount = ret[0]; + d.tag = ret[1]; + d.sourceIndex = ret[2]; + return ret[3] + base; +} + +/* given a data stream and a tree, decode a symbol */ +this.decode_symbol = function(d, t) +{ + while (d.bitcount < 16) { + d.tag = d.tag | (d.source[d.sourceIndex++] & 0xff) << d.bitcount; + d.bitcount += 8; + } + + var sum = 0, cur = 0, len = 0; + do { + cur = 2 * cur + ((d.tag & (1 << len)) >> len); + + ++len; + + sum += t.table[len]; + cur -= t.table[len]; + + } while (cur >= 0); + + d.tag >>= len; + d.bitcount -= len; + + return t.trans[sum + cur]; +} + +/* given a data stream, decode dynamic trees from it */ +this.decode_trees = function(d, lt, dt) +{ + var code_tree = new this.TREE(); + var lengths = new Array(288+32); + var hlit, hdist, hclen; + var i, num, length; + + /* get 5 bits HLIT (257-286) */ + hlit = this.read_bits(d, 5, 257); + + /* get 5 bits HDIST (1-32) */ + hdist = this.read_bits(d, 5, 1); + + /* get 4 bits HCLEN (4-19) */ + hclen = this.read_bits(d, 4, 4); + + for (i = 0; i < 19; ++i) lengths[i] = 0; + + /* read code lengths for code length alphabet */ + for (i = 0; i < hclen; ++i) + { + /* get 3 bits code length (0-7) */ + var clen = this.read_bits(d, 3, 0); + + lengths[this.clcidx[i]] = clen; + } + + /* build code length tree */ + this.build_tree(code_tree, lengths, 0, 19); + + /* decode code lengths for the dynamic trees */ + for (num = 0; num < hlit + hdist; ) + { + var sym = this.decode_symbol(d, code_tree); + + switch (sym) + { + case 16: + /* copy previous code length 3-6 times (read 2 bits) */ + { + var prev = lengths[num - 1]; + for (length = this.read_bits(d, 2, 3); length; --length) + { + lengths[num++] = prev; + } + } + break; + case 17: + /* repeat code length 0 for 3-10 times (read 3 bits) */ + for (length = this.read_bits(d, 3, 3); length; --length) + { + lengths[num++] = 0; + } + break; + case 18: + /* repeat code length 0 for 11-138 times (read 7 bits) */ + for (length = this.read_bits(d, 7, 11); length; --length) + { + lengths[num++] = 0; + } + break; + default: + /* values 0-15 represent the actual code lengths */ + lengths[num++] = sym; + break; + } + } + + /* build dynamic trees */ + this.build_tree(lt, lengths, 0, hlit); + this.build_tree(dt, lengths, hlit, hdist); +} + +/* ----------------------------- * + * -- block inflate functions -- * + * ----------------------------- */ + +/* given a stream and two trees, inflate a block of data */ +this.inflate_block_data = function(d, lt, dt) +{ + // js optimization. + var ddest = d.dest; + var ddestlength = ddest.length; + + while (1) + { + var sym = this.decode_symbol(d, lt); + + /* check for end of block */ + if (sym == 256) + { + return this.OK; + } + + if (sym < 256) + { + ddest[ddestlength++] = sym; // ? String.fromCharCode(sym); + d.history.push(sym); + } else { + + var length, dist, offs; + var i; + + sym -= 257; + + /* possibly get more bits from length code */ + length = this.read_bits(d, this.length_bits[sym], this.length_base[sym]); + + dist = this.decode_symbol(d, dt); + + /* possibly get more bits from distance code */ + offs = d.history.length - this.read_bits(d, this.dist_bits[dist], this.dist_base[dist]); + + if (offs < 0) + throw ("Invalid zlib offset " + offs); + + /* copy match */ + for (i = offs; i < offs + length; ++i) { + //ddest[ddestlength++] = ddest[i]; + ddest[ddestlength++] = d.history[i]; + d.history.push(d.history[i]); + } + } + } +} + +/* inflate an uncompressed block of data */ +this.inflate_uncompressed_block = function(d) +{ + var length, invlength; + var i; + + if (d.bitcount > 7) { + var overflow = Math.floor(d.bitcount / 8); + d.sourceIndex -= overflow; + d.bitcount = 0; + d.tag = 0; + } + + /* get length */ + length = d.source[d.sourceIndex+1]; + length = 256*length + d.source[d.sourceIndex]; + + /* get one's complement of length */ + invlength = d.source[d.sourceIndex+3]; + invlength = 256*invlength + d.source[d.sourceIndex+2]; + + /* check length */ + if (length != (~invlength & 0x0000ffff)) return this.DATA_ERROR; + + d.sourceIndex += 4; + + /* copy block */ + for (i = length; i; --i) { + d.history.push(d.source[d.sourceIndex]); + d.dest[d.dest.length] = d.source[d.sourceIndex++]; + } + + /* make sure we start next block on a byte boundary */ + d.bitcount = 0; + + return this.OK; +} + +/* inflate a block of data compressed with fixed huffman trees */ +this.inflate_fixed_block = function(d) +{ + /* decode block using fixed trees */ + return this.inflate_block_data(d, this.sltree, this.sdtree); +} + +/* inflate a block of data compressed with dynamic huffman trees */ +this.inflate_dynamic_block = function(d) +{ + /* decode trees from stream */ + this.decode_trees(d, d.ltree, d.dtree); + + /* decode block using decoded trees */ + return this.inflate_block_data(d, d.ltree, d.dtree); +} + +/* ---------------------- * + * -- public functions -- * + * ---------------------- */ + +/* initialize global (static) data */ +this.init = function() +{ + /* build fixed huffman trees */ + this.build_fixed_trees(this.sltree, this.sdtree); + + /* build extra bits and base tables */ + this.build_bits_base(this.length_bits, this.length_base, 4, 3); + this.build_bits_base(this.dist_bits, this.dist_base, 2, 1); + + /* fix a special case */ + this.length_bits[28] = 0; + this.length_base[28] = 258; + + this.reset(); +} + +this.reset = function() +{ + this.d = new this.DATA(this); + delete this.header; +} + +/* inflate stream from source to dest */ +this.uncompress = function(source, offset) +{ + + var d = this.d; + var bfinal; + + /* initialise data */ + d.source = source; + d.sourceIndex = offset; + d.bitcount = 0; + + d.dest = []; + + // Skip zlib header at start of stream + if (typeof this.header == 'undefined') { + this.header = this.read_bits(d, 16, 0); + /* byte 0: 0x78, 7 = 32k window size, 8 = deflate */ + /* byte 1: check bits for header and other flags */ + } + + var blocks = 0; + + do { + + var btype; + var res; + + /* read final block flag */ + bfinal = this.getbit(d); + + /* read block type (2 bits) */ + btype = this.read_bits(d, 2, 0); + + /* decompress block */ + switch (btype) + { + case 0: + /* decompress uncompressed block */ + res = this.inflate_uncompressed_block(d); + break; + case 1: + /* decompress block with fixed huffman trees */ + res = this.inflate_fixed_block(d); + break; + case 2: + /* decompress block with dynamic huffman trees */ + res = this.inflate_dynamic_block(d); + break; + default: + return { 'status' : this.DATA_ERROR }; + } + + if (res != this.OK) return { 'status' : this.DATA_ERROR }; + blocks++; + + } while (!bfinal && d.sourceIndex < d.source.length); + + d.history = d.history.slice(-this.WINDOW_SIZE); + + return { 'status' : this.OK, 'data' : d.dest }; +} + +}; diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/keyboard-extra.css b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/keyboard-extra.css new file mode 100644 index 0000000000000000000000000000000000000000..446dd53458f9f0932d0e99fa3039d98fc0bd952f --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/keyboard-extra.css @@ -0,0 +1,271 @@ +#keyboardInputMaster { + position:absolute; + font:normal 11px Arial,sans-serif; + border-top:1px solid #eeeeee; + border-right:1px solid #888888; + border-bottom:1px solid #444444; + border-left:1px solid #cccccc; + -webkit-border-radius:0.6em; + -moz-border-radius:0.6em; + border-radius:0.6em; + -webkit-box-shadow:0px 2px 10px #444444; + -moz-box-shadow:0px 2px 10px #444444; + box-shadow:0px 2px 10px #444444; + opacity:0.95; + filter:alpha(opacity=95); + background-color:#dddddd; + text-align:left; + z-index:1000000; + width:auto; + height:auto; + min-width:0; + min-height:0; + margin:0px; + padding:0px; + line-height:normal; + -moz-user-select:none; + cursor:default; +} +#keyboardInputMaster * { + position:static; + color:#000000; + background:transparent; + font:normal 11px Arial,sans-serif; + width:auto; + height:auto; + min-width:0; + min-height:0; + margin:0px; + padding:0px; + border:0px none; + outline:0px; + vertical-align:baseline; + line-height:1.3em; +} +#keyboardInputMaster table { + table-layout:auto; +} +#keyboardInputMaster.keyboardInputSize1, +#keyboardInputMaster.keyboardInputSize1 * { + font-size:9px; +} +#keyboardInputMaster.keyboardInputSize3, +#keyboardInputMaster.keyboardInputSize3 * { + font-size:13px; +} +#keyboardInputMaster.keyboardInputSize4, +#keyboardInputMaster.keyboardInputSize4 * { + font-size:16px; +} +#keyboardInputMaster.keyboardInputSize5, +#keyboardInputMaster.keyboardInputSize5 * { + font-size:20px; +} + +#keyboardInputMaster thead tr th { + padding:0.3em 0.3em 0.1em 0.3em; + background-color:#999999; + white-space:nowrap; + text-align:right; + -webkit-border-radius:0.6em 0.6em 0px 0px; + -moz-border-radius:0.6em 0.6em 0px 0px; + border-radius:0.6em 0.6em 0px 0px; +} +#keyboardInputMaster thead tr th div { + float:left; + font-size:130% !important; + height:1.3em; + font-weight:bold; + position:relative; + z-index:1; + margin-right:0.5em; + cursor:pointer; + background-color:transparent; +} +#keyboardInputMaster thead tr th div ol { + position:absolute; + left:0px; + top:90%; + list-style-type:none; + height:9.4em; + overflow-y:auto; + overflow-x:hidden; + background-color:#f6f6f6; + border:1px solid #999999; + display:none; + text-align:left; + width:12em; +} +#keyboardInputMaster thead tr th div ol li { + padding:0.2em 0.4em; + cursor:pointer; + white-space:nowrap; + width:12em; +} +#keyboardInputMaster thead tr th div ol li.selected { + background-color:#ffffcc; +} +#keyboardInputMaster thead tr th div ol li:hover, +#keyboardInputMaster thead tr th div ol li.hover { + background-color:#dddddd; +} +#keyboardInputMaster thead tr th span, +#keyboardInputMaster thead tr th strong, +#keyboardInputMaster thead tr th small, +#keyboardInputMaster thead tr th big { + display:inline-block; + padding:0px 0.4em; + height:1.4em; + line-height:1.4em; + border-top:1px solid #e5e5e5; + border-right:1px solid #5d5d5d; + border-bottom:1px solid #5d5d5d; + border-left:1px solid #e5e5e5; + background-color:#cccccc; + cursor:pointer; + margin:0px 0px 0px 0.3em; + -webkit-border-radius:0.3em; + -moz-border-radius:0.3em; + border-radius:0.3em; + vertical-align:middle; + -webkit-transition:background-color .15s ease-in-out; + -o-transition:background-color .15s ease-in-out; + transition:background-color .15s ease-in-out; +} +#keyboardInputMaster thead tr th strong { + font-weight:bold; +} +#keyboardInputMaster thead tr th small { + -webkit-border-radius:0.3em 0px 0px 0.3em; + -moz-border-radius:0.3em 0px 0px 0.3em; + border-radius:0.3em 0px 0px 0.3em; + border-right:1px solid #aaaaaa; + padding:0px 0.2em 0px 0.3em; +} +#keyboardInputMaster thead tr th big { + -webkit-border-radius:0px 0.3em 0.3em 0px; + -moz-border-radius:0px 0.3em 0.3em 0px; + border-radius:0px 0.3em 0.3em 0px; + border-left:0px none; + margin:0px; + padding:0px 0.3em 0px 0.2em; +} +#keyboardInputMaster thead tr th span:hover, +#keyboardInputMaster thead tr th span.hover, +#keyboardInputMaster thead tr th strong:hover, +#keyboardInputMaster thead tr th strong.hover, +#keyboardInputMaster thead tr th small:hover, +#keyboardInputMaster thead tr th small.hover, +#keyboardInputMaster thead tr th big:hover, +#keyboardInputMaster thead tr th big.hover { + background-color:#dddddd; +} + +#keyboardInputMaster tbody tr td { + text-align:left; + padding:0.2em 0.3em 0.3em 0.3em; + vertical-align:top; +} +#keyboardInputMaster tbody tr td div { + text-align:center; + position:relative; + zoom:1; +} +#keyboardInputMaster tbody tr td table { + white-space:nowrap; + width:100%; + border-collapse:separate; + border-spacing:0px; +} +#keyboardInputMaster tbody tr td#keyboardInputNumpad table { + margin-left:0.2em; + width:auto; +} +#keyboardInputMaster tbody tr td table.keyboardInputCenter { + width:auto; + margin:0px auto; +} +#keyboardInputMaster tbody tr td table tbody tr td { + vertical-align:middle; + padding:0px 0.45em; + white-space:pre; + height:1.8em; + font-family:'Lucida Console','Arial Unicode MS',monospace; + border-top:1px solid #e5e5e5; + border-right:1px solid #5d5d5d; + border-bottom:1px solid #5d5d5d; + border-left:1px solid #e5e5e5; + background-color:#eeeeee; + cursor:default; + min-width:0.75em; + -webkit-border-radius:0.2em; + -moz-border-radius:0.2em; + border-radius:0.2em; + -webkit-transition:background-color .15s ease-in-out; + -o-transition:background-color .15s ease-in-out; + transition:background-color .15s ease-in-out; +} +#keyboardInputMaster tbody tr td table tbody tr td.last { + width:99%; +} +#keyboardInputMaster tbody tr td table tbody tr td.space { + padding:0px 4em; +} +#keyboardInputMaster tbody tr td table tbody tr td.deadkey { + background-color:#ccccdd; +} +#keyboardInputMaster tbody tr td table tbody tr td.target { + background-color:#ddddcc; +} +#keyboardInputMaster tbody tr td table tbody tr td:hover, +#keyboardInputMaster tbody tr td table tbody tr td.hover { + border-top:1px solid #d5d5d5; + border-right:1px solid #555555; + border-bottom:1px solid #555555; + border-left:1px solid #d5d5d5; + background-color:#cccccc; +} +#keyboardInputMaster thead tr th span:active, +#keyboardInputMaster thead tr th span.pressed, +#keyboardInputMaster tbody tr td table tbody tr td:active, +#keyboardInputMaster tbody tr td table tbody tr td.pressed { + border-top:1px solid #555555 !important; + border-right:1px solid #d5d5d5; + border-bottom:1px solid #d5d5d5; + border-left:1px solid #555555; + background-color:#cccccc; +} + +#keyboardInputMaster tbody tr td table tbody tr td small { + display:block; + text-align:center; + font-size:0.6em !important; + line-height:1.1em; +} + +#keyboardInputMaster tbody tr td div label { + position:absolute; + bottom:0.2em; + left:0.3em; +} +#keyboardInputMaster tbody tr td div label input { + background-color:#f6f6f6; + vertical-align:middle; + font-size:inherit; + width:1.1em; + height:1.1em; +} +#keyboardInputMaster tbody tr td div var { + position:absolute; + bottom:0px; + right:3px; + font-weight:bold; + font-style:italic; + color:#444444; +} + +.keyboardInputInitiator { + margin:0px 3px; + vertical-align:middle; + cursor:pointer; +} diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/keyboard-extra.js b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/keyboard-extra.js new file mode 100644 index 0000000000000000000000000000000000000000..d43c6ed43325da7cc995a2f2888141eb47a8158c --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/keyboard-extra.js @@ -0,0 +1,1798 @@ +/* ******************************************************************** + ********************************************************************** + * HTML Virtual Keyboard Interface Script - v1.49 + * Copyright (c) 2011 - GreyWyvern + * + * - Licenced for free distribution under the BSDL + * http://www.opensource.org/licenses/bsd-license.php + * + * Add a script-driven keyboard interface to text fields, password + * fields and textareas. + * + * See http://www.greywyvern.com/code/javascript/keyboard for examples + * and usage instructions. + * + * Version 1.49 - November 8, 2011 + * - Don't display language drop-down if only one keyboard available + * + * See full changelog at: + * http://www.greywyvern.com/code/javascript/keyboard.changelog.txt + * + * Keyboard Credits + * - Yiddish (Yidish Lebt) keyboard layout by Simche Taub (jidysz.net) + * - Urdu Phonetic keyboard layout by Khalid Malik + * - Yiddish keyboard layout by Helmut Wollmersdorfer + * - Khmer keyboard layout by Sovann Heng (km-kh.com) + * - Dari keyboard layout by Saif Fazel + * - Kurdish keyboard layout by Ara Qadir + * - Assamese keyboard layout by Kanchan Gogoi + * - Bulgarian BDS keyboard layout by Milen Georgiev + * - Basic Japanese Hiragana/Katakana keyboard layout by Damjan + * - Ukrainian keyboard layout by Dmitry Nikitin + * - Macedonian keyboard layout by Damjan Dimitrioski + * - Pashto keyboard layout by Ahmad Wali Achakzai (qamosona.com) + * - Armenian Eastern and Western keyboard layouts by Hayastan Project (www.hayastan.co.uk) + * - Pinyin keyboard layout from a collaboration with Lou Winklemann + * - Kazakh keyboard layout by Alex Madyankin + * - Danish keyboard layout by Verner Kjærsgaard + * - Slovak keyboard layout by Daniel Lara (www.learningslovak.com) + * - Belarusian and Serbian Cyrillic keyboard layouts by Evgeniy Titov + * - Bulgarian Phonetic keyboard layout by Samuil Gospodinov + * - Swedish keyboard layout by Håkan Sandberg + * - Romanian keyboard layout by Aurel + * - Farsi (Persian) keyboard layout by Kaveh Bakhtiyari (www.bakhtiyari.com) + * - Burmese keyboard layout by Cetanapa + * - Bosnian/Croatian/Serbian Latin/Slovenian keyboard layout by Miran Zeljko + * - Hungarian keyboard layout by Antal Sall 'Hiromacu' + * - Arabic keyboard layout by Srinivas Reddy + * - Italian and Spanish (Spain) keyboard layouts by dictionarist.com + * - Lithuanian and Russian keyboard layouts by Ramunas + * - German keyboard layout by QuHno + * - French keyboard layout by Hidden Evil + * - Polish Programmers layout by moose + * - Turkish keyboard layouts by offcu + * - Dutch and US Int'l keyboard layouts by jerone + * + */ +var VKI_attach, VKI_close; +(function() { + var self = this; + + this.VKI_version = "1.49"; + this.VKI_showVersion = true; + this.VKI_target = false; + this.VKI_shift = this.VKI_shiftlock = false; + this.VKI_altgr = this.VKI_altgrlock = false; + this.VKI_dead = false; + this.VKI_deadBox = true; // Show the dead keys checkbox + this.VKI_deadkeysOn = false; // Turn dead keys on by default + this.VKI_numberPad = true; // Allow user to open and close the number pad + this.VKI_numberPadOn = false; // Show number pad by default + this.VKI_kts = this.VKI_kt = "US International"; // Default keyboard layout + this.VKI_langAdapt = true; // Use lang attribute of input to select keyboard + this.VKI_size = 2; // Default keyboard size (1-5) + this.VKI_sizeAdj = true; // Allow user to adjust keyboard size + this.VKI_clearPasswords = false; // Clear password fields on focus + this.VKI_imageURI = "keyboard.png"; // If empty string, use imageless mode + this.VKI_clickless = 0; // 0 = disabled, > 0 = delay in ms + this.VKI_activeTab = 0; // Tab moves to next: 1 = element, 2 = keyboard enabled element + this.VKI_enterSubmit = true; // Submit forms when Enter is pressed + this.VKI_keyCenter = 3; + + this.VKI_isIE = /*@cc_on!@*/false; + this.VKI_isIE6 = /*@if(@_jscript_version == 5.6)!@end@*/false; + this.VKI_isIElt8 = /*@if(@_jscript_version < 5.8)!@end@*/false; + this.VKI_isWebKit = RegExp("KHTML").test(navigator.userAgent); + this.VKI_isOpera = RegExp("Opera").test(navigator.userAgent); + this.VKI_isMoz = (!this.VKI_isWebKit && navigator.product == "Gecko"); + + /* ***** i18n text strings ************************************* */ + this.VKI_i18n = { + '00': "Display Number Pad", + '01': "Display virtual keyboard interface", + '02': "Select keyboard layout", + '03': "Dead keys", + '04': "On", + '05': "Off", + '06': "Close the keyboard", + '07': "Clear", + '08': "Clear this input", + '09': "Version", + '10': "Decrease keyboard size", + '11': "Increase keyboard size" + }; + + + /* ***** Create keyboards ************************************** */ + this.VKI_layout = {}; + + // - Lay out each keyboard in rows of sub-arrays. Each sub-array + // represents one key. + // + // - Each sub-array consists of four slots described as follows: + // example: ["a", "A", "\u00e1", "\u00c1"] + // + // a) Normal character + // A) Character + Shift/Caps + // \u00e1) Character + Alt/AltGr/AltLk + // \u00c1) Character + Shift/Caps + Alt/AltGr/AltLk + // + // You may include sub-arrays which are fewer than four slots. + // In these cases, the missing slots will be blanked when the + // corresponding modifier key (Shift or AltGr) is pressed. + // + // - If the second slot of a sub-array matches one of the following + // strings: + // "Tab", "Caps", "Shift", "Enter", "Bksp", + // "Alt" OR "AltGr", "AltLk" + // then the function of the key will be the following, + // respectively: + // - Insert a tab + // - Toggle Caps Lock (technically a Shift Lock) + // - Next entered character will be the shifted character + // - Insert a newline (textarea), or close the keyboard + // - Delete the previous character + // - Next entered character will be the alternate character + // - Toggle Alt/AltGr Lock + // + // The first slot of this sub-array will be the text to display + // on the corresponding key. This allows for easy localisation + // of key names. + // + // - Layout dead keys (diacritic + letter) should be added as + // property/value pairs of objects with hash keys equal to the + // diacritic. See the "this.VKI_deadkey" object below the layout + // definitions. In each property/value pair, the value is what + // the diacritic would change the property name to. + // + // - Note that any characters beyond the normal ASCII set should be + // entered in escaped Unicode format. (eg \u00a3 = Pound symbol) + // You can find Unicode values for characters here: + // http://unicode.org/charts/ + // + // - To remove a keyboard, just delete it, or comment it out of the + // source code. If you decide to remove the US International + // keyboard layout, make sure you change the default layout + // (this.VKI_kt) above so it references an existing layout. + + this.VKI_layout['\u0627\u0644\u0639\u0631\u0628\u064a\u0629'] = { + 'name': "Arabic", 'keys': [ + [["\u0630", "\u0651 "], ["1", "!", "\u00a1", "\u00b9"], ["2", "@", "\u00b2"], ["3", "#", "\u00b3"], ["4", "$", "\u00a4", "\u00a3"], ["5", "%", "\u20ac"], ["6", "^", "\u00bc"], ["7", "&", "\u00bd"], ["8", "*", "\u00be"], ["9", "(", "\u2018"], ["0", ")", "\u2019"], ["-", "_", "\u00a5"], ["=", "+", "\u00d7", "\u00f7"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0636", "\u064e"], ["\u0635", "\u064b"], ["\u062b", "\u064f"], ["\u0642", "\u064c"], ["\u0641", "\u0644"], ["\u063a", "\u0625"], ["\u0639", "\u2018"], ["\u0647", "\u00f7"], ["\u062e", "\u00d7"], ["\u062d", "\u061b"], ["\u062c", "<"], ["\u062f", ">"], ["\\", "|"]], + [["Caps", "Caps"], ["\u0634", "\u0650"], ["\u0633", "\u064d"], ["\u064a", "]"], ["\u0628", "["], ["\u0644", "\u0644"], ["\u0627", "\u0623"], ["\u062a", "\u0640"], ["\u0646", "\u060c"], ["\u0645", "/"], ["\u0643", ":"], ["\u0637", '"'], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0626", "~"], ["\u0621", "\u0652"], ["\u0624", "}"], ["\u0631", "{"], ["\u0644", "\u0644"], ["\u0649", "\u0622"], ["\u0629", "\u2019"], ["\u0648", ","], ["\u0632", "."], ["\u0638", "\u061f"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["Alt", "Alt"]] + ], 'lang': ["ar"] }; + + this.VKI_layout['\u0985\u09b8\u09ae\u09c0\u09df\u09be'] = { + 'name': "Assamese", 'keys': [ + [["+", "?"], ["\u09E7", "{", "\u09E7"], ["\u09E8", "}", "\u09E8"], ["\u09E9", "\u09CD\u09F0", "\u09E9"], ["\u09EA", "\u09F0\u09CD", "\u09EA"], ["\u09EB", "\u099C\u09CD\u09F0", "\u09EB"], ["\u09EC", "\u0995\u09CD\u09B7", "\u09EC"], ["\u09ED", "\u0995\u09CD\u09F0", "\u09ED"], ["\u09EE", "\u09B6\u09CD\u09F0", "\u09EE"], ["\u09EF", "(", "\u09EF"], ["\u09E6", ")", "\u09E6"], ["-", ""], ["\u09C3", "\u098B", "\u09E2", "\u09E0"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u09CC", "\u0994", "\u09D7"], ["\u09C8", "\u0990"], ["\u09BE", "\u0986"], ["\u09C0", "\u0988", "\u09E3", "\u09E1"], ["\u09C2", "\u098A"], ["\u09F1", "\u09AD"], ["\u09B9", "\u0999"], ["\u0997", "\u0998"], ["\u09A6", "\u09A7"], ["\u099C", "\u099D"], ["\u09A1", "\u09A2", "\u09DC", "\u09DD"], ["Enter", "Enter"]], + [["Caps", "Caps"], ["\u09CB", "\u0993", "\u09F4", "\u09F5"], ["\u09C7", "\u098F", "\u09F6", "\u09F7"], ["\u09CD", "\u0985", "\u09F8", "\u09F9"], ["\u09BF", "\u0987", "\u09E2", "\u098C"], ["\u09C1", "\u0989"], ["\u09AA", "\u09AB"], ["\u09F0", "", "\u09F0", "\u09F1"], ["\u0995", "\u0996"], ["\u09A4", "\u09A5"], ["\u099A", "\u099B"], ["\u099F", "\u09A0"], ["\u09BC", "\u099E"]], + [["Shift", "Shift"], ["\u09CE", "\u0983"], ["\u0982", "\u0981", "\u09FA"], ["\u09AE", "\u09A3"], ["\u09A8", "\u09F7"], ["\u09AC", "\""], ["\u09B2", "'"], ["\u09B8", "\u09B6"], [",", "\u09B7"], [".", ";"], ["\u09AF", "\u09DF"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["as"] }; + + this.VKI_layout['\u0410\u0437\u04d9\u0440\u0431\u0430\u0458\u04b9\u0430\u043d\u04b9\u0430'] = { + 'name': "Azerbaijani Cyrillic", 'keys': [ + [["`", "~"], ["1", "!"], ["2", '"'], ["3", "\u2116"], ["4", ";"], ["5", "%"], ["6", ":"], ["7", "?"], ["8", "*"], ["9", "("], ["0", ")"], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0458", "\u0408"], ["\u04AF", "\u04AE"], ["\u0443", "\u0423"], ["\u043A", "\u041A"], ["\u0435", "\u0415"], ["\u043D", "\u041D"], ["\u0433", "\u0413"], ["\u0448", "\u0428"], ["\u04BB", "\u04BA"], ["\u0437", "\u0417"], ["\u0445", "\u0425"], ["\u04B9", "\u04B8"], ["\\", "/"]], + [["Caps", "Caps"], ["\u0444", "\u0424"], ["\u044B", "\u042B"], ["\u0432", "\u0412"], ["\u0430", "\u0410"], ["\u043F", "\u041F"], ["\u0440", "\u0420"], ["\u043E", "\u041E"], ["\u043B", "\u041B"], ["\u0434", "\u0414"], ["\u0436", "\u0416"], ["\u049D", "\u049C"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\\", "|"], ["\u04D9", "\u04D8"], ["\u0447", "\u0427"], ["\u0441", "\u0421"], ["\u043C", "\u041C"], ["\u0438", "\u0418"], ["\u0442", "\u0422"], ["\u0493", "\u0492"], ["\u0431", "\u0411"], ["\u04E9", "\u04E8"], [".", ","], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["az-Cyrl"] }; + + this.VKI_layout['Az\u0259rbaycanca'] = { + 'name': "Azerbaijani Latin", 'keys': [ + [["`", "~"], ["1", "!"], ["2", '"'], ["3", "\u2166"], ["4", ";"], ["5", "%"], ["6", ":"], ["7", "?"], ["8", "*"], ["9", "("], ["0", ")"], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["\u00FC", "\u00DC"], ["e", "E"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "\u0130"], ["o", "O"], ["p", "P"], ["\u00F6", "\u00D6"], ["\u011F", "\u011E"], ["\\", "/"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["\u0131", "I"], ["\u0259", "\u018F"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["z", "Z"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M"], ["\u00E7", "\u00C7"], ["\u015F", "\u015E"], [".", ","], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["az"] }; + + this.VKI_layout['\u0411\u0435\u043b\u0430\u0440\u0443\u0441\u043a\u0430\u044f'] = { + 'name': "Belarusian", 'keys': [ + [["\u0451", "\u0401"], ["1", "!"], ["2", '"'], ["3", "\u2116"], ["4", ";"], ["5", "%"], ["6", ":"], ["7", "?"], ["8", "*"], ["9", "("], ["0", ")"], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0439", "\u0419"], ["\u0446", "\u0426"], ["\u0443", "\u0423"], ["\u043a", "\u041a"], ["\u0435", "\u0415"], ["\u043d", "\u041d"], ["\u0433", "\u0413"], ["\u0448", "\u0428"], ["\u045e", "\u040e"], ["\u0437", "\u0417"], ["\u0445", "\u0425"], ["'", "'"], ["\\", "/"]], + [["Caps", "Caps"], ["\u0444", "\u0424"], ["\u044b", "\u042b"], ["\u0432", "\u0412"], ["\u0430", "\u0410"], ["\u043f", "\u041f"], ["\u0440", "\u0420"], ["\u043e", "\u041e"], ["\u043b", "\u041b"], ["\u0434", "\u0414"], ["\u0436", "\u0416"], ["\u044d", "\u042d"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["/", "|"], ["\u044f", "\u042f"], ["\u0447", "\u0427"], ["\u0441", "\u0421"], ["\u043c", "\u041c"], ["\u0456", "\u0406"], ["\u0442", "\u0422"], ["\u044c", "\u042c"], ["\u0431", "\u0411"], ["\u044e", "\u042e"], [".", ","], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["be"] }; + + this.VKI_layout['Belgische / Belge'] = { + 'name': "Belgian", 'keys': [ + [["\u00b2", "\u00b3"], ["&", "1", "|"], ["\u00e9", "2", "@"], ['"', "3", "#"], ["'", "4"], ["(", "5"], ["\u00a7", "6", "^"], ["\u00e8", "7"], ["!", "8"], ["\u00e7", "9", "{"], ["\u00e0", "0", "}"], [")", "\u00b0"], ["-", "_"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["a", "A"], ["z", "Z"], ["e", "E", "\u20ac"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["^", "\u00a8", "["], ["$", "*", "]"], ["\u03bc", "\u00a3", "`"]], + [["Caps", "Caps"], ["q", "Q"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["m", "M"], ["\u00f9", "%", "\u00b4"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">", "\\"], ["w", "W"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], [",", "?"], [";", "."], [":", "/"], ["=", "+", "~"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["nl-BE", "fr-BE"] }; + + this.VKI_layout['\u0411\u044a\u043b\u0433\u0430\u0440\u0441\u043a\u0438 \u0424\u043e\u043d\u0435\u0442\u0438\u0447\u0435\u043d'] = { + 'name': "Bulgarian Phonetic", 'keys': [ + [["\u0447", "\u0427"], ["1", "!"], ["2", "@"], ["3", "#"], ["4", "$"], ["5", "%"], ["6", "^"], ["7", "&"], ["8", "*"], ["9", "("], ["0", ")"], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u044F", "\u042F"], ["\u0432", "\u0412"], ["\u0435", "\u0415"], ["\u0440", "\u0420"], ["\u0442", "\u0422"], ["\u044A", "\u042A"], ["\u0443", "\u0423"], ["\u0438", "\u0418"], ["\u043E", "\u041E"], ["\u043F", "\u041F"], ["\u0448", "\u0428"], ["\u0449", "\u0429"], ["\u044E", "\u042E"]], + [["Caps", "Caps"], ["\u0430", "\u0410"], ["\u0441", "\u0421"], ["\u0434", "\u0414"], ["\u0444", "\u0424"], ["\u0433", "\u0413"], ["\u0445", "\u0425"], ["\u0439", "\u0419"], ["\u043A", "\u041A"], ["\u043B", "\u041B"], [";", ":"], ["'", '"'], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0437", "\u0417"], ["\u044C", "\u042C"], ["\u0446", "\u0426"], ["\u0436", "\u0416"], ["\u0431", "\u0411"], ["\u043D", "\u041D"], ["\u043C", "\u041C"], [",", "<"], [".", ">"], ["/", "?"], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["bg"] }; + + this.VKI_layout['\u0411\u044a\u043b\u0433\u0430\u0440\u0441\u043a\u0438'] = { + 'name': "Bulgarian BDS", 'keys': [ + [["`", "~"], ["1", "!"], ["2", "?"], ["3", "+"], ["4", '"'], ["5", "%"], ["6", "="], ["7", ":"], ["8", "/"], ["9", "_"], ["0", "\u2116"], ["-", "\u0406"], ["=", "V"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], [",", "\u044b"], ["\u0443", "\u0423"], ["\u0435", "\u0415"], ["\u0438", "\u0418"], ["\u0448", "\u0428"], ["\u0449", "\u0429"], ["\u043a", "\u041a"], ["\u0441", "\u0421"], ["\u0434", "\u0414"], ["\u0437", "\u0417"], ["\u0446", "\u0426"], [";", "\u00a7"], ["(", ")"]], + [["Caps", "Caps"], ["\u044c", "\u042c"], ["\u044f", "\u042f"], ["\u0430", "\u0410"], ["\u043e", "\u041e"], ["\u0436", "\u0416"], ["\u0433", "\u0413"], ["\u0442", "\u0422"], ["\u043d", "\u041d"], ["\u0412", "\u0412"], ["\u043c", "\u041c"], ["\u0447", "\u0427"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u042e", "\u044e"], ["\u0439", "\u0419"], ["\u044a", "\u042a"], ["\u044d", "\u042d"], ["\u0444", "\u0424"], ["\u0445", "\u0425"], ["\u043f", "\u041f"], ["\u0440", "\u0420"], ["\u043b", "\u041b"], ["\u0431", "\u0411"], ["Shift", "Shift"]], + [[" ", " "]] + ]}; + + this.VKI_layout['\u09ac\u09be\u0982\u09b2\u09be'] = { + 'name': "Bengali", 'keys': [ + [[""], ["1", "", "\u09E7"], ["2", "", "\u09E8"], ["3", "\u09CD\u09B0", "\u09E9"], ["4", "\u09B0\u09CD", "\u09EA"], ["5", "\u099C\u09CD\u09B0", "\u09EB"], ["6", "\u09A4\u09CD\u09B7", "\u09EC"], ["7", "\u0995\u09CD\u09B0", "\u09ED"], ["8", "\u09B6\u09CD\u09B0", "\u09EE"], ["9", "(", "\u09EF"], ["0", ")", "\u09E6"], ["-", "\u0983"], ["\u09C3", "\u098B", "\u09E2", "\u09E0"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u09CC", "\u0994", "\u09D7"], ["\u09C8", "\u0990"], ["\u09BE", "\u0986"], ["\u09C0", "\u0988", "\u09E3", "\u09E1"], ["\u09C2", "\u098A"], ["\u09AC", "\u09AD"], ["\u09B9", "\u0999"], ["\u0997", "\u0998"], ["\u09A6", "\u09A7"], ["\u099C", "\u099D"], ["\u09A1", "\u09A2", "\u09DC", "\u09DD"], ["Enter", "Enter"]], + [["Caps", "Caps"], ["\u09CB", "\u0993", "\u09F4", "\u09F5"], ["\u09C7", "\u098F", "\u09F6", "\u09F7"], ["\u09CD", "\u0985", "\u09F8", "\u09F9"], ["\u09BF", "\u0987", "\u09E2", "\u098C"], ["\u09C1", "\u0989"], ["\u09AA", "\u09AB"], ["\u09B0", "", "\u09F0", "\u09F1"], ["\u0995", "\u0996"], ["\u09A4", "\u09A5"], ["\u099A", "\u099B"], ["\u099F", "\u09A0"], ["\u09BC", "\u099E"]], + [["Shift", "Shift"], [""], ["\u0982", "\u0981", "\u09FA"], ["\u09AE", "\u09A3"], ["\u09A8"], ["\u09AC"], ["\u09B2"], ["\u09B8", "\u09B6"], [",", "\u09B7"], [".", "{"], ["\u09AF", "\u09DF"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["bn"] }; + + this.VKI_layout['Bosanski'] = { + 'name': "Bosnian", 'keys': [ + [["\u00B8", "\u00A8"], ["1", "!", "~"], ["2", '"', "\u02C7"], ["3", "#", "^"], ["4", "$", "\u02D8"], ["5", "%", "\u00B0"], ["6", "&", "\u02DB"], ["7", "/", "`"], ["8", "(", "\u02D9"], ["9", ")", "\u00B4"], ["0", "=", "\u02DD"], ["'", "?", "\u00A8"], ["+", "*", "\u00B8"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q", "\\"], ["w", "W", "|"], ["e", "E", "\u20AC"], ["r", "R"], ["t", "T"], ["z", "Z"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["\u0161", "\u0160", "\u00F7"], ["\u0111", "\u0110", "\u00D7"], ["\u017E", "\u017D", "\u00A4"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F", "["], ["g", "G", "]"], ["h", "H"], ["j", "J"], ["k", "K", "\u0142"], ["l", "L", "\u0141"], ["\u010D", "\u010C"], ["\u0107", "\u0106", "\u00DF"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">"], ["y", "Y"], ["x", "X"], ["c", "C"], ["v", "V", "@"], ["b", "B", "{"], ["n", "N", "}"], ["m", "M", "\u00A7"], [",", ";", "<"], [".", ":", ">"], ["-", "_", "\u00A9"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["bs"] }; + + this.VKI_layout['Canadienne-fran\u00e7aise'] = { + 'name': "Canadian French", 'keys': [ + [["#", "|", "\\"], ["1", "!", "\u00B1"], ["2", '"', "@"], ["3", "/", "\u00A3"], ["4", "$", "\u00A2"], ["5", "%", "\u00A4"], ["6", "?", "\u00AC"], ["7", "&", "\u00A6"], ["8", "*", "\u00B2"], ["9", "(", "\u00B3"], ["0", ")", "\u00BC"], ["-", "_", "\u00BD"], ["=", "+", "\u00BE"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O", "\u00A7"], ["p", "P", "\u00B6"], ["^", "^", "["], ["\u00B8", "\u00A8", "]"], ["<", ">", "}"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], [";", ":", "~"], ["`", "`", "{"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u00AB", "\u00BB", "\u00B0"], ["z", "Z"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M", "\u00B5"], [",", "'", "\u00AF"], [".", ".", "\u00AD"], ["\u00E9", "\u00C9", "\u00B4"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["fr-CA"] }; + + this.VKI_layout['\u010cesky'] = { + 'name': "Czech", 'keys': [ + [[";", "\u00b0", "`", "~"], ["+", "1", "!"], ["\u011B", "2", "@"], ["\u0161", "3", "#"], ["\u010D", "4", "$"], ["\u0159", "5", "%"], ["\u017E", "6", "^"], ["\u00FD", "7", "&"], ["\u00E1", "8", "*"], ["\u00ED", "9", "("], ["\u00E9", "0", ")"], ["=", "%", "-", "_"], ["\u00B4", "\u02c7", "=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E", "\u20AC"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["\u00FA", "/", "[", "{"], [")", "(", "]", "}"], ["\u00A8", "'", "\\", "|"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["\u016F", '"', ";", ":"], ["\u00A7", "!", "\u00a4", "^"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\\", "|", "", "\u02dd"], ["z", "Z"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M"], [",", "?", "<", "\u00d7"], [".", ":", ">", "\u00f7"], ["-", "_", "/", "?"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["Alt", "Alt"]] + ], 'lang': ["cs"] }; + + this.VKI_layout['Dansk'] = { + 'name': "Danish", 'keys': [ + [["\u00bd", "\u00a7"], ["1", "!"], ["2", '"', "@"], ["3", "#", "\u00a3"], ["4", "\u00a4", "$"], ["5", "%", "\u20ac"], ["6", "&"], ["7", "/", "{"], ["8", "(", "["], ["9", ")", "]"], ["0", "=", "}"], ["+", "?"], ["\u00b4", "`", "|"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E", "\u20ac"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["\u00e5", "\u00c5"], ["\u00a8", "^", "~"], ["'", "*"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["\u00e6", "\u00c6"], ["\u00f8", "\u00d8"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">", "\\"], ["z", "Z"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M", "\u03bc", "\u039c"], [",", ";"], [".", ":"], ["-", "_"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["da"] }; + + this.VKI_layout['Deutsch'] = { + 'name': "German", 'keys': [ + [["^", "\u00b0"], ["1", "!"], ["2", '"', "\u00b2"], ["3", "\u00a7", "\u00b3"], ["4", "$"], ["5", "%"], ["6", "&"], ["7", "/", "{"], ["8", "(", "["], ["9", ")", "]"], ["0", "=", "}"], ["\u00df", "?", "\\"], ["\u00b4", "`"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q", "@"], ["w", "W"], ["e", "E", "\u20ac"], ["r", "R"], ["t", "T"], ["z", "Z"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["\u00fc", "\u00dc"], ["+", "*", "~"], ["#", "'"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["\u00f6", "\u00d6"], ["\u00e4", "\u00c4"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">", "\u00a6"], ["y", "Y"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M", "\u00b5"], [",", ";"], [".", ":"], ["-", "_"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["de"] }; + + this.VKI_layout['Dingbats'] = { + 'name': "Dingbats", 'keys': [ + [["\u2764", "\u2765", "\u2766", "\u2767"], ["\u278a", "\u2780", "\u2776", "\u2768"], ["\u278b", "\u2781", "\u2777", "\u2769"], ["\u278c", "\u2782", "\u2778", "\u276a"], ["\u278d", "\u2783", "\u2779", "\u276b"], ["\u278e", "\u2784", "\u277a", "\u276c"], ["\u278f", "\u2785", "\u277b", "\u276d"], ["\u2790", "\u2786", "\u277c", "\u276e"], ["\u2791", "\u2787", "\u277d", "\u276f"], ["\u2792", "\u2788", "\u277e", "\u2770"], ["\u2793", "\u2789", "\u277f", "\u2771"], ["\u2795", "\u2796", "\u274c", "\u2797"], ["\u2702", "\u2704", "\u2701", "\u2703"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u2714", "\u2705", "\u2713"], ["\u2718", "\u2715", "\u2717", "\u2716"], ["\u271a", "\u2719", "\u271b", "\u271c"], ["\u271d", "\u271e", "\u271f", "\u2720"], ["\u2722", "\u2723", "\u2724", "\u2725"], ["\u2726", "\u2727", "\u2728", "\u2756"], ["\u2729", "\u272a", "\u272d", "\u2730"], ["\u272c", "\u272b", "\u272e", "\u272f"], ["\u2736", "\u2731", "\u2732", "\u2749"], ["\u273b", "\u273c", "\u273d", "\u273e"], ["\u2744", "\u2745", "\u2746", "\u2743"], ["\u2733", "\u2734", "\u2735", "\u2721"], ["\u2737", "\u2738", "\u2739", "\u273a"]], + [["Caps", "Caps"], ["\u2799", "\u279a", "\u2798", "\u2758"], ["\u27b5", "\u27b6", "\u27b4", "\u2759"], ["\u27b8", "\u27b9", "\u27b7", "\u275a"], ["\u2794", "\u279c", "\u27ba", "\u27bb"], ["\u279d", "\u279e", "\u27a1", "\u2772"], ["\u27a9", "\u27aa", "\u27ab", "\u27ac"], ["\u27a4", "\u27a3", "\u27a2", "\u279b"], ["\u27b3", "\u27bc", "\u27bd", "\u2773"], ["\u27ad", "\u27ae", "\u27af", "\u27b1"], ["\u27a8", "\u27a6", "\u27a5", "\u27a7"], ["\u279f", "\u27a0", "\u27be", "\u27b2"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u270c", "\u270b", "\u270a", "\u270d"], ["\u274f", "\u2750", "\u2751", "\u2752"], ["\u273f", "\u2740", "\u2741", "\u2742"], ["\u2747", "\u2748", "\u274a", "\u274b"], ["\u2757", "\u2755", "\u2762", "\u2763"], ["\u2753", "\u2754", "\u27b0", "\u27bf"], ["\u270f", "\u2710", "\u270e", "\u2774"], ["\u2712", "\u2711", "\u274d", "\u274e"], ["\u2709", "\u2706", "\u2708", "\u2707"], ["\u275b", "\u275d", "\u2761", "\u2775"], ["\u275c", "\u275e", "\u275f", "\u2760"], ["Shift", "Shift"]], + [["AltLk", "AltLk"], [" ", " ", " ", " "], ["AltGr", "AltGr"]] + ]}; + + this.VKI_layout['\u078b\u07a8\u0788\u07ac\u0780\u07a8\u0784\u07a6\u0790\u07b0'] = { + 'name': "Divehi", 'keys': [ + [["`", "~"], ["1", "!"], ["2", "@"], ["3", "#"], ["4", "$"], ["5", "%"], ["6", "^"], ["7", "&"], ["8", "*"], ["9", ")"], ["0", "("], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u07ab", "\u00d7"], ["\u07ae", "\u2019"], ["\u07a7", "\u201c"], ["\u07a9", "/"], ["\u07ad", ":"], ["\u078e", "\u07a4"], ["\u0783", "\u079c"], ["\u0789", "\u07a3"], ["\u078c", "\u07a0"], ["\u0780", "\u0799"], ["\u078d", "\u00f7"], ["[", "{"], ["]", "}"]], + [["Caps", "Caps"], ["\u07a8", "<"], ["\u07aa", ">"], ["\u07b0", ".", ",", ","], ["\u07a6", "\u060c"], ["\u07ac", '"'], ["\u0788", "\u07a5"], ["\u0787", "\u07a2"], ["\u0782", "\u0798"], ["\u0786", "\u079a"], ["\u078a", "\u07a1"], ["\ufdf2", "\u061b", ";", ";"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\\", "|"], ["\u0792", "\u0796"], ["\u0791", "\u0795"], ["\u0790", "\u078f"], ["\u0794", "\u0797", "\u200D"], ["\u0785", "\u079f", "\u200C"], ["\u078b", "\u079b", "\u200E"], ["\u0784", "\u079D", "\u200F"], ["\u0781", "\\"], ["\u0793", "\u079e"], ["\u07af", "\u061f"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["dv"] }; + + this.VKI_layout['Dvorak'] = { + 'name': "Dvorak", 'keys': [ + [["`", "~"], ["1", "!"], ["2", "@"], ["3", "#"], ["4", "$"], ["5", "%"], ["6", "^"], ["7", "&"], ["8", "*"], ["9", "("], ["0", ")"], ["[", "{"], ["]", "}"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["'", '"'], [",", "<"], [".", ">"], ["p", "P"], ["y", "Y"], ["f", "F"], ["g", "G"], ["c", "C"], ["r", "R"], ["l", "L"], ["/", "?"], ["=", "+"], ["\\", "|"]], + [["Caps", "Caps"], ["a", "A"], ["o", "O"], ["e", "E"], ["u", "U"], ["i", "I"], ["d", "D"], ["h", "H"], ["t", "T"], ["n", "N"], ["s", "S"], ["-", "_"], ["Enter", "Enter"]], + [["Shift", "Shift"], [";", ":"], ["q", "Q"], ["j", "J"], ["k", "K"], ["x", "X"], ["b", "B"], ["m", "M"], ["w", "W"], ["v", "V"], ["z", "Z"], ["Shift", "Shift"]], + [[" ", " "]] + ]}; + + this.VKI_layout['\u0395\u03bb\u03bb\u03b7\u03bd\u03b9\u03ba\u03ac'] = { + 'name': "Greek", 'keys': [ + [["`", "~"], ["1", "!"], ["2", "@", "\u00b2"], ["3", "#", "\u00b3"], ["4", "$", "\u00a3"], ["5", "%", "\u00a7"], ["6", "^", "\u00b6"], ["7", "&"], ["8", "*", "\u00a4"], ["9", "(", "\u00a6"], ["0", ")", "\u00ba"], ["-", "_", "\u00b1"], ["=", "+", "\u00bd"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], [";", ":"], ["\u03c2", "^"], ["\u03b5", "\u0395"], ["\u03c1", "\u03a1"], ["\u03c4", "\u03a4"], ["\u03c5", "\u03a5"], ["\u03b8", "\u0398"], ["\u03b9", "\u0399"], ["\u03bf", "\u039f"], ["\u03c0", "\u03a0"], ["[", "{", "\u201c"], ["]", "}", "\u201d"], ["\\", "|", "\u00ac"]], + [["Caps", "Caps"], ["\u03b1", "\u0391"], ["\u03c3", "\u03a3"], ["\u03b4", "\u0394"], ["\u03c6", "\u03a6"], ["\u03b3", "\u0393"], ["\u03b7", "\u0397"], ["\u03be", "\u039e"], ["\u03ba", "\u039a"], ["\u03bb", "\u039b"], ["\u0384", "\u00a8", "\u0385"], ["'", '"'], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">"], ["\u03b6", "\u0396"], ["\u03c7", "\u03a7"], ["\u03c8", "\u03a8"], ["\u03c9", "\u03a9"], ["\u03b2", "\u0392"], ["\u03bd", "\u039d"], ["\u03bc", "\u039c"], [",", "<"], [".", ">"], ["/", "?"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["el"] }; + + this.VKI_layout['Eesti'] = { + 'name': "Estonian", 'keys': [ + [["\u02C7", "~"], ["1", "!"], ["2", '"', "@", "@"], ["3", "#", "\u00A3", "\u00A3"], ["4", "\u00A4", "$", "$"], ["5", "%", "\u20AC"], ["6", "&"], ["7", "/", "{", "{"], ["8", "(", "[", "["], ["9", ")", "]", "]"], ["0", "=", "}", "}"], ["+", "?", "\\", "\\"], ["\u00B4", "`"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E", "\u20AC"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["\u00FC", "\u00DC"], ["\u00F5", "\u00D5", "\u00A7", "\u00A7"], ["'", "*", "\u00BD", "\u00BD"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S", "\u0161", "\u0160"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["\u00F6", "\u00D6"], ["\u00E4", "\u00C4", "^", "^"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">", "|", "|"], ["z", "Z", "\u017E", "\u017D"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M"], [",", ";"], [".", ":"], ["-", "_"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["et"] }; + + this.VKI_layout['Espa\u00f1ol'] = { + 'name': "Spanish", 'keys': [ + [["\u00ba", "\u00aa", "\\"], ["1", "!", "|"], ["2", '"', "@"], ["3", "'", "#"], ["4", "$", "~"], ["5", "%", "\u20ac"], ["6", "&", "\u00ac"], ["7", "/"], ["8", "("], ["9", ")"], ["0", "="], ["'", "?"], ["\u00a1", "\u00bf"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["`", "^", "["], ["+", "*", "]"], ["\u00e7", "\u00c7", "}"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["\u00f1", "\u00d1"], ["\u00b4", "\u00a8", "{"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">"], ["z", "Z"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M"], [",", ";"], [".", ":"], ["-", "_"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["es"] }; + + this.VKI_layout['\u062f\u0631\u06cc'] = { + 'name': "Dari", 'keys': [ + [["\u200D", "\u00F7", "~"], ["\u06F1", "!", "`"], ["\u06F2", "\u066C", "@"], ["\u06F3", "\u066B", "#"], ["\u06F4", "\u060B", "$"], ["\u06F5", "\u066A", "%"], ["\u06F6", "\u00D7", "^"], ["\u06F7", "\u060C", "&"], ["\u06F8", "*", "\u2022"], ["\u06F9", ")", "\u200E"], ["\u06F0", "(", "\u200F"], ["-", "\u0640", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0636", "\u0652", "\u00B0"], ["\u0635", "\u064C"], ["\u062B", "\u064D", "\u20AC"], ["\u0642", "\u064B", "\uFD3E"], ["\u0641", "\u064F", "\uFD3F"], ["\u063A", "\u0650", "\u0656"], ["\u0639", "\u064E", "\u0659"], ["\u0647", "\u0651", "\u0655"], ["\u062E", "]", "'"], ["\u062D", "[", '"'], ["\u062C", "}", "\u0681"], ["\u0686", "{", "\u0685"], ["\\", "|", "?"]], + [["Caps", "Caps"], ["\u0634", "\u0624", "\u069A"], ["\u0633", "\u0626", "\u06CD"], ["\u06CC", "\u064A", "\u0649"], ["\u0628", "\u0625", "\u06D0"], ["\u0644", "\u0623", "\u06B7"], ["\u0627", "\u0622", "\u0671"], ["\u062A", "\u0629", "\u067C"], ["\u0646", "\u00BB", "\u06BC"], ["\u0645", "\u00AB", "\u06BA"], ["\u06A9", ":", ";"], ["\u06AF", "\u061B", "\u06AB"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0638", "\u0643", "\u06D2"], ["\u0637", "\u0653", "\u0691"], ["\u0632", "\u0698", "\u0696"], ["\u0631", "\u0670", "\u0693"], ["\u0630", "\u200C", "\u0688"], ["\u062F", "\u0654", "\u0689"], ["\u067E", "\u0621", "\u0679"], ["\u0648", ">", ","], [".", "<", "\u06C7"], ["/", "\u061F", "\u06C9"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["fa-AF"] }; + + this.VKI_layout['\u0641\u0627\u0631\u0633\u06cc'] = { + 'name': "Farsi", 'keys': [ + [["\u067e", "\u0651 "], ["1", "!", "\u00a1", "\u00b9"], ["2", "@", "\u00b2"], ["3", "#", "\u00b3"], ["4", "$", "\u00a4", "\u00a3"], ["5", "%", "\u20ac"], ["6", "^", "\u00bc"], ["7", "&", "\u00bd"], ["8", "*", "\u00be"], ["9", "(", "\u2018"], ["0", ")", "\u2019"], ["-", "_", "\u00a5"], ["=", "+", "\u00d7", "\u00f7"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0636", "\u064e"], ["\u0635", "\u064b"], ["\u062b", "\u064f"], ["\u0642", "\u064c"], ["\u0641", "\u0644"], ["\u063a", "\u0625"], ["\u0639", "\u2018"], ["\u0647", "\u00f7"], ["\u062e", "\u00d7"], ["\u062d", "\u061b"], ["\u062c", "<"], ["\u0686", ">"], ["\u0698", "|"]], + [["Caps", "Caps"], ["\u0634", "\u0650"], ["\u0633", "\u064d"], ["\u064a", "]"], ["\u0628", "["], ["\u0644", "\u0644"], ["\u0627", "\u0623"], ["\u062a", "\u0640"], ["\u0646", "\u060c"], ["\u0645", "\\"], ["\u06af", ":"], ["\u0643", '"'], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0638", "~"], ["\u0637", "\u0652"], ["\u0632", "}"], ["\u0631", "{"], ["\u0630", "\u0644"], ["\u062f", "\u0622"], ["\u0626", "\u0621"], ["\u0648", ","], [".", "."], ["/", "\u061f"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["Alt", "Alt"]] + ], 'lang': ["fa"] }; + + this.VKI_layout['F\u00f8royskt'] = { + 'name': "Faeroese", 'keys': [ + [["\u00BD", "\u00A7"], ["1", "!"], ["2", '"', "@"], ["3", "#", "\u00A3"], ["4", "\u00A4", "$"], ["5", "%", "\u20AC"], ["6", "&"], ["7", "/", "{"], ["8", "(", "["], ["9", ")", "]"], ["0", "=", "}"], ["+", "?"], ["\u00B4", "`", "|"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E", "\u20AC"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["\u00E5", "\u00C5", "\u00A8"], ["\u00F0", "\u00D0", "~"], ["'", "*"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["\u00E6", "\u00C6"], ["\u00F8", "\u00D8", "^"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">", "\\"], ["z", "Z"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M", "\u00B5"], [",", ";"], [".", ":"], ["-", "_"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["fo"] }; + + this.VKI_layout['Fran\u00e7ais'] = { + 'name': "French", 'keys': [ + [["\u00b2", "\u00b3"], ["&", "1"], ["\u00e9", "2", "~"], ['"', "3", "#"], ["'", "4", "{"], ["(", "5", "["], ["-", "6", "|"], ["\u00e8", "7", "`"], ["_", "8", "\\"], ["\u00e7", "9", "^"], ["\u00e0", "0", "@"], [")", "\u00b0", "]"], ["=", "+", "}"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["a", "A"], ["z", "Z"], ["e", "E", "\u20ac"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["^", "\u00a8"], ["$", "\u00a3", "\u00a4"], ["*", "\u03bc"]], + [["Caps", "Caps"], ["q", "Q"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["m", "M"], ["\u00f9", "%"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">"], ["w", "W"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], [",", "?"], [";", "."], [":", "/"], ["!", "\u00a7"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["fr"] }; + + this.VKI_layout['Gaeilge'] = { + 'name': "Irish / Gaelic", 'keys': [ + [["`", "\u00AC", "\u00A6", "\u00A6"], ["1", "!"], ["2", '"'], ["3", "\u00A3"], ["4", "$", "\u20AC"], ["5", "%"], ["6", "^"], ["7", "&"], ["8", "*"], ["9", "("], ["0", ")"], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E", "\u00E9", "\u00C9"], ["r", "R"], ["t", "T"], ["y", "Y", "\u00FD", "\u00DD"], ["u", "U", "\u00FA", "\u00DA"], ["i", "I", "\u00ED", "\u00CD"], ["o", "O", "\u00F3", "\u00D3"], ["p", "P"], ["[", "{"], ["]", "}"], ["#", "~"]], + [["Caps", "Caps"], ["a", "A", "\u00E1", "\u00C1"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], [";", ":"], ["'", "@", "\u00B4", "`"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\\", "|"], ["z", "Z"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M"], [",", "<"], [".", ">"], ["/", "?"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["ga", "gd"] }; + + this.VKI_layout['\u0a97\u0ac1\u0a9c\u0ab0\u0abe\u0aa4\u0ac0'] = { + 'name': "Gujarati", 'keys': [ + [[""], ["1", "\u0A8D", "\u0AE7"], ["2", "\u0AC5", "\u0AE8"], ["3", "\u0ACD\u0AB0", "\u0AE9"], ["4", "\u0AB0\u0ACD", "\u0AEA"], ["5", "\u0A9C\u0ACD\u0A9E", "\u0AEB"], ["6", "\u0AA4\u0ACD\u0AB0", "\u0AEC"], ["7", "\u0A95\u0ACD\u0AB7", "\u0AED"], ["8", "\u0AB6\u0ACD\u0AB0", "\u0AEE"], ["9", "(", "\u0AEF"], ["0", ")", "\u0AE6"], ["-", "\u0A83"], ["\u0AC3", "\u0A8B", "\u0AC4", "\u0AE0"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0ACC", "\u0A94"], ["\u0AC8", "\u0A90"], ["\u0ABE", "\u0A86"], ["\u0AC0", "\u0A88"], ["\u0AC2", "\u0A8A"], ["\u0AAC", "\u0AAD"], ["\u0AB9", "\u0A99"], ["\u0A97", "\u0A98"], ["\u0AA6", "\u0AA7"], ["\u0A9C", "\u0A9D"], ["\u0AA1", "\u0AA2"], ["\u0ABC", "\u0A9E"], ["\u0AC9", "\u0A91"]], + [["Caps", "Caps"], ["\u0ACB", "\u0A93"], ["\u0AC7", "\u0A8F"], ["\u0ACD", "\u0A85"], ["\u0ABF", "\u0A87"], ["\u0AC1", "\u0A89"], ["\u0AAA", "\u0AAB"], ["\u0AB0"], ["\u0A95", "\u0A96"], ["\u0AA4", "\u0AA5"], ["\u0A9A", "\u0A9B"], ["\u0A9F", "\u0AA0"], ["Enter", "Enter"]], + [["Shift", "Shift"], [""], ["\u0A82", "\u0A81", "", "\u0AD0"], ["\u0AAE", "\u0AA3"], ["\u0AA8"], ["\u0AB5"], ["\u0AB2", "\u0AB3"], ["\u0AB8", "\u0AB6"], [",", "\u0AB7"], [".", "\u0964", "\u0965", "\u0ABD"], ["\u0AAF"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["gu"] }; + + this.VKI_layout['\u05e2\u05d1\u05e8\u05d9\u05ea'] = { + 'name': "Hebrew", 'keys': [ + [["~", "`"], ["1", "!"], ["2", "@"], ["3", "#"], ["4" , "$", "\u20aa"], ["5" , "%"], ["6", "^"], ["7", "&"], ["8", "*"], ["9", ")"], ["0", "("], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["/", "Q"], ["'", "W"], ["\u05e7", "E", "\u20ac"], ["\u05e8", "R"], ["\u05d0", "T"], ["\u05d8", "Y"], ["\u05d5", "U", "\u05f0"], ["\u05df", "I"], ["\u05dd", "O"], ["\u05e4", "P"], ["\\", "|"], ["Enter", "Enter"]], + [["Caps", "Caps"], ["\u05e9", "A"], ["\u05d3", "S"], ["\u05d2", "D"], ["\u05db", "F"], ["\u05e2", "G"], ["\u05d9", "H", "\u05f2"], ["\u05d7", "J", "\u05f1"], ["\u05dc", "K"], ["\u05da", "L"], ["\u05e3", ":"], ["," , '"'], ["]", "}"], ["[", "{"]], + [["Shift", "Shift"], ["\u05d6", "Z"], ["\u05e1", "X"], ["\u05d1", "C"], ["\u05d4", "V"], ["\u05e0", "B"], ["\u05de", "N"], ["\u05e6", "M"], ["\u05ea", ">"], ["\u05e5", "<"], [".", "?"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["he"] }; + + this.VKI_layout['\u0926\u0947\u0935\u0928\u093e\u0917\u0930\u0940'] = { + 'name': "Devanagari", 'keys': [ + [["\u094A", "\u0912"], ["1", "\u090D", "\u0967"], ["2", "\u0945", "\u0968"], ["3", "\u094D\u0930", "\u0969"], ["4", "\u0930\u094D", "\u096A"], ["5", "\u091C\u094D\u091E", "\u096B"], ["6", "\u0924\u094D\u0930", "\u096C"], ["7", "\u0915\u094D\u0937", "\u096D"], ["8", "\u0936\u094D\u0930", "\u096E"], ["9", "(", "\u096F"], ["0", ")", "\u0966"], ["-", "\u0903"], ["\u0943", "\u090B", "\u0944", "\u0960"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u094C", "\u0914"], ["\u0948", "\u0910"], ["\u093E", "\u0906"], ["\u0940", "\u0908", "\u0963", "\u0961"], ["\u0942", "\u090A"], ["\u092C", "\u092D"], ["\u0939", "\u0919"], ["\u0917", "\u0918", "\u095A"], ["\u0926", "\u0927"], ["\u091C", "\u091D", "\u095B"], ["\u0921", "\u0922", "\u095C", "\u095D"], ["\u093C", "\u091E"], ["\u0949", "\u0911"]], + [["Caps", "Caps"], ["\u094B", "\u0913"], ["\u0947", "\u090F"], ["\u094D", "\u0905"], ["\u093F", "\u0907", "\u0962", "\u090C"], ["\u0941", "\u0909"], ["\u092A", "\u092B", "", "\u095E"], ["\u0930", "\u0931"], ["\u0915", "\u0916", "\u0958", "\u0959"], ["\u0924", "\u0925"], ["\u091A", "\u091B", "\u0952"], ["\u091F", "\u0920", "", "\u0951"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0946", "\u090E", "\u0953"], ["\u0902", "\u0901", "", "\u0950"], ["\u092E", "\u0923", "\u0954"], ["\u0928", "\u0929"], ["\u0935", "\u0934"], ["\u0932", "\u0933"], ["\u0938", "\u0936"], [",", "\u0937", "\u0970"], [".", "\u0964", "\u0965", "\u093D"], ["\u092F", "\u095F"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["hi-Deva"] }; + + this.VKI_layout['\u0939\u093f\u0902\u0926\u0940'] = { + 'name': "Hindi", 'keys': [ + [["\u200d", "\u200c", "`", "~"], ["1", "\u090D", "\u0967", "!"], ["2", "\u0945", "\u0968", "@"], ["3", "\u094D\u0930", "\u0969", "#"], ["4", "\u0930\u094D", "\u096A", "$"], ["5", "\u091C\u094D\u091E", "\u096B", "%"], ["6", "\u0924\u094D\u0930", "\u096C", "^"], ["7", "\u0915\u094D\u0937", "\u096D", "&"], ["8", "\u0936\u094D\u0930", "\u096E", "*"], ["9", "(", "\u096F", "("], ["0", ")", "\u0966", ")"], ["-", "\u0903", "-", "_"], ["\u0943", "\u090B", "=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u094C", "\u0914"], ["\u0948", "\u0910"], ["\u093E", "\u0906"], ["\u0940", "\u0908"], ["\u0942", "\u090A"], ["\u092C", "\u092D"], ["\u0939", "\u0919"], ["\u0917", "\u0918"], ["\u0926", "\u0927"], ["\u091C", "\u091D"], ["\u0921", "\u0922", "[", "{"], ["\u093C", "\u091E", "]", "}"], ["\u0949", "\u0911", "\\", "|"]], + [["Caps", "Caps"], ["\u094B", "\u0913"], ["\u0947", "\u090F"], ["\u094D", "\u0905"], ["\u093F", "\u0907"], ["\u0941", "\u0909"], ["\u092A", "\u092B"], ["\u0930", "\u0931"], ["\u0915", "\u0916"], ["\u0924", "\u0925"], ["\u091A", "\u091B", ";", ":"], ["\u091F", "\u0920", "'", '"'], ["Enter", "Enter"]], + [["Shift", "Shift"], [""], ["\u0902", "\u0901", "", "\u0950"], ["\u092E", "\u0923"], ["\u0928"], ["\u0935"], ["\u0932", "\u0933"], ["\u0938", "\u0936"], [",", "\u0937", ",", "<"], [".", "\u0964", ".", ">"], ["\u092F", "\u095F", "/", "?"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["hi"] }; + + this.VKI_layout['Hrvatski'] = { + 'name': "Croatian", 'keys': this.VKI_layout['Bosanski'].keys.slice(0), 'lang': ["hr"] + }; + + this.VKI_layout['\u0540\u0561\u0575\u0565\u0580\u0565\u0576 \u0561\u0580\u0565\u0582\u0574\u0578\u0582\u057f\u0584'] = { + 'name': "Western Armenian", 'keys': [ + [["\u055D", "\u055C"], [":", "1"], ["\u0571", "\u0541"], ["\u0575", "\u0545"], ["\u055B", "3"], [",", "4"], ["-", "9"], [".", "\u0587"], ["\u00AB", "("], ["\u00BB", ")"], ["\u0585", "\u0555"], ["\u057C", "\u054C"], ["\u056A", "\u053A"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u056D", "\u053D"], ["\u057E", "\u054E"], ["\u0567", "\u0537"], ["\u0580", "\u0550"], ["\u0564", "\u0534"], ["\u0565", "\u0535"], ["\u0568", "\u0538"], ["\u056B", "\u053B"], ["\u0578", "\u0548"], ["\u0562", "\u0532"], ["\u0579", "\u0549"], ["\u057B", "\u054B"], ["'", "\u055E"]], + [["Caps", "Caps"], ["\u0561", "\u0531"], ["\u057D", "\u054D"], ["\u057F", "\u054F"], ["\u0586", "\u0556"], ["\u056F", "\u053F"], ["\u0570", "\u0540"], ["\u0573", "\u0543"], ["\u0584", "\u0554"], ["\u056C", "\u053C"], ["\u0569", "\u0539"], ["\u0583", "\u0553"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0566", "\u0536"], ["\u0581", "\u0551"], ["\u0563", "\u0533"], ["\u0582", "\u0552"], ["\u057A", "\u054A"], ["\u0576", "\u0546"], ["\u0574", "\u0544"], ["\u0577", "\u0547"], ["\u0572", "\u0542"], ["\u056E", "\u053E"], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["hy-arevmda"] }; + + this.VKI_layout['\u0540\u0561\u0575\u0565\u0580\u0565\u0576 \u0561\u0580\u0565\u0582\u0565\u056c\u0584'] = { + 'name': "Eastern Armenian", 'keys': [ + [["\u055D", "\u055C"], [":", "1"], ["\u0571", "\u0541"], ["\u0575", "\u0545"], ["\u055B", "3"], [",", "4"], ["-", "9"], [".", "\u0587"], ["\u00AB", "("], ["\u00BB", ")"], ["\u0585", "\u0555"], ["\u057C", "\u054C"], ["\u056A", "\u053A"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u056D", "\u053D"], ["\u0582", "\u0552"], ["\u0567", "\u0537"], ["\u0580", "\u0550"], ["\u057F", "\u054F"], ["\u0565", "\u0535"], ["\u0568", "\u0538"], ["\u056B", "\u053B"], ["\u0578", "\u0548"], ["\u057A", "\u054A"], ["\u0579", "\u0549"], ["\u057B", "\u054B"], ["'", "\u055E"]], + [["Caps", "Caps"], ["\u0561", "\u0531"], ["\u057D", "\u054D"], ["\u0564", "\u0534"], ["\u0586", "\u0556"], ["\u0584", "\u0554"], ["\u0570", "\u0540"], ["\u0573", "\u0543"], ["\u056F", "\u053F"], ["\u056C", "\u053C"], ["\u0569", "\u0539"], ["\u0583", "\u0553"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0566", "\u0536"], ["\u0581", "\u0551"], ["\u0563", "\u0533"], ["\u057E", "\u054E"], ["\u0562", "\u0532"], ["\u0576", "\u0546"], ["\u0574", "\u0544"], ["\u0577", "\u0547"], ["\u0572", "\u0542"], ["\u056E", "\u053E"], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["hy"] }; + + this.VKI_layout['\u00cdslenska'] = { + 'name': "Icelandic", 'keys': [ + [["\u00B0", "\u00A8", "\u00B0"], ["1", "!"], ["2", '"'], ["3", "#"], ["4", "$"], ["5", "%", "\u20AC"], ["6", "&"], ["7", "/", "{"], ["8", "(", "["], ["9", ")", "]"], ["0", "=", "}"], ["\u00F6", "\u00D6", "\\"], ["-", "_"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q", "@"], ["w", "W"], ["e", "E", "\u20AC"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["\u00F0", "\u00D0"], ["'", "?", "~"], ["+", "*", "`"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["\u00E6", "\u00C6"], ["\u00B4", "'", "^"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">", "|"], ["z", "Z"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M", "\u00B5"], [",", ";"], [".", ":"], ["\u00FE", "\u00DE"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["is"] }; + + this.VKI_layout['Italiano'] = { + 'name': "Italian", 'keys': [ + [["\\", "|"], ["1", "!"], ["2", '"'], ["3", "\u00a3"], ["4", "$", "\u20ac"], ["5", "%"], ["6", "&"], ["7", "/"], ["8", "("], ["9", ")"], ["0", "="], ["'", "?"], ["\u00ec", "^"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E", "\u20ac"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["\u00e8", "\u00e9", "[", "{"], ["+", "*", "]", "}"], ["\u00f9", "\u00a7"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["\u00f2", "\u00e7", "@"], ["\u00e0", "\u00b0", "#"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">"], ["z", "Z"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M"], [",", ";"], [".", ":"], ["-", "_"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["it"] }; + + this.VKI_layout['\u65e5\u672c\u8a9e'] = { + 'name': "Japanese Hiragana/Katakana", 'keys': [ + [["\uff5e"], ["\u306c", "\u30cc"], ["\u3075", '\u30d5'], ["\u3042", "\u30a2", "\u3041", "\u30a1"], ["\u3046", "\u30a6", "\u3045", "\u30a5"], ["\u3048", "\u30a8", "\u3047", "\u30a7"], ["\u304a", "\u30aa", "\u3049", "\u30a9"], ["\u3084", "\u30e4", "\u3083", "\u30e3"], ["\u3086", "\u30e6", "\u3085", "\u30e5"], ["\u3088", "\u30e8", "\u3087", "\u30e7"], ["\u308f", "\u30ef", "\u3092", "\u30f2"], ["\u307b", "\u30db", "\u30fc", "\uff1d"], ["\u3078", "\u30d8" , "\uff3e", "\uff5e"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u305f", "\u30bf"], ["\u3066", "\u30c6"], ["\u3044", "\u30a4", "\u3043", "\u30a3"], ["\u3059", "\u30b9"], ["\u304b", "\u30ab"], ["\u3093", "\u30f3"], ["\u306a", "\u30ca"], ["\u306b", "\u30cb"], ["\u3089", "\u30e9"], ["\u305b", "\u30bb"], ["\u3001", "\u3001", "\uff20", "\u2018"], ["\u3002", "\u3002", "\u300c", "\uff5b"], ["\uffe5", "", "", "\uff0a"], ['\u309B', '"', "\uffe5", "\uff5c"]], + [["Caps", "Caps"], ["\u3061", "\u30c1"], ["\u3068", "\u30c8"], ["\u3057", "\u30b7"], ["\u306f", "\u30cf"], ["\u304d", "\u30ad"], ["\u304f", "\u30af"], ["\u307e", "\u30de"], ["\u306e", "\u30ce"], ["\u308c", "\u30ec", "\uff1b", "\uff0b"], ["\u3051", "\u30b1", "\uff1a", "\u30f6"], ["\u3080", "\u30e0", "\u300d", "\uff5d"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u3064", "\u30c4"], ["\u3055", "\u30b5"], ["\u305d", "\u30bd"], ["\u3072", "\u30d2"], ["\u3053", "\u30b3"], ["\u307f", "\u30df"], ["\u3082", "\u30e2"], ["\u306d", "\u30cd", "\u3001", "\uff1c"], ["\u308b", "\u30eb", "\u3002", "\uff1e"], ["\u3081", "\u30e1", "\u30fb", "\uff1f"], ["\u308d", "\u30ed", "", "\uff3f"], ["Shift", "Shift"]], + [["AltLk", "AltLk"], [" ", " ", " ", " "], ["Alt", "Alt"]] + ], 'lang': ["ja"] }; + + this.VKI_layout['\u10e5\u10d0\u10e0\u10d7\u10e3\u10da\u10d8'] = { + 'name': "Georgian", 'keys': [ + [["\u201E", "\u201C"], ["!", "1"], ["?", "2"], ["\u2116", "3"], ["\u00A7", "4"], ["%", "5"], [":", "6"], [".", "7"], [";", "8"], [",", "9"], ["/", "0"], ["\u2013", "-"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u10E6", "\u10E6"], ["\u10EF", "\u10EF"], ["\u10E3", "\u10E3"], ["\u10D9", "\u10D9"], ["\u10D4", "\u10D4", "\u10F1"], ["\u10DC", "\u10DC"], ["\u10D2", "\u10D2"], ["\u10E8", "\u10E8"], ["\u10EC", "\u10EC"], ["\u10D6", "\u10D6"], ["\u10EE", "\u10EE", "\u10F4"], ["\u10EA", "\u10EA"], ["(", ")"]], + [["Caps", "Caps"], ["\u10E4", "\u10E4", "\u10F6"], ["\u10EB", "\u10EB"], ["\u10D5", "\u10D5", "\u10F3"], ["\u10D7", "\u10D7"], ["\u10D0", "\u10D0"], ["\u10DE", "\u10DE"], ["\u10E0", "\u10E0"], ["\u10DD", "\u10DD"], ["\u10DA", "\u10DA"], ["\u10D3", "\u10D3"], ["\u10DF", "\u10DF"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u10ED", "\u10ED"], ["\u10E9", "\u10E9"], ["\u10E7", "\u10E7"], ["\u10E1", "\u10E1"], ["\u10DB", "\u10DB"], ["\u10D8", "\u10D8", "\u10F2"], ["\u10E2", "\u10E2"], ["\u10E5", "\u10E5"], ["\u10D1", "\u10D1"], ["\u10F0", "\u10F0", "\u10F5"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["ka"] }; + + this.VKI_layout['\u049a\u0430\u0437\u0430\u049b\u0448\u0430'] = { + 'name': "Kazakh", 'keys': [ + [["(", ")"], ['"', "!"], ["\u04d9", "\u04d8"], ["\u0456", "\u0406"], ["\u04a3", "\u04a2"], ["\u0493", "\u0492"], [",", ";"], [".", ":"], ["\u04af", "\u04ae"], ["\u04b1", "\u04b0"], ["\u049b", "\u049a"], ["\u04e9", "\u04e8"], ["\u04bb", "\u04ba"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0439", "\u0419"], ["\u0446", "\u0426"], ["\u0443", "\u0423"], ["\u043A", "\u041A"], ["\u0435", "\u0415"], ["\u043D", "\u041D"], ["\u0433", "\u0413"], ["\u0448", "\u0428"], ["\u0449", "\u0429"], ["\u0437", "\u0417"], ["\u0445", "\u0425"], ["\u044A", "\u042A"], ["\\", "/"]], + [["Caps", "Caps"], ["\u0444", "\u0424"], ["\u044B", "\u042B"], ["\u0432", "\u0412"], ["\u0430", "\u0410"], ["\u043F", "\u041F"], ["\u0440", "\u0420"], ["\u043E", "\u041E"], ["\u043B", "\u041B"], ["\u0434", "\u0414"], ["\u0436", "\u0416"], ["\u044D", "\u042D"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\\", "|"], ["\u044F", "\u042F"], ["\u0447", "\u0427"], ["\u0441", "\u0421"], ["\u043C", "\u041C"], ["\u0438", "\u0418"], ["\u0442", "\u0422"], ["\u044C", "\u042C"], ["\u0431", "\u0411"], ["\u044E", "\u042E"], ["\u2116", "?"], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["kk"] }; + + this.VKI_layout['\u1797\u17b6\u179f\u17b6\u1781\u17d2\u1798\u17c2\u179a'] = { + 'name': "Khmer", 'keys': [ + [["\u00AB", "\u00BB","\u200D"], ["\u17E1", "!","\u200C","\u17F1"], ["\u17E2", "\u17D7", "@", "\u17F2"], ["\u17E3", '"', "\u17D1", "\u17F3"], ["\u17E4", "\u17DB", "$", "\u17F4"], ["\u17E5", "%" ,"\u20AC", "\u17F5"], ["\u17E6", "\u17CD", "\u17D9", "\u17F6"], ["\u17E7", "\u17D0", "\u17DA", "\u17F7"], ["\u17E8", "\u17CF", "*", "\u17F8"], ["\u17E9", "(", "{", "\u17F9"], ["\u17E0", ")", "}", "\u17F0"], ["\u17A5", "\u17CC", "x"], ["\u17B2", "=", "\u17CE"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u1786", "\u1788", "\u17DC", "\u19E0"], ["\u17B9", "\u17BA", "\u17DD", "\u19E1"], ["\u17C1", "\u17C2", "\u17AF", "\u19E2"], ["\u179A", "\u17AC", "\u17AB", "\u19E3"], ["\u178F", "\u1791", "\u17A8", "\u19E4"], ["\u1799", "\u17BD", "\u1799\u17BE\u1784", "\u19E5"], ["\u17BB", "\u17BC", "", "\u19E6"], ["\u17B7", "\u17B8", "\u17A6", "\u19E7"], ["\u17C4", "\u17C5", "\u17B1", "\u19E8"], ["\u1795", "\u1797", "\u17B0", "\u19E9"], ["\u17C0", "\u17BF", "\u17A9", "\u19EA"], ["\u17AA", "\u17A7", "\u17B3", "\u19EB"], ["\u17AE", "\u17AD", "\\"]], + [["Caps", "Caps"], ["\u17B6", "\u17B6\u17C6", "\u17B5", "\u19EC"], ["\u179F", "\u17C3", "", "\u19ED"], ["\u178A", "\u178C", "\u17D3", "\u19EE"], ["\u1790", "\u1792", "", "\u19EF"], ["\u1784", "\u17A2", "\u17A4", "\u19F0"], ["\u17A0", "\u17C7", "\u17A3", "\u19F1"], ["\u17D2", "\u1789", "\u17B4", "\u19F2"], ["\u1780", "\u1782", "\u179D", "\u19F3"], ["\u179B", "\u17A1", "\u17D8", "\u19F4"], ["\u17BE", "\u17C4\u17C7", "\u17D6", "\u19F5"], ["\u17CB", "\u17C9", "\u17C8", "\u19F6"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u178B", "\u178D", "|", "\u19F7"], ["\u1781", "\u1783", "\u1781\u17D2\u1789\u17BB\u17C6", "\u19F8"], ["\u1785", "\u1787", "-", "\u19F9"], ["\u179C", "\u17C1\u17C7", "+", "\u19FA"], ["\u1794", "\u1796", "\u179E", "\u19FB"], ["\u1793", "\u178E", "[", "\u19FC"], ["\u1798", "\u17C6", "]", "\u19FD"], ["\u17BB\u17C6", "\u17BB\u17C7", ",", "\u19FE"], ["\u17D4", "\u17D5", ".", "\u19FF"], ["\u17CA", "?", "/"], ["Shift", "Shift"]], + [["\u200B", " ", "\u00A0", " "], ["AltGr", "AltGr"]] + ], 'lang': ["km"] }; + + this.VKI_layout['\u0c95\u0ca8\u0ccd\u0ca8\u0ca1'] = { + 'name': "Kannada", 'keys': [ + [["\u0CCA", "\u0C92"], ["1", "", "\u0CE7"], ["2", "", "\u0CE8"], ["3", "\u0CCD\u0CB0", "\u0CE9"], ["4", "\u0CB0\u0CCD", "\u0CEA"], ["5", "\u0C9C\u0CCD\u0C9E", "\u0CEB"], ["6", "\u0CA4\u0CCD\u0CB0", "\u0CEC"], ["7", "\u0C95\u0CCD\u0CB7", "\u0CED"], ["8", "\u0CB6\u0CCD\u0CB0", "\u0CEE"], ["9", "(", "\u0CEF"], ["0", ")", "\u0CE6"], ["-", "\u0C83"], ["\u0CC3", "\u0C8B", "\u0CC4", "\u0CE0"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0CCC", "\u0C94"], ["\u0CC8", "\u0C90", "\u0CD6"], ["\u0CBE", "\u0C86"], ["\u0CC0", "\u0C88", "", "\u0CE1"], ["\u0CC2", "\u0C8A"], ["\u0CAC", "\u0CAD"], ["\u0CB9", "\u0C99"], ["\u0C97", "\u0C98"], ["\u0CA6", "\u0CA7"], ["\u0C9C", "\u0C9D"], ["\u0CA1", "\u0CA2"], ["Enter", "Enter"]], + [["Caps", "Caps"], ["\u0CCB", "\u0C93"], ["\u0CC7", "\u0C8F", "\u0CD5"], ["\u0CCD", "\u0C85"], ["\u0CBF", "\u0C87", "", "\u0C8C"], ["\u0CC1", "\u0C89"], ["\u0CAA", "\u0CAB", "", "\u0CDE"], ["\u0CB0", "\u0CB1"], ["\u0C95", "\u0C96"], ["\u0CA4", "\u0CA5"], ["\u0C9A", "\u0C9B"], ["\u0C9F", "\u0CA0"], ["", "\u0C9E"]], + [["Shift", "Shift"], ["\u0CC6", "\u0C8F"], ["\u0C82"], ["\u0CAE", "\u0CA3"], ["\u0CA8"], ["\u0CB5"], ["\u0CB2", "\u0CB3"], ["\u0CB8", "\u0CB6"], [",", "\u0CB7"], [".", "|"], ["\u0CAF"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["kn"] }; + + this.VKI_layout['\ud55c\uad6d\uc5b4'] = { + 'name': "Korean", 'keys': [ + [["`", "~", "`", "~"], ["1", "!", "1", "!"], ["2", "@", "2", "@"], ["3", "#", "3", "#"], ["4", "$", "4", "$"], ["5", "%", "5", "%"], ["6", "^", "6", "^"], ["7", "&", "7", "&"], ["8", "*", "8", "*"], ["9", ")", "9", ")"], ["0", "(", "0", "("], ["-", "_", "-", "_"], ["=", "+", "=", "+"], ["\u20A9", "|", "\u20A9", "|"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u1107", "\u1108", "q", "Q"], ["\u110C", "\u110D", "w", "W"], ["\u1103", "\u1104", "e", "E"], ["\u1100", "\u1101", "r", "R"], ["\u1109", "\u110A", "t", "T"], ["\u116D", "", "y", "Y"], ["\u1167", "", "u", "U"], ["\u1163", "", "i", "I"], ["\u1162", "\u1164", "o", "O"], ["\u1166", "\u1168", "p", "P"], ["[", "{", "[", "{"], ["]", "}", "]", "}"]], + [["Caps", "Caps"], ["\u1106", "", "a", "A"], ["\u1102", "", "s", "S"], ["\u110B", "", "d", "D"], ["\u1105", "", "f", "F"], ["\u1112", "", "g", "G"], ["\u1169", "", "h", "H"], ["\u1165", "", "j", "J"], ["\u1161", "", "k", "K"], ["\u1175", "", "l", "L"], [";", ":", ";", ":"], ["'", '"', "'", '"'], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u110F", "", "z", "Z"], ["\u1110", "", "x", "X"], ["\u110E", "", "c", "C"], ["\u1111", "", "v", "V"], ["\u1172", "", "b", "B"], ["\u116E", "", "n", "N"], ["\u1173", "", "m", "M"], [",", "<", ",", "<"], [".", ">", ".", ">"], ["/", "?", "/", "?"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["Kor", "Alt"]] + ], 'lang': ["ko"] }; + + this.VKI_layout['Kurd\u00ee'] = { + 'name': "Kurdish", 'keys': [ + [["\u20ac", "~"], ["\u0661", "!"], ["\u0662", "@"], ["\u0663", "#"], ["\u0664", "$"], ["\u0665", "%"], ["\u0666", "^"], ["\u0667", "&"], ["\u0668", "*"], ["\u0669", "("], ["\u0660", ")"], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0642", "`"], ["\u0648", "\u0648\u0648"], ["\u06d5", "\u064a"], ["\u0631", "\u0695"], ["\u062a", "\u0637"], ["\u06cc", "\u06ce"], ["\u0626", "\u0621"], ["\u062d", "\u0639"], ["\u06c6", "\u0624"], ["\u067e", "\u062b"], ["[", "{"], ["]", "}"], ["\\", "|"]], + [["Caps", "Caps"], ["\u0627", "\u0622"], ["\u0633", "\u0634"], ["\u062f", "\u0630"], ["\u0641", "\u0625"], ["\u06af", "\u063a"], ["\u0647", "\u200c"], ["\u0698", "\u0623"], ["\u06a9", "\u0643"], ["\u0644", "\u06b5"], ["\u061b", ":"], ["'", '"'], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0632", "\u0636"], ["\u062e", "\u0635"], ["\u062c", "\u0686"], ["\u06a4", "\u0638"], ["\u0628", "\u0649"], ["\u0646", "\u0629"], ["\u0645", "\u0640"], ["\u060c", "<"], [".", ">"], ["/", "\u061f"], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["ku"] }; + + this.VKI_layout['\u041a\u044b\u0440\u0433\u044b\u0437\u0447\u0430'] = { + 'name': "Kyrgyz", 'keys': [ + [["\u0451", "\u0401"], ["1", "!"], ["2", '"'], ["3", "\u2116"], ["4", ";"], ["5", "%"], ["6", ":"], ["7", "?"], ["8", "*"], ["9", "("], ["0", ")"], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0439", "\u0419"], ["\u0446", "\u0426"], ["\u0443", "\u0423", "\u04AF", "\u04AE"], ["\u043A", "\u041A"], ["\u0435", "\u0415"], ["\u043D", "\u041D", "\u04A3", "\u04A2"], ["\u0433", "\u0413"], ["\u0448", "\u0428"], ["\u0449", "\u0429"], ["\u0437", "\u0417"], ["\u0445", "\u0425"], ["\u044A", "\u042A"], ["\\", "/"]], + [["Caps", "Caps"], ["\u0444", "\u0424"], ["\u044B", "\u042B"], ["\u0432", "\u0412"], ["\u0430", "\u0410"], ["\u043F", "\u041F"], ["\u0440", "\u0420"], ["\u043E", "\u041E", "\u04E9", "\u04E8"], ["\u043B", "\u041B"], ["\u0434", "\u0414"], ["\u0436", "\u0416"], ["\u044D", "\u042D"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u044F", "\u042F"], ["\u0447", "\u0427"], ["\u0441", "\u0421"], ["\u043C", "\u041C"], ["\u0438", "\u0418"], ["\u0442", "\u0422"], ["\u044C", "\u042C"], ["\u0431", "\u0411"], ["\u044E", "\u042E"], [".", ","], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["ky"] }; + + this.VKI_layout['Latvie\u0161u'] = { + 'name': "Latvian", 'keys': [ + [["\u00AD", "?"], ["1", "!", "\u00AB"], ["2", "\u00AB", "", "@"], ["3", "\u00BB", "", "#"], ["4", "$", "\u20AC", "$"], ["5", "%", '"', "~"], ["6", "/", "\u2019", "^"], ["7", "&", "", "\u00B1"], ["8", "\u00D7", ":"], ["9", "("], ["0", ")"], ["-", "_", "\u2013", "\u2014"], ["f", "F", "=", ";"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u016B", "\u016A", "q", "Q"], ["g", "G", "\u0123", "\u0122"], ["j", "J"], ["r", "R", "\u0157", "\u0156"], ["m", "M", "w", "W"], ["v", "V", "y", "Y"], ["n", "N"], ["z", "Z"], ["\u0113", "\u0112"], ["\u010D", "\u010C"], ["\u017E", "\u017D", "[", "{"], ["h", "H", "]", "}"], ["\u0137", "\u0136"]], + [["Caps", "Caps"], ["\u0161", "\u0160"], ["u", "U"], ["s", "S"], ["i", "I"], ["l", "L"], ["d", "D"], ["a", "A"], ["t", "T"], ["e", "E", "\u20AC"], ["c", "C"], ["\u00B4", "\u00B0", "\u00B4", "\u00A8"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0146", "\u0145"], ["b", "B", "x", "X"], ["\u012B", "\u012A"], ["k", "K", "\u0137", "\u0136"], ["p", "P"], ["o", "O", "\u00F5", "\u00D5"], ["\u0101", "\u0100"], [",", ";", "<"], [".", ":", ">"], ["\u013C", "\u013B"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["lv"] }; + + this.VKI_layout['Lietuvi\u0173'] = { + 'name': "Lithuanian", 'keys': [ + [["`", "~"], ["\u0105", "\u0104"], ["\u010D", "\u010C"], ["\u0119", "\u0118"], ["\u0117", "\u0116"], ["\u012F", "\u012E"], ["\u0161", "\u0160"], ["\u0173", "\u0172"], ["\u016B", "\u016A"], ["\u201E", "("], ["\u201C", ")"], ["-", "_"], ["\u017E", "\u017D"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["[", "{"], ["]", "}"], ["\\", "|"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], [";", ":"], ["'", '"'], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u2013", "\u20AC"], ["z", "Z"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M"], [",", "<"], [".", ">"], ["/", "?"], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["lt"] }; + + this.VKI_layout['Magyar'] = { + 'name': "Hungarian", 'keys': [ + [["0", "\u00a7"], ["1", "'", "~"], ["2", '"', "\u02c7"], ["3", "+", "\u02c6"], ["4", "!", "\u02d8"], ["5", "%", "\u00b0"], ["6", "/", "\u02db"], ["7", "=", "`"], ["8", "(", "\u02d9"], ["9", ")", "\u00b4"], ["\u00f6", "\u00d6", "\u02dd"], ["\u00fc", "\u00dc", "\u00a8"], ["\u00f3", "\u00d3", "\u00b8"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q", "\\"], ["w", "W", "|"], ["e", "E", "\u00c4"], ["r", "R"], ["t", "T"], ["z", "Z"], ["u", "U", "\u20ac"], ["i", "I", "\u00cd"], ["o", "O"], ["p", "P"], ["\u0151", "\u0150", "\u00f7"], ["\u00fa", "\u00da", "\u00d7"], ["\u0171", "\u0170", "\u00a4"]], + [["Caps", "Caps"], ["a", "A", "\u00e4"], ["s", "S", "\u0111"], ["d", "D", "\u0110"], ["f", "F", "["], ["g", "G", "]"], ["h", "H"], ["j", "J", "\u00ed"], ["k", "K", "\u0141"], ["l", "L", "\u0142"], ["\u00e9", "\u00c9", "$"], ["\u00e1", "\u00c1", "\u00df"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u00ed", "\u00cd", "<"], ["y", "Y", ">"], ["x", "X", "#"], ["c", "C", "&"], ["v", "V", "@"], ["b", "B", "{"], ["n", "N", "}"], ["m", "M", "<"], [",", "?", ";"], [".", ":", ">"], ["-", "_", "*"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["hu"] }; + + this.VKI_layout['Malti'] = { + 'name': "Maltese 48", 'keys': [ + [["\u010B", "\u010A", "`"], ["1", "!"], ["2", '"'], ["3", "\u20ac", "\u00A3"], ["4", "$"], ["5", "%"], ["6", "^"], ["7", "&"], ["8", "*"], ["9", "("], ["0", ")"], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E", "\u00E8", "\u00C8"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U", "\u00F9", "\u00D9"], ["i", "I", "\u00EC", "\u00cc"], ["o", "O", "\u00F2", "\u00D2"], ["p", "P"], ["\u0121", "\u0120", "[", "{"], ["\u0127", "\u0126", "]", "}"], ["#", "\u017e"]], + [["Caps", "Caps"], ["a", "A", "\u00E0", "\u00C0"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], [";", ":"], ["'", "@"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u017c", "\u017b", "\\", "|"], ["z", "Z"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M"], [",", "<"], [".", ">"], ["/", "?", "", "`"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["mt"] }; + + this.VKI_layout['\u041c\u0430\u043a\u0435\u0434\u043e\u043d\u0441\u043a\u0438'] = { + 'name': "Macedonian Cyrillic", 'keys': [ + [["`", "~"], ["1", "!"], ["2", "\u201E"], ["3", "\u201C"], ["4", "\u2019"], ["5", "%"], ["6", "\u2018"], ["7", "&"], ["8", "*"], ["9", "("], ["0", ")"], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0459", "\u0409"], ["\u045A", "\u040A"], ["\u0435", "\u0415", "\u20AC"], ["\u0440", "\u0420"], ["\u0442", "\u0422"], ["\u0455", "\u0405"], ["\u0443", "\u0423"], ["\u0438", "\u0418"], ["\u043E", "\u041E"], ["\u043F", "\u041F"], ["\u0448", "\u0428", "\u0402"], ["\u0453", "\u0403", "\u0452"], ["\u0436", "\u0416"]], + [["Caps", "Caps"], ["\u0430", "\u0410"], ["\u0441", "\u0421"], ["\u0434", "\u0414"], ["\u0444", "\u0424", "["], ["\u0433", "\u0413", "]"], ["\u0445", "\u0425"], ["\u0458", "\u0408"], ["\u043A", "\u041A"], ["\u043B", "\u041B"], ["\u0447", "\u0427", "\u040B"], ["\u045C", "\u040C", "\u045B"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0451", "\u0401"], ["\u0437", "\u0417"], ["\u045F", "\u040F"], ["\u0446", "\u0426"], ["\u0432", "\u0412", "@"], ["\u0431", "\u0411", "{"], ["\u043D", "\u041D", "}"], ["\u043C", "\u041C", "\u00A7"], [",", ";"], [".", ":"], ["/", "?"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["mk"] }; + + this.VKI_layout['\u0d2e\u0d32\u0d2f\u0d3e\u0d33\u0d02'] = { + 'name': "Malayalam", 'keys': [ + [["\u0D4A", "\u0D12"], ["1", "", "\u0D67"], ["2", "", "\u0D68"], ["3", "\u0D4D\u0D30", "\u0D69"], ["4", "", "\u0D6A"], ["5", "", "\u0D6B"], ["6", "", "\u0D6C"], ["7", "\u0D15\u0D4D\u0D37", "\u0D6D"], ["8", "", "\u0D6E"], ["9", "(", "\u0D6F"], ["0", ")", "\u0D66"], ["-", "\u0D03"], ["\u0D43", "\u0D0B", "", "\u0D60"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0D4C", "\u0D14", "\u0D57"], ["\u0D48", "\u0D10"], ["\u0D3E", "\u0D06"], ["\u0D40", "\u0D08", "", "\u0D61"], ["\u0D42", "\u0D0A"], ["\u0D2C", "\u0D2D"], ["\u0D39", "\u0D19"], ["\u0D17", "\u0D18"], ["\u0D26", "\u0D27"], ["\u0D1C", "\u0D1D"], ["\u0D21", "\u0D22"], ["", "\u0D1E"]], + [["Caps", "Caps"], ["\u0D4B", "\u0D13"], ["\u0D47", "\u0D0F"], ["\u0D4D", "\u0D05", "", "\u0D0C"], ["\u0D3F", "\u0D07"], ["\u0D41", "\u0D09"], ["\u0D2A", "\u0D2B"], ["\u0D30", "\u0D31"], ["\u0D15", "\u0D16"], ["\u0D24", "\u0D25"], ["\u0D1A", "\u0D1B"], ["\u0D1F", "\u0D20"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0D46", "\u0D0F"], ["\u0D02"], ["\u0D2E", "\u0D23"], ["\u0D28"], ["\u0D35", "\u0D34"], ["\u0D32", "\u0D33"], ["\u0D38", "\u0D36"], [",", "\u0D37"], ["."], ["\u0D2F"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["ml"] }; + + this.VKI_layout['Misc. Symbols'] = { + 'name': "Misc. Symbols", 'keys': [ + [["\u2605", "\u2606", "\u260e", "\u260f"], ["\u2648", "\u2673", "\u2659", "\u2630"], ["\u2649", "\u2674", "\u2658", "\u2631"], ["\u264a", "\u2675", "\u2657", "\u2632"], ["\u264b", "\u2676", "\u2656", "\u2633"], ["\u264c", "\u2677", "\u2655", "\u2634"], ["\u264d", "\u2678", "\u2654", "\u2635"], ["\u264e", "\u2679", "\u265f", "\u2636"], ["\u264f", "\u267a", "\u265e", "\u2637"], ["\u2650", "\u267b", "\u265d", "\u2686"], ["\u2651", "\u267c", "\u265c", "\u2687"], ["\u2652", "\u267d", "\u265b", "\u2688"], ["\u2653", "\u2672", "\u265a", "\u2689"], ["Bksp", "Bksp"]], + [["\u263f", "\u2680", "\u268a", "\u26a2"], ["\u2640", "\u2681", "\u268b", "\u26a3"], ["\u2641", "\u2682", "\u268c", "\u26a4"], ["\u2642", "\u2683", "\u268d", "\u26a5"], ["\u2643", "\u2684", "\u268e", "\u26a6"], ["\u2644", "\u2685", "\u268f", "\u26a7"], ["\u2645", "\u2620", "\u26ff", "\u26a8"], ["\u2646", "\u2622", "\u2692", "\u26a9"], ["\u2647", "\u2623", "\u2693", "\u26b2"], ["\u2669", "\u266d", "\u2694", "\u26ac"], ["\u266a", "\u266e", "\u2695", "\u26ad"], ["\u266b", "\u266f", "\u2696", "\u26ae"], ["\u266c", "\u2607", "\u2697", "\u26af"], ["\u26f9", "\u2608", "\u2698", "\u26b0"], ["\u267f", "\u262e", "\u2638", "\u2609"]], + [["Tab", "Tab"], ["\u261e", "\u261c", "\u261d", "\u261f"], ["\u261b", "\u261a", "\u2618", "\u2619"], ["\u2602", "\u2614", "\u26f1", "\u26d9"], ["\u2615", "\u2668", "\u26fe", "\u26d8"], ["\u263a", "\u2639", "\u263b", "\u26dc"], ["\u2617", "\u2616", "\u26ca", "\u26c9"], ["\u2660", "\u2663", "\u2665", "\u2666"], ["\u2664", "\u2667", "\u2661", "\u2662"], ["\u26c2", "\u26c0", "\u26c3", "\u26c1"], ["\u2624", "\u2625", "\u269a", "\u26b1"], ["\u2610", "\u2611", "\u2612", "\u2613"], ["\u2628", "\u2626", "\u2627", "\u2629"], ["\u262a", "\u262b", "\u262c", "\u262d"], ["\u26fa", "\u26fb", "\u26fc", "\u26fd"]], + [["Caps", "Caps"], ["\u262f", "\u2670", "\u2671", "\u267e"], ["\u263c", "\u2699", "\u263d", "\u263e"], ["\u26c4", "\u2603", "\u26c7", "\u26c6"], ["\u26a0", "\u26a1", "\u2621", "\u26d4"], ["\u26e4", "\u26e5", "\u26e6", "\u26e7"], ["\u260a", "\u260b", "\u260c", "\u260d"], ["\u269c", "\u269b", "\u269d", "\u2604"], ["\u26b3", "\u26b4", "\u26b5", "\u26b6"], ["\u26b7", "\u26bf", "\u26b8", "\u26f8"], ["\u26b9", "\u26ba", "\u26bb", "\u26bc"], ["\u26bd", "\u26be", "\u269f", "\u269e"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u2600", "\u2601", "\u26c5", "\u26c8"], ["\u2691", "\u2690", "\u26ab", "\u26aa"], ["\u26cb", "\u26cc", "\u26cd", "\u26ce"], ["\u26cf", "\u26d0", "\u26d1", "\u26d2"], ["\u26d3", "\u26d5", "\u26d6", "\u26d7"], ["\u26da", "\u26db", "\u26dd", "\u26de"], ["\u26df", "\u26e0", "\u26e1", "\u26e2"], ["\u26e3", "\u26e8", "\u26e9", "\u26ea"], ["\u26eb", "\u26ec", "\u26ed", "\u26ee"], ["\u26ef", "\u26f0", "\u26f2", "\u26f3"], ["\u26f4", "\u26f5", "\u26f6", "\u26f7"], ["Shift", "Shift"]], + [["AltLk", "AltLk"], [" ", " ", " ", " "], ["Alt", "Alt"]] + ]}; + + this.VKI_layout['\u041c\u043e\u043d\u0433\u043e\u043b'] = { + 'name': "Mongolian Cyrillic", 'keys': [ + [["=", "+"], ["\u2116", "1"], ["-", "2"], ['"', "3"], ["\u20AE", "4"], [":", "5"], [".", "6"], ["_", "7"], [",", "8"], ["%", "9"], ["?", "0"], ["\u0435", "\u0415"], ["\u0449", "\u0429"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0444", "\u0424"], ["\u0446", "\u0426"], ["\u0443", "\u0423"], ["\u0436", "\u0416"], ["\u044d", "\u042d"], ["\u043D", "\u041D"], ["\u0433", "\u0413"], ["\u0448", "\u0428"], ["\u04af", "\u04AE"], ["\u0437", "\u0417"], ["\u043A", "\u041a"], ["\u044A", "\u042A"], ["\\", "|"]], + [["Caps", "Caps"], ["\u0439", "\u0419"], ["\u044B", "\u042B"], ["\u0431", "\u0411"], ["\u04e9", "\u04e8"], ["\u0430", "\u0410"], ["\u0445", "\u0425"], ["\u0440", "\u0420"], ["\u043e", "\u041e"], ["\u043B", "\u041b"], ["\u0434", "\u0414"], ["\u043f", "\u041f"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u044F", "\u042F"], ["\u0447", "\u0427"], ["\u0451", "\u0401"], ["\u0441", "\u0421"], ["\u043c", "\u041c"], ["\u0438", "\u0418"], ["\u0442", "\u0422"], ["\u044c", "\u042c"], ["\u0432", "\u0412"], ["\u044e", "\u042e"], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["mn"] }; + + this.VKI_layout['\u092e\u0930\u093e\u0920\u0940'] = { + 'name': "Marathi", 'keys': [ + [["", "", "`", "~"], ["\u0967", "\u090D", "1", "!"], ["\u0968", "\u0945", "2", "@"], ["\u0969", "\u094D\u0930", "3", "#"], ["\u096A", "\u0930\u094D", "4", "$"], ["\u096B", "\u091C\u094D\u091E", "5", "%"], ["\u096C", "\u0924\u094D\u0930", "6", "^"], ["\u096D", "\u0915\u094D\u0937", "7", "&"], ["\u096E", "\u0936\u094D\u0930", "8", "*"], ["\u096F", "(", "9", "("], ["\u0966", ")", "0", ")"], ["-", "\u0903", "-", "_"], ["\u0943", "\u090B", "=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u094C", "\u0914"], ["\u0948", "\u0910"], ["\u093E", "\u0906"], ["\u0940", "\u0908"], ["\u0942", "\u090A"], ["\u092C", "\u092D"], ["\u0939", "\u0919"], ["\u0917", "\u0918"], ["\u0926", "\u0927"], ["\u091C", "\u091D"], ["\u0921", "\u0922", "[", "{"], ["\u093C", "\u091E", "]", "}"], ["\u0949", "\u0911", "\\", "|"]], + [["Caps", "Caps"], ["\u094B", "\u0913"], ["\u0947", "\u090F"], ["\u094D", "\u0905"], ["\u093F", "\u0907"], ["\u0941", "\u0909"], ["\u092A", "\u092B"], ["\u0930", "\u0931"], ["\u0915", "\u0916"], ["\u0924", "\u0925"], ["\u091A", "\u091B", ";", ":"], ["\u091F", "\u0920", "'", '"'], ["Enter", "Enter"]], + [["Shift", "Shift"], [""], ["\u0902", "\u0901", "", "\u0950"], ["\u092E", "\u0923"], ["\u0928"], ["\u0935"], ["\u0932", "\u0933"], ["\u0938", "\u0936"], [",", "\u0937", ",", "<"], [".", "\u0964", ".", ">"], ["\u092F", "\u095F", "/", "?"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["mr"] }; + + this.VKI_layout['\u1019\u103c\u1014\u103a\u1019\u102c\u1018\u102c\u101e\u102c'] = { + 'name': "Burmese", 'keys': [ + [["\u1039`", "~"], ["\u1041", "\u100D"], ["\u1042", "\u100E"], ["\u1043", "\u100B"], ["\u1044", "\u1000\u103B\u1015\u103A"], ["\u1045", "%"], ["\u1046", "/"], ["\u1047", "\u101B"], ["\u1048", "\u1002"], ["\u1049", "("], ["\u1040", ")"], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u1006", "\u1029"], ["\u1010", "\u1040"], ["\u1014", "\u103F"], ["\u1019", "\u1023"], ["\u1021", "\u1024"], ["\u1015", "\u104C"], ["\u1000", "\u1009"], ["\u1004", "\u104D"], ["\u101E", "\u1025"], ["\u1005", "\u100F"], ["\u101F", "\u1027"], ["\u2018", "\u2019"], ["\u104F", "\u100B\u1039\u100C"]], + [["Caps", "Caps"], ["\u200B\u1031", "\u1017"], ["\u200B\u103B", "\u200B\u103E"], ["\u200B\u102D", "\u200B\u102E"], ["\u200B\u103A", "\u1004\u103A\u1039\u200B"], ["\u200B\u102B", "\u200B\u103D"], ["\u200B\u1037", "\u200B\u1036"], ["\u200B\u103C", "\u200B\u1032"], ["\u200B\u102F", "\u200B\u102F"], ["\u200B\u1030", "\u200B\u1030"], ["\u200B\u1038", "\u200B\u102B\u103A"], ["\u1012", "\u1013"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u1016", "\u1007"], ["\u1011", "\u100C"], ["\u1001", "\u1003"], ["\u101C", "\u1020"], ["\u1018", "\u1026"], ["\u100A", "\u1008"], ["\u200B\u102C", "\u102A"], ["\u101A", "\u101B"], [".", "\u101B"], ["\u104B", "\u104A"], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["my"] }; + + this.VKI_layout['Nederlands'] = { + 'name': "Dutch", 'keys': [ + [["@", "\u00a7", "\u00ac"], ["1", "!", "\u00b9"], ["2", '"', "\u00b2"], ["3", "#", "\u00b3"], ["4", "$", "\u00bc"], ["5", "%", "\u00bd"], ["6", "&", "\u00be"], ["7", "_", "\u00a3"], ["8", "(", "{"], ["9", ")", "}"], ["0", "'"], ["/", "?", "\\"], ["\u00b0", "~", "\u00b8"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E", "\u20ac"], ["r", "R", "\u00b6"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["\u00a8", "^"], ["*", "|"], ["<", ">"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S", "\u00df"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["+", "\u00b1"], ["\u00b4", "`"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["]", "[", "\u00a6"], ["z", "Z", "\u00ab"], ["x", "X", "\u00bb"], ["c", "C", "\u00a2"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M", "\u00b5"], [",", ";"], [".", ":", "\u00b7"], ["-", "="], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["nl"] }; + + this.VKI_layout['Norsk'] = { + 'name': "Norwegian", 'keys': [ + [["|", "\u00a7"], ["1", "!"], ["2", '"', "@"], ["3", "#", "\u00a3"], ["4", "\u00a4", "$"], ["5", "%"], ["6", "&"], ["7", "/", "{"], ["8", "(", "["], ["9", ")", "]"], ["0", "=", "}"], ["+", "?"], ["\\", "`", "\u00b4"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E", "\u20ac"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["\u00e5", "\u00c5"], ["\u00a8", "^", "~"], ["'", "*"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["\u00f8", "\u00d8"], ["\u00e6", "\u00c6"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">"], ["z", "Z"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M", "\u03bc", "\u039c"], [",", ";"], [".", ":"], ["-", "_"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["no", "nb", "nn"] }; + + this.VKI_layout['\u067e\u069a\u062a\u0648'] = { + 'name': "Pashto", 'keys': [ + [["\u200d", "\u00f7", "`"], ["\u06f1", "!", "`"], ["\u06f2", "\u066c", "@"], ["\u06f3", "\u066b", "\u066b"], ["\u06f4", "\u00a4", "\u00a3"], ["\u06f5", "\u066a", "%"], ["\u06f6", "\u00d7", "^"], ["\u06f7", "\u00ab", "&"], ["\u06f8", "\u00bb", "*"], ["\u06f9", "(", "\ufdf2"], ["\u06f0", ")", "\ufefb"], ["-", "\u0640", "_"], ["=", "+", "\ufe87", "\u00f7"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0636", "\u0652", "\u06d5"], ["\u0635", "\u064c", "\u0653"], ["\u062b", "\u064d", "\u20ac"], ["\u0642", "\u064b", "\ufef7"], ["\u0641", "\u064f", "\ufef5"], ["\u063a", "\u0650", "'"], ["\u0639", "\u064e", "\ufe84"], ["\u0647", "\u0651", "\u0670"], ["\u062e", "\u0681", "'"], ["\u062d", "\u0685", '"'], ["\u062c", "]", "}"], ["\u0686", "[", "{"], ["\\", "\u066d", "|"]], + [["Caps", "Caps"], ["\u0634", "\u069a", "\ufbb0"], ["\u0633", "\u06cd", "\u06d2"], ["\u06cc", "\u064a", "\u06d2"], ["\u0628", "\u067e", "\u06ba"], ["\u0644", "\u0623", "\u06b7"], ["\u0627", "\u0622", "\u0671"], ["\u062a", "\u067c", "\u0679"], ["\u0646", "\u06bc", "<"], ["\u0645", "\u0629", ">"], ["\u06a9", ":", "\u0643"], ["\u06af", "\u061b", "\u06ab"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0638", "\u0626", "?"], ["\u0637", "\u06d0", ";"], ["\u0632", "\u0698", "\u0655"], ["\u0631", "\u0621", "\u0654"], ["\u0630", "\u200c", "\u0625"], ["\u062f", "\u0689", "\u0688"], ["\u0693", "\u0624", "\u0691"], ["\u0648", "\u060c", ","], ["\u0696", ".", "\u06c7"], ["/", "\u061f", "\u06c9"], ["Shift", "Shift", "\u064d"]], + [[" ", " ", " ", " "], ["Alt", "Alt"]] + ], 'lang': ["ps"] }; + + this.VKI_layout['\u0a2a\u0a70\u0a1c\u0a3e\u0a2c\u0a40'] = { + 'name': "Punjabi (Gurmukhi)", 'keys': [ + [[""], ["1", "\u0A4D\u0A35", "\u0A67", "\u0A67"], ["2", "\u0A4D\u0A2F", "\u0A68", "\u0A68"], ["3", "\u0A4D\u0A30", "\u0A69", "\u0A69"], ["4", "\u0A71", "\u0A6A", "\u0A6A"], ["5", "", "\u0A6B", "\u0A6B"], ["6", "", "\u0A6C", "\u0A6C"], ["7", "", "\u0A6D", "\u0A6D"], ["8", "", "\u0A6E", "\u0A6E"], ["9", "(", "\u0A6F", "\u0A6F"], ["0", ")", "\u0A66", "\u0A66"], ["-"], [""], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0A4C", "\u0A14"], ["\u0A48", "\u0A10"], ["\u0A3E", "\u0A06"], ["\u0A40", "\u0A08"], ["\u0A42", "\u0A0A"], ["\u0A2C", "\u0A2D"], ["\u0A39", "\u0A19"], ["\u0A17", "\u0A18", "\u0A5A", "\u0A5A"], ["\u0A26", "\u0A27"], ["\u0A1C", "\u0A1D", "\u0A5B", "\u0A5B"], ["\u0A21", "\u0A22", "\u0A5C", "\u0A5C"], ["Enter", "Enter"]], + [["Caps", "Caps"], ["\u0A4B", "\u0A13"], ["\u0A47", "\u0A0F"], ["\u0A4D", "\u0A05"], ["\u0A3F", "\u0A07"], ["\u0A41", "\u0A09"], ["\u0A2A", "\u0A2B", "\u0A5E", "\u0A5E"], ["\u0A30"], ["\u0A15", "\u0A16", "\u0A59", "\u0A59"], ["\u0A24", "\u0A25"], ["\u0A1A", "\u0A1B"], ["\u0A1F", "\u0A20"], ["\u0A3C", "\u0A1E"]], + [["Shift", "Shift"], [""], ["\u0A02", "\u0A02"], ["\u0A2E", "\u0A23"], ["\u0A28"], ["\u0A35", "\u0A72", "\u0A73", "\u0A73"], ["\u0A32", "\u0A33"], ["\u0A38", "\u0A36"], [","], [".", "|", "\u0965", "\u0965"], ["\u0A2F"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["pa"] }; + + this.VKI_layout['\u62fc\u97f3 (Pinyin)'] = { + 'name': "Pinyin", 'keys': [ + [["`", "~", "\u4e93", "\u301C"], ["1", "!", "\uFF62"], ["2", "@", "\uFF63"], ["3", "#", "\u301D"], ["4", "$", "\u301E"], ["5", "%", "\u301F"], ["6", "^", "\u3008"], ["7", "&", "\u3009"], ["8", "*", "\u302F"], ["9", "(", "\u300A"], ["0", ")", "\u300B"], ["-", "_", "\u300E"], ["=", "+", "\u300F"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q", "\u0101", "\u0100"], ["w", "W", "\u00E1", "\u00C1"], ["e", "E", "\u01CE", "\u01CD"], ["r", "R", "\u00E0", "\u00C0"], ["t", "T", "\u0113", "\u0112"], ["y", "Y", "\u00E9", "\u00C9"], ["u", "U", "\u011B", "\u011A"], ["i", "I", "\u00E8", "\u00C8"], ["o", "O", "\u012B", "\u012A"], ["p", "P", "\u00ED", "\u00CD"], ["[", "{", "\u01D0", "\u01CF"], ["]", "}", "\u00EC", "\u00CC"], ["\\", "|", "\u3020"]], + [["Caps", "Caps"], ["a", "A", "\u014D", "\u014C"], ["s", "S", "\u00F3", "\u00D3"], ["d", "D", "\u01D2", "\u01D1"], ["f", "F", "\u00F2", "\u00D2"], ["g", "G", "\u00fc", "\u00dc"], ["h", "H", "\u016B", "\u016A"], ["j", "J", "\u00FA", "\u00DA"], ["k", "K", "\u01D4", "\u01D3"], ["l", "L", "\u00F9", "\u00D9"], [";", ":"], ["'", '"'], ["Enter", "Enter"]], + [["Shift", "Shift"], ["z", "Z", "\u01D6", "\u01D5"], ["x", "X", "\u01D8", "\u01D7"], ["c", "C", "\u01DA", "\u01D9"], ["v", "V", "\u01DC", "\u01DB"], ["b", "B"], ["n", "N"], ["m", "M"], [",", "<", "\u3001"], [".", ">", "\u3002"], ["/", "?"], ["Shift", "Shift"]], + [["AltLk", "AltLk"], [" ", " ", " ", " "], ["Alt", "Alt"]] + ], 'lang': ["zh-Latn"] }; + + this.VKI_layout['Polski'] = { + 'name': "Polish (214)", 'keys': [ + [["\u02DB", "\u00B7"], ["1", "!", "~"], ["2", '"', "\u02C7"], ["3", "#", "^"], ["4", "\u00A4", "\u02D8"], ["5", "%", "\u00B0"], ["6", "&", "\u02DB"], ["7", "/", "`"], ["8", "(", "\u00B7"], ["9", ")", "\u00B4"], ["0", "=", "\u02DD"], ["+", "?", "\u00A8"], ["'", "*", "\u00B8"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q", "\\"], ["w", "W", "\u00A6"], ["e", "E"], ["r", "R"], ["t", "T"], ["z", "Z"], ["u", "U", "\u20AC"], ["i", "I"], ["o", "O"], ["p", "P"], ["\u017C", "\u0144", "\u00F7"], ["\u015B", "\u0107", "\u00D7"], ["\u00F3", "\u017A"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S", "\u0111"], ["d", "D", "\u0110"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["\u0142", "\u0141", "$"], ["\u0105", "\u0119", "\u00DF"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">"], ["y", "Y"], ["x", "X"], ["c", "C"], ["v", "V", "@"], ["b", "B", "{"], ["n", "N", "}"], ["m", "M", "\u00A7"], [",", ";", "<"], [".", ":", ">"], ["-", "_"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ]}; + + this.VKI_layout['Polski Programisty'] = { + 'name': "Polish Programmers", 'keys': [ + [["`", "~"], ["1", "!"], ["2", "@"], ["3", "#"], ["4", "$"], ["5", "%"], ["6", "^"], ["7", "&"], ["8", "*"], ["9", "("], ["0", ")"], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E", "\u0119", "\u0118"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O", "\u00f3", "\u00d3"], ["p", "P"], ["[", "{"], ["]", "}"], ["\\", "|"]], + [["Caps", "Caps"], ["a", "A", "\u0105", "\u0104"], ["s", "S", "\u015b", "\u015a"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L", "\u0142", "\u0141"], [";", ":"], ["'", '"'], ["Enter", "Enter"]], + [["Shift", "Shift"], ["z", "Z", "\u017c", "\u017b"], ["x", "X", "\u017a", "\u0179"], ["c", "C", "\u0107", "\u0106"], ["v", "V"], ["b", "B"], ["n", "N", "\u0144", "\u0143"], ["m", "M"], [",", "<"], [".", ">"], ["/", "?"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["Alt", "Alt"]] + ], 'lang': ["pl"] }; + + this.VKI_layout['Portugu\u00eas Brasileiro'] = { + 'name': "Portuguese (Brazil)", 'keys': [ + [["'", '"'], ["1", "!", "\u00b9"], ["2", "@", "\u00b2"], ["3", "#", "\u00b3"], ["4", "$", "\u00a3"], ["5", "%", "\u00a2"], ["6", "\u00a8", "\u00ac"], ["7", "&"], ["8", "*"], ["9", "("], ["0", ")"], ["-", "_"], ["=", "+", "\u00a7"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q", "/"], ["w", "W", "?"], ["e", "E", "\u20ac"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["\u00b4", "`"], ["[", "{", "\u00aa"], ["Enter", "Enter"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["\u00e7", "\u00c7"], ["~", "^"], ["]", "}", "\u00ba"], ["/", "?"]], + [["Shift", "Shift"], ["\\", "|"], ["z", "Z"], ["x", "X"], ["c", "C", "\u20a2"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M"], [",", "<"], [".", ">"], [":", ":"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["pt-BR"] }; + + this.VKI_layout['Portugu\u00eas'] = { + 'name': "Portuguese", 'keys': [ + [["\\", "|"], ["1", "!"], ["2", '"', "@"], ["3", "#", "\u00a3"], ["4", "$", "\u00a7"], ["5", "%"], ["6", "&"], ["7", "/", "{"], ["8", "(", "["], ["9", ")", "]"], ["0", "=", "}"], ["'", "?"], ["\u00ab", "\u00bb"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E", "\u20ac"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["+", "*", "\u00a8"], ["\u00b4", "`"], ["~", "^"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["\u00e7", "\u00c7"], ["\u00ba", "\u00aa"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">", "\\"], ["z", "Z"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M"], [",", ";"], [".", ":"], ["-", "_"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["pt"] }; + + this.VKI_layout['Rom\u00e2n\u0103'] = { + 'name': "Romanian", 'keys': [ + [["\u201E", "\u201D", "`", "~"], ["1", "!", "~"], ["2", "@", "\u02C7"], ["3", "#", "^"], ["4", "$", "\u02D8"], ["5", "%", "\u00B0"], ["6", "^", "\u02DB"], ["7", "&", "`"], ["8", "*", "\u02D9"], ["9", "(", "\u00B4"], ["0", ")", "\u02DD"], ["-", "_", "\u00A8"], ["=", "+", "\u00B8", "\u00B1"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E", "\u20AC"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P", "\u00A7"], ["\u0103", "\u0102", "[", "{"], ["\u00EE", "\u00CE", "]", "}"], ["\u00E2", "\u00C2", "\\", "|"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S", "\u00df"], ["d", "D", "\u00f0", "\u00D0"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L", "\u0142", "\u0141"], [(this.VKI_isIElt8) ? "\u015F" : "\u0219", (this.VKI_isIElt8) ? "\u015E" : "\u0218", ";", ":"], [(this.VKI_isIElt8) ? "\u0163" : "\u021B", (this.VKI_isIElt8) ? "\u0162" : "\u021A", "\'", "\""], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\\", "|"], ["z", "Z"], ["x", "X"], ["c", "C", "\u00A9"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M"], [",", ";", "<", "\u00AB"], [".", ":", ">", "\u00BB"], ["/", "?"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["ro"] }; + + this.VKI_layout['\u0420\u0443\u0441\u0441\u043a\u0438\u0439'] = { + 'name': "Russian", 'keys': [ + [["\u0451", "\u0401"], ["1", "!"], ["2", '"'], ["3", "\u2116"], ["4", ";"], ["5", "%"], ["6", ":"], ["7", "?"], ["8", "*"], ["9", "("], ["0", ")"], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0439", "\u0419"], ["\u0446", "\u0426"], ["\u0443", "\u0423"], ["\u043A", "\u041A"], ["\u0435", "\u0415"], ["\u043D", "\u041D"], ["\u0433", "\u0413"], ["\u0448", "\u0428"], ["\u0449", "\u0429"], ["\u0437", "\u0417"], ["\u0445", "\u0425"], ["\u044A", "\u042A"], ["\\", "/"]], + [["Caps", "Caps"], ["\u0444", "\u0424"], ["\u044B", "\u042B"], ["\u0432", "\u0412"], ["\u0430", "\u0410"], ["\u043F", "\u041F"], ["\u0440", "\u0420"], ["\u043E", "\u041E"], ["\u043B", "\u041B"], ["\u0434", "\u0414"], ["\u0436", "\u0416"], ["\u044D", "\u042D"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["/", "|"], ["\u044F", "\u042F"], ["\u0447", "\u0427"], ["\u0441", "\u0421"], ["\u043C", "\u041C"], ["\u0438", "\u0418"], ["\u0442", "\u0422"], ["\u044C", "\u042C"], ["\u0431", "\u0411"], ["\u044E", "\u042E"], [".", ","], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["ru"] }; + + this.VKI_layout['Schweizerdeutsch'] = { + 'name': "Swiss German", 'keys': [ + [["\u00A7", "\u00B0"], ["1", "+", "\u00A6"], ["2", '"', "@"], ["3", "*", "#"], ["4", "\u00E7", "\u00B0"], ["5", "%", "\u00A7"], ["6", "&", "\u00AC"], ["7", "/", "|"], ["8", "(", "\u00A2"], ["9", ")"], ["0", "="], ["'", "?", "\u00B4"], ["^", "`", "~"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E", "\u20AC"], ["r", "R"], ["t", "T"], ["z", "Z"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["\u00FC", "\u00E8", "["], ["\u00A8", "!", "]"], ["$", "\u00A3", "}"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["\u00F6", "\u00E9"], ["\u00E4", "\u00E0", "{"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">", "\\"], ["y", "Y"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M"], [",", ";"], [".", ":"], ["-", "_"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["de-CH"] }; + + this.VKI_layout['Shqip'] = { + 'name': "Albanian", 'keys': [ + [["\\", "|"], ["1", "!", "~"], ["2", '"', "\u02C7"], ["3", "#", "^"], ["4", "$", "\u02D8"], ["5", "%", "\u00B0"], ["6", "^", "\u02DB"], ["7", "&", "`"], ["8", "*", "\u02D9"], ["9", "(", "\u00B4"], ["0", ")", "\u02DD"], ["-", "_", "\u00A8"], ["=", "+", "\u00B8"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q", "\\"], ["w", "W", "|"], ["e", "E"], ["r", "R"], ["t", "T"], ["z", "Z"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["\u00E7", "\u00C7", "\u00F7"], ["[", "{", "\u00DF"], ["]", "}", "\u00A4"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S", "\u0111"], ["d", "D", "\u0110"], ["f", "F", "["], ["g", "G", "]"], ["h", "H"], ["j", "J"], ["k", "K", "\u0142"], ["l", "L", "\u0141"], ["\u00EB", "\u00CB", "$"], ["@", "'", "\u00D7"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">"], ["y", "Y"], ["x", "X"], ["c", "C"], ["v", "V", "@"], ["b", "B", "{"], ["n", "N", "}"], ["m", "M", "\u00A7"], [",", ";", "<"], [".", ":", ">"], ["/", "?"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["sq"] }; + + this.VKI_layout['Sloven\u010dina'] = { + 'name': "Slovak", 'keys': [ + [[";", "\u00b0"], ["+", "1", "~"], ["\u013E", "2", "\u02C7"], ["\u0161", "3", "^"], ["\u010D", "4", "\u02D8"], ["\u0165", "5", "\u00B0"], ["\u017E", "6", "\u02DB"], ["\u00FD", "7", "`"], ["\u00E1", "8", "\u02D9"], ["\u00ED", "9", "\u00B4"], ["\u00E9", "0", "\u02DD"], ["=", "%", "\u00A8"], ["\u00B4", "\u02c7", "\u00B8"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q", "\\"], ["w", "W", "|"], ["e", "E", "\u20AC"], ["r", "R"], ["t", "T"], ["z", "Z"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P", "'"], ["\u00FA", "/", "\u00F7"], ["\u00E4", "(", "\u00D7"], ["\u0148", ")", "\u00A4"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S", "\u0111"], ["d", "D", "\u0110"], ["f", "F", "["], ["g", "G", "]"], ["h", "H"], ["j", "J"], ["k", "K", "\u0142"], ["l", "L", "\u0141"], ["\u00F4", '"', "$"], ["\u00A7", "!", "\u00DF"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["&", "*", "<"], ["y", "Y", ">"], ["x", "X", "#"], ["c", "C", "&"], ["v", "V", "@"], ["b", "B", "{"], ["n", "N", "}"], ["m", "M"], [",", "?", "<"], [".", ":", ">"], ["-", "_", "*", ], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["sk"] }; + + this.VKI_layout['Sloven\u0161\u010dina'] = { + 'name': "Slovenian", 'keys': this.VKI_layout['Bosanski'].keys.slice(0), 'lang': ["sl"] + }; + + this.VKI_layout['\u0441\u0440\u043f\u0441\u043a\u0438'] = { + 'name': "Serbian Cyrillic", 'keys': [ + [["`", "~"], ["1", "!"], ["2", '"'], ["3", "#"], ["4", "$"], ["5", "%"], ["6", "&"], ["7", "/"], ["8", "("], ["9", ")"], ["0", "="], ["'", "?"], ["+", "*"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0459", "\u0409"], ["\u045a", "\u040a"], ["\u0435", "\u0415", "\u20ac"], ["\u0440", "\u0420"], ["\u0442", "\u0422"], ["\u0437", "\u0417"], ["\u0443", "\u0423"], ["\u0438", "\u0418"], ["\u043e", "\u041e"], ["\u043f", "\u041f"], ["\u0448", "\u0428"], ["\u0452", "\u0402"], ["\u0436", "\u0416"]], + [["Caps", "Caps"], ["\u0430", "\u0410"], ["\u0441", "\u0421"], ["\u0434", "\u0414"], ["\u0444", "\u0424"], ["\u0433", "\u0413"], ["\u0445", "\u0425"], ["\u0458", "\u0408"], ["\u043a", "\u041a"], ["\u043b", "\u041b"], ["\u0447", "\u0427"], ["\u045b", "\u040b"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">"], ["\u0455", "\u0405"], ["\u045f", "\u040f"], ["\u0446", "\u0426"], ["\u0432", "\u0412"], ["\u0431", "\u0411"], ["\u043d", "\u041d"], ["\u043c", "\u041c"], [",", ";", "<"], [".", ":", ">"], ["-", "_", "\u00a9"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["sr-Cyrl"] }; + + this.VKI_layout['Srpski'] = { + 'name': "Serbian Latin", 'keys': this.VKI_layout['Bosanski'].keys.slice(0), 'lang': ["sr"] + }; + + this.VKI_layout['Suomi'] = { + 'name': "Finnish", 'keys': [ + [["\u00a7", "\u00BD"], ["1", "!"], ["2", '"', "@"], ["3", "#", "\u00A3"], ["4", "\u00A4", "$"], ["5", "%", "\u20AC"], ["6", "&"], ["7", "/", "{"], ["8", "(", "["], ["9", ")", "]"], ["0", "=", "}"], ["+", "?", "\\"], ["\u00B4", "`"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q", "\u00E2", "\u00C2"], ["w", "W"], ["e", "E", "\u20AC"], ["r", "R"], ["t", "T", "\u0167", "\u0166"], ["y", "Y"], ["u", "U"], ["i", "I", "\u00ef", "\u00CF"], ["o", "O", "\u00f5", "\u00D5"], ["p", "P"], ["\u00E5", "\u00C5"], ["\u00A8", "^", "~"], ["'", "*"]], + [["Caps", "Caps"], ["a", "A", "\u00E1", "\u00C1"], ["s", "S", "\u0161", "\u0160"], ["d", "D", "\u0111", "\u0110"], ["f", "F", "\u01e5", "\u01E4"], ["g", "G", "\u01E7", "\u01E6"], ["h", "H", "\u021F", "\u021e"], ["j", "J"], ["k", "K", "\u01e9", "\u01E8"], ["l", "L"], ["\u00F6", "\u00D6", "\u00F8", "\u00D8"], ["\u00E4", "\u00C4", "\u00E6", "\u00C6"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">", "|"], ["z", "Z", "\u017E", "\u017D"], ["x", "X"], ["c", "C", "\u010d", "\u010C"], ["v", "V", "\u01EF", "\u01EE"], ["b", "B", "\u0292", "\u01B7"], ["n", "N", "\u014B", "\u014A"], ["m", "M", "\u00B5"], [",", ";"], [".", ":"], ["-", "_"], ["Shift", "Shift"]], + [["Alt", "Alt"], [" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["fi"] }; + + this.VKI_layout['Svenska'] = { + 'name': "Swedish", 'keys': [ + [["\u00a7", "\u00bd"], ["1", "!"], ["2", '"', "@"], ["3", "#", "\u00a3"], ["4", "\u00a4", "$"], ["5", "%", "\u20ac"], ["6", "&"], ["7", "/", "{"], ["8", "(", "["], ["9", ")", "]"], ["0", "=", "}"], ["+", "?", "\\"], ["\u00b4", "`"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E", "\u20ac"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["\u00e5", "\u00c5"], ["\u00a8", "^", "~"], ["'", "*"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["\u00f6", "\u00d6"], ["\u00e4", "\u00c4"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">", "|"], ["z", "Z"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M", "\u03bc", "\u039c"], [",", ";"], [".", ":"], ["-", "_"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["sv"] }; + + this.VKI_layout['Swiss Fran\u00e7ais'] = { + 'name': "Swiss French", 'keys': [ + [["\u00A7", "\u00B0"], ["1", "+", "\u00A6"], ["2", '"', "@"], ["3", "*", "#"], ["4", "\u00E7", "\u00B0"], ["5", "%", "\u00A7"], ["6", "&", "\u00AC"], ["7", "/", "|"], ["8", "(", "\u00A2"], ["9", ")"], ["0", "="], ["'", "?", "\u00B4"], ["^", "`", "~"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E", "\u20AC"], ["r", "R"], ["t", "T"], ["z", "Z"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["\u00E8", "\u00FC", "["], ["\u00A8", "!", "]"], ["$", "\u00A3", "}"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["\u00E9", "\u00F6"], ["\u00E0", "\u00E4", "{"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">", "\\"], ["y", "Y"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M"], [",", ";"], [".", ":"], ["-", "_"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["fr-CH"] }; + + this.VKI_layout['\u0723\u0718\u072a\u071d\u071d\u0710'] = { + 'name': "Syriac", 'keys': [ + [["\u070f", "\u032e", "\u0651", "\u0651"], ["1", "!", "\u0701", "\u0701"], ["2", "\u030a", "\u0702", "\u0702"], ["3", "\u0325", "\u0703", "\u0703"], ["4", "\u0749", "\u0704", "\u0704"], ["5", "\u2670", "\u0705", "\u0705"], ["6", "\u2671", "\u0708", "\u0708"], ["7", "\u070a", "\u0709", "\u0709"], ["8", "\u00bb", "\u070B", "\u070B"], ["9", ")", "\u070C", "\u070C"], ["0", "(", "\u070D", "\u070D"], ["-", "\u00ab", "\u250C", "\u250C"], ["=", "+", "\u2510", "\u2510"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0714", "\u0730", "\u064E", "\u064E"], ["\u0728", "\u0733", "\u064B", "\u064B"], ["\u0716", "\u0736", "\u064F", "\u064F"], ["\u0729", "\u073A", "\u064C", "\u064C"], ["\u0726", "\u073D", "\u0653", "\u0653"], ["\u071c", "\u0740", "\u0654", "\u0654"], ["\u0725", "\u0741", "\u0747", "\u0747"], ["\u0717", "\u0308", "\u0743", "\u0743"], ["\u071e", "\u0304", "\u0745", "\u0745"], ["\u071a", "\u0307", "\u032D", "\u032D"], ["\u0713", "\u0303"], ["\u0715", "\u074A"], ["\u0706", ":"]], + [["Caps", "Caps"], ["\u072b", "\u0731", "\u0650", "\u0650"], ["\u0723", "\u0734", "\u064d", "\u064d"], ["\u071d", "\u0737"], ["\u0712", "\u073b", "\u0621", "\u0621"], ["\u0720", "\u073e", "\u0655", "\u0655"], ["\u0710", "\u0711", "\u0670", "\u0670"], ["\u072c", "\u0640", "\u0748", "\u0748"], ["\u0722", "\u0324", "\u0744", "\u0744"], ["\u0721", "\u0331", "\u0746", "\u0746"], ["\u071f", "\u0323"], ["\u071b", "\u0330"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["]", "\u0732"], ["[", "\u0735", "\u0652", "\u0652"], ["\u0724", "\u0738"], ["\u072a", "\u073c", "\u200D"], ["\u0727", "\u073f", "\u200C"], ["\u0700", "\u0739", "\u200E"], [".", "\u0742", "\u200F"], ["\u0718", "\u060c"], ["\u0719", "\u061b"], ["\u0707", "\u061F"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["syc"] }; + + this.VKI_layout['\u0ba4\u0bae\u0bbf\u0bb4\u0bcd'] = { + 'name': "Tamil", 'keys': [ + [["\u0BCA", "\u0B92"], ["1", "", "\u0BE7"], ["2", "", "\u0BE8"], ["3", "", "\u0BE9"], ["4", "", "\u0BEA"], ["5", "", "\u0BEB"], ["6", "\u0BA4\u0BCD\u0BB0", "\u0BEC"], ["7", "\u0B95\u0BCD\u0BB7", "\u0BED"], ["8", "\u0BB7\u0BCD\u0BB0", "\u0BEE"], ["9", "", "\u0BEF"], ["0", "", "\u0BF0"], ["-", "\u0B83", "\u0BF1"], ["", "", "\u0BF2"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0BCC", "\u0B94"], ["\u0BC8", "\u0B90"], ["\u0BBE", "\u0B86"], ["\u0BC0", "\u0B88"], ["\u0BC2", "\u0B8A"], ["\u0BAA", "\u0BAA"], ["\u0BB9", "\u0B99"], ["\u0B95", "\u0B95"], ["\u0BA4", "\u0BA4"], ["\u0B9C", "\u0B9A"], ["\u0B9F", "\u0B9F"], ["\u0B9E"]], + [["Caps", "Caps"], ["\u0BCB", "\u0B93"], ["\u0BC7", "\u0B8F"], ["\u0BCD", "\u0B85"], ["\u0BBF", "\u0B87"], ["\u0BC1", "\u0B89"], ["\u0BAA", "\u0BAA"], ["\u0BB0", "\u0BB1"], ["\u0B95", "\u0B95"], ["\u0BA4", "\u0BA4"], ["\u0B9A", "\u0B9A"], ["\u0B9F", "\u0B9F"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0BC6", "\u0B8E"], [""], ["\u0BAE", "\u0BA3"], ["\u0BA8", "\u0BA9"], ["\u0BB5", "\u0BB4"], ["\u0BB2", "\u0BB3"], ["\u0BB8", "\u0BB7"], [",", "\u0BB7"], [".", "\u0BB8\u0BCD\u0BB0\u0BC0"], ["\u0BAF", "\u0BAF"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["ta"] }; + + this.VKI_layout['\u0c24\u0c46\u0c32\u0c41\u0c17\u0c41'] = { + 'name': "Telugu", 'keys': [ + [["\u0C4A", "\u0C12"], ["1", "", "\u0C67"], ["2", "", "\u0C68"], ["3", "\u0C4D\u0C30", "\u0C69"], ["4", "", "\u0C6A"], ["5", "\u0C1C\u0C4D\u0C1E", "\u0C6B"], ["6", "\u0C24\u0C4D\u0C30", "\u0C6C"], ["7", "\u0C15\u0C4D\u0C37", "\u0C6D"], ["8", "\u0C36\u0C4D\u0C30", "\u0C6E"], ["9", "(", "\u0C6F"], ["0", ")", "\u0C66"], ["-", "\u0C03"], ["\u0C43", "\u0C0B", "\u0C44"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0C4C", "\u0C14"], ["\u0C48", "\u0C10", "\u0C56"], ["\u0C3E", "\u0C06"], ["\u0C40", "\u0C08", "", "\u0C61"], ["\u0C42", "\u0C0A"], ["\u0C2C"], ["\u0C39", "\u0C19"], ["\u0C17", "\u0C18"], ["\u0C26", "\u0C27"], ["\u0C1C", "\u0C1D"], ["\u0C21", "\u0C22"], ["", "\u0C1E"]], + [["Caps", "Caps"], ["\u0C4B", "\u0C13"], ["\u0C47", "\u0C0F", "\u0C55"], ["\u0C4D", "\u0C05"], ["\u0C3F", "\u0C07", "", "\u0C0C"], ["\u0C41", "\u0C09"], ["\u0C2A", "\u0C2B"], ["\u0C30", "\u0C31"], ["\u0C15", "\u0C16"], ["\u0C24", "\u0C25"], ["\u0C1A", "\u0C1B"], ["\u0C1F", "\u0C25"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0C46", "\u0C0E"], ["\u0C02", "\u0C01"], ["\u0C2E", "\u0C23"], ["\u0C28", "\u0C28"], ["\u0C35"], ["\u0C32", "\u0C33"], ["\u0C38", "\u0C36"], [",", "\u0C37"], ["."], ["\u0C2F"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["te"] }; + + this.VKI_layout['Ti\u1ebfng Vi\u1ec7t'] = { + 'name': "Vietnamese", 'keys': [ + [["`", "~", "`", "~"], ["\u0103", "\u0102", "1", "!"], ["\u00E2", "\u00C2", "2", "@"], ["\u00EA", "\u00CA", "3", "#"], ["\u00F4", "\u00D4", "4", "$"], ["\u0300", "\u0300", "5", "%"], ["\u0309", "\u0309", "6", "^"], ["\u0303", "\u0303", "7", "&"], ["\u0301", "\u0301", "8", "*"], ["\u0323", "\u0323", "9", "("], ["\u0111", "\u0110", "0", ")"], ["-", "_", "-", "_"], ["\u20AB", "+", "=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q", "q", "Q"], ["w", "W", "w", "W"], ["e", "E", "e", "E"], ["r", "R", "r", "R"], ["t", "T", "t", "T"], ["y", "Y", "y", "Y"], ["u", "U", "u", "U"], ["i", "I", "i", "I"], ["o", "O", "o", "O"], ["p", "P", "p", "P"], ["\u01B0", "\u01AF", "[", "{"], ["\u01A1", "\u01A0", "]", "}"], ["\\", "|", "\\", "|"]], + [["Caps", "Caps"], ["a", "A", "a", "A"], ["s", "S", "s", "S"], ["d", "D", "d", "D"], ["f", "F", "f", "F"], ["g", "G", "g", "G"], ["h", "H", "h", "H"], ["j", "J", "j", "J"], ["k", "K", "k", "K"], ["l", "L", "l", "L"], [";", ":", ";", ":"], ["'", '"', "'", '"'], ["Enter", "Enter"]], + [["Shift", "Shift"], ["z", "Z", "z", "Z"], ["x", "X", "x", "X"], ["c", "C", "c", "C"], ["v", "V", "v", "V"], ["b", "B", "b", "B"], ["n", "N", "n", "N"], ["m", "M", "m", "M"], [",", "<", ",", "<"], [".", ">", ".", ">"], ["/", "?", "/", "?"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["vi"] }; + + this.VKI_layout['\u0e44\u0e17\u0e22 Kedmanee'] = { + 'name': "Thai Kedmanee", 'keys': [ + [["_", "%"], ["\u0E45", "+"], ["/", "\u0E51"], ["-", "\u0E52"], ["\u0E20", "\u0E53"], ["\u0E16", "\u0E54"], ["\u0E38", "\u0E39"], ["\u0E36", "\u0E3F"], ["\u0E04", "\u0E55"], ["\u0E15", "\u0E56"], ["\u0E08", "\u0E57"], ["\u0E02", "\u0E58"], ["\u0E0A", "\u0E59"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0E46", "\u0E50"], ["\u0E44", '"'], ["\u0E33", "\u0E0E"], ["\u0E1E", "\u0E11"], ["\u0E30", "\u0E18"], ["\u0E31", "\u0E4D"], ["\u0E35", "\u0E4A"], ["\u0E23", "\u0E13"], ["\u0E19", "\u0E2F"], ["\u0E22", "\u0E0D"], ["\u0E1A", "\u0E10"], ["\u0E25", ","], ["\u0E03", "\u0E05"]], + [["Caps", "Caps"], ["\u0E1F", "\u0E24"], ["\u0E2B", "\u0E06"], ["\u0E01", "\u0E0F"], ["\u0E14", "\u0E42"], ["\u0E40", "\u0E0C"], ["\u0E49", "\u0E47"], ["\u0E48", "\u0E4B"], ["\u0E32", "\u0E29"], ["\u0E2A", "\u0E28"], ["\u0E27", "\u0E0B"], ["\u0E07", "."], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0E1C", "("], ["\u0E1B", ")"], ["\u0E41", "\u0E09"], ["\u0E2D", "\u0E2E"], ["\u0E34", "\u0E3A"], ["\u0E37", "\u0E4C"], ["\u0E17", "?"], ["\u0E21", "\u0E12"], ["\u0E43", "\u0E2C"], ["\u0E1D", "\u0E26"], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["th"] }; + + this.VKI_layout['\u0e44\u0e17\u0e22 Pattachote'] = { + 'name': "Thai Pattachote", 'keys': [ + [["_", "\u0E3F"], ["=", "+"], ["\u0E52", '"'], ["\u0E53", "/"], ["\u0E54", ","], ["\u0E55", "?"], ["\u0E39", "\u0E38"], ["\u0E57", "_"], ["\u0E58", "."], ["\u0E59", "("], ["\u0E50", ")"], ["\u0E51", "-"], ["\u0E56", "%"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0E47", "\u0E4A"], ["\u0E15", "\u0E24"], ["\u0E22", "\u0E46"], ["\u0E2D", "\u0E0D"], ["\u0E23", "\u0E29"], ["\u0E48", "\u0E36"], ["\u0E14", "\u0E1D"], ["\u0E21", "\u0E0B"], ["\u0E27", "\u0E16"], ["\u0E41", "\u0E12"], ["\u0E43", "\u0E2F"], ["\u0E0C", "\u0E26"], ["\uF8C7", "\u0E4D"]], + [["Caps", "Caps"], ["\u0E49", "\u0E4B"], ["\u0E17", "\u0E18"], ["\u0E07", "\u0E33"], ["\u0E01", "\u0E13"], ["\u0E31", "\u0E4C"], ["\u0E35", "\u0E37"], ["\u0E32", "\u0E1C"], ["\u0E19", "\u0E0A"], ["\u0E40", "\u0E42"], ["\u0E44", "\u0E06"], ["\u0E02", "\u0E11"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0E1A", "\u0E0E"], ["\u0E1B", "\u0E0F"], ["\u0E25", "\u0E10"], ["\u0E2B", "\u0E20"], ["\u0E34", "\u0E31"], ["\u0E04", "\u0E28"], ["\u0E2A", "\u0E2E"], ["\u0E30", "\u0E1F"], ["\u0E08", "\u0E09"], ["\u0E1E", "\u0E2C"], ["Shift", "Shift"]], + [[" ", " "]] + ]}; + + this.VKI_layout['\u0422\u0430\u0442\u0430\u0440\u0447\u0430'] = { + 'name': "Tatar", 'keys': [ + [["\u04BB", "\u04BA", "\u0451", "\u0401"], ["1", "!"], ["2", '"', "@"], ["3", "\u2116", "#"], ["4", ";", "$"], ["5", "%"], ["6", ":"], ["7", "?", "["], ["8", "*", "]"], ["9", "(", "{"], ["0", ")", "}"], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0439", "\u0419"], ["\u04E9", "\u04E8", "\u0446", "\u0426"], ["\u0443", "\u0423"], ["\u043A", "\u041A"], ["\u0435", "\u0415"], ["\u043D", "\u041D"], ["\u0433", "\u0413"], ["\u0448", "\u0428"], ["\u04D9", "\u04D8", "\u0449", "\u0429"], ["\u0437", "\u0417"], ["\u0445", "\u0425"], ["\u04AF", "\u04AE", "\u044A", "\u042A"], ["\\", "/"]], + [["Caps", "Caps"], ["\u0444", "\u0424"], ["\u044B", "\u042B"], ["\u0432", "\u0412"], ["\u0430", "\u0410"], ["\u043F", "\u041F"], ["\u0440", "\u0420"], ["\u043E", "\u041E"], ["\u043B", "\u041B"], ["\u0434", "\u0414"], ["\u04A3", "\u04A2", "\u0436", "\u0416"], ["\u044D", "\u042D", "'"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0491", "\u0490"], ["\u044F", "\u042F"], ["\u0447", "\u0427"], ["\u0441", "\u0421"], ["\u043C", "\u041C"], ["\u0438", "\u0418"], ["\u0442", "\u0422"], ["\u0497", "\u0496", "\u044C", "\u042C"], ["\u0431", "\u0411", "<"], ["\u044E", "\u042E", ">"], [".", ","], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["tt"] }; + + this.VKI_layout['T\u00fcrk\u00e7e F'] = { + 'name': "Turkish F", 'keys': [ + [['+', "*", "\u00ac"], ["1", "!", "\u00b9", "\u00a1"], ["2", '"', "\u00b2"], ["3", "^", "#", "\u00b3"], ["4", "$", "\u00bc", "\u00a4"], ["5", "%", "\u00bd"], ["6", "&", "\u00be"], ["7", "'", "{"], ["8", "(", '['], ["9", ")", ']'], ["0", "=", "}"], ["/", "?", "\\", "\u00bf"], ["-", "_", "|"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["f", "F", "@"], ["g", "G"], ["\u011f", "\u011e"], ["\u0131", "I", "\u00b6", "\u00ae"], ["o", "O"], ["d", "D", "\u00a5"], ["r", "R"], ["n", "N"], ["h", "H", "\u00f8", "\u00d8"], ["p", "P", "\u00a3"], ["q", "Q", "\u00a8"], ["w", "W", "~"], ["x", "X", "`"]], + [["Caps", "Caps"], ["u", "U", "\u00e6", "\u00c6"], ["i", "\u0130", "\u00df", "\u00a7"], ["e", "E", "\u20ac"], ["a", "A", " ", "\u00aa"], ["\u00fc", "\u00dc"], ["t", "T"], ["k", "K"], ["m", "M"], ["l", "L"], ["y", "Y", "\u00b4"], ["\u015f", "\u015e"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">", "|", "\u00a6"], ["j", "J", "\u00ab", "<"], ["\u00f6", "\u00d6", "\u00bb", ">"], ["v", "V", "\u00a2", "\u00a9"], ["c", "C"], ["\u00e7", "\u00c7"], ["z", "Z"], ["s", "S", "\u00b5", "\u00ba"], ["b", "B", "\u00d7"], [".", ":", "\u00f7"], [",", ";", "-"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ]}; + + this.VKI_layout['T\u00fcrk\u00e7e Q'] = { + 'name': "Turkish Q", 'keys': [ + [['"', "\u00e9", "<"], ["1", "!", ">"], ["2", "'", "\u00a3"], ["3", "^", "#"], ["4", "+", "$"], ["5", "%", "\u00bd"], ["6", "&"], ["7", "/", "{"], ["8", "(", '['], ["9", ")", ']'], ["0", "=", "}"], ["*", "?", "\\"], ["-", "_", "|"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q", "@"], ["w", "W"], ["e", "E", "\u20ac"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["\u0131", "I", "i", "\u0130"], ["o", "O"], ["p", "P"], ["\u011f", "\u011e", "\u00a8"], ["\u00fc", "\u00dc", "~"], [",", ";", "`"]], + [["Caps", "Caps"], ["a", "A", "\u00e6", "\u00c6"], ["s", "S", "\u00df"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["\u015f", "\u015e", "\u00b4"], ["i", "\u0130"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">", "|"], ["z", "Z"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M"], ["\u00f6", "\u00d6"], ["\u00e7", "\u00c7"], [".", ":"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["tr"] }; + + this.VKI_layout['\u0423\u043a\u0440\u0430\u0457\u043d\u0441\u044c\u043a\u0430'] = { + 'name': "Ukrainian", 'keys': [ + [["\u00b4", "~"], ["1", "!"], ["2", '"'], ["3", "\u2116"], ["4", ";"], ["5", "%"], ["6", ":"], ["7", "?"], ["8", "*"], ["9", "("], ["0", ")"], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0439", "\u0419"], ["\u0446", "\u0426"], ["\u0443", "\u0423"], ["\u043A", "\u041A"], ["\u0435", "\u0415"], ["\u043D", "\u041D"], ["\u0433", "\u0413"], ["\u0448", "\u0428"], ["\u0449", "\u0429"], ["\u0437", "\u0417"], ["\u0445", "\u0425"], ["\u0457", "\u0407"], ["\u0491", "\u0490"]], + [["Caps", "Caps"], ["\u0444", "\u0424"], ["\u0456", "\u0406"], ["\u0432", "\u0412"], ["\u0430", "\u0410"], ["\u043F", "\u041F"], ["\u0440", "\u0420"], ["\u043E", "\u041E"], ["\u043B", "\u041B"], ["\u0434", "\u0414"], ["\u0436", "\u0416"], ["\u0454", "\u0404"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u044F", "\u042F"], ["\u0447", "\u0427"], ["\u0441", "\u0421"], ["\u043C", "\u041C"], ["\u0438", "\u0418"], ["\u0442", "\u0422"], ["\u044C", "\u042C"], ["\u0431", "\u0411"], ["\u044E", "\u042E"], [".", ","], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["uk"] }; + + this.VKI_layout['United Kingdom'] = { + 'name': "United Kingdom", 'keys': [ + [["`", "\u00ac", "\u00a6"], ["1", "!"], ["2", '"'], ["3", "\u00a3"], ["4", "$", "\u20ac"], ["5", "%"], ["6", "^"], ["7", "&"], ["8", "*"], ["9", "("], ["0", ")"], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E", "\u00e9", "\u00c9"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U", "\u00fa", "\u00da"], ["i", "I", "\u00ed", "\u00cd"], ["o", "O", "\u00f3", "\u00d3"], ["p", "P"], ["[", "{"], ["]", "}"], ["#", "~"]], + [["Caps", "Caps"], ["a", "A", "\u00e1", "\u00c1"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], [";", ":"], ["'", "@"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\\", "|"], ["z", "Z"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M"], [",", "<"], [".", ">"], ["/", "?"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["en-gb"] }; + + this.VKI_layout['\u0627\u0631\u062f\u0648'] = { + 'name': "Urdu", 'keys': [ + [["`", "~"], ["1", "!"], ["2", "@"], ["3", "#"], ["4", "$"], ["5", "\u066A"], ["6", "^"], ["7", "\u06D6"], ["8", "\u066D"], ["9", ")"], ["0", "("], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0637", "\u0638"], ["\u0635", "\u0636"], ["\u06be", "\u0630"], ["\u062f", "\u0688"], ["\u0679", "\u062B"], ["\u067e", "\u0651"], ["\u062a", "\u06C3"], ["\u0628", "\u0640"], ["\u062c", "\u0686"], ["\u062d", "\u062E"], ["]", "}"], ["[", "{"], ["\\", "|"]], + [["Caps", "Caps"], ["\u0645", "\u0698"], ["\u0648", "\u0632"], ["\u0631", "\u0691"], ["\u0646", "\u06BA"], ["\u0644", "\u06C2"], ["\u06c1", "\u0621"], ["\u0627", "\u0622"], ["\u06A9", "\u06AF"], ["\u06CC", "\u064A"], ["\u061b", ":"], ["'", '"'], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0642", "\u200D"], ["\u0641", "\u200C"], ["\u06D2", "\u06D3"], ["\u0633", "\u200E"], ["\u0634", "\u0624"], ["\u063a", "\u0626"], ["\u0639", "\u200F"], ["\u060C", ">"], ["\u06D4", "<"], ["/", "\u061F"], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["ur"] }; + + this.VKI_layout['\u0627\u0631\u062f\u0648 Phonetic'] = { + 'name': "Urdu Phonetic", 'keys': [ + [["\u064D", "\u064B", "~"], ["\u06F1", "1", "!"], ["\u06F2", "2", "@"], ["\u06F3", "3", "#"], ["\u06F4", "4", "$"], ["\u06F5", "5", "\u066A"], ["\u06F6", "6", "^"], ["\u06F7", "7", "&"], ["\u06F8", "8", "*"], ["\u06F9", "9", "("], ["\u06F0", "0", ")"], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0642", "\u0652"], ["\u0648", "\u0651", "\u0602"], ["\u0639", "\u0670", "\u0656"], ["\u0631", "\u0691", "\u0613"], ["\u062A", "\u0679", "\u0614"], ["\u06D2", "\u064E", "\u0601"], ["\u0621", "\u0626", "\u0654"], ["\u06CC", "\u0650", "\u0611"], ["\u06C1", "\u06C3"], ["\u067E", "\u064F", "\u0657"], ["[", "{"], ["]", "}"], ["\\", "|"]], + [["Caps", "Caps"], ["\u0627", "\u0622", "\uFDF2"], ["\u0633", "\u0635", "\u0610"], ["\u062F", "\u0688", "\uFDFA"], ["\u0641"], ["\u06AF", "\u063A"], ["\u062D", "\u06BE", "\u0612"], ["\u062C", "\u0636", "\uFDFB"], ["\u06A9", "\u062E"], ["\u0644"], ["\u061B", ":"], ["'", '"'], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0632", "\u0630", "\u060F"], ["\u0634", "\u0698", "\u060E"], ["\u0686", "\u062B", "\u0603"], ["\u0637", "\u0638"], ["\u0628", "", "\uFDFD"], ["\u0646", "\u06BA", "\u0600"], ["\u0645", "\u0658"], ["\u060C", "", "<"], ["\u06D4", "\u066B", ">"], ["/", "\u061F"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["Alt", "Alt"]] + ]}; + + this.VKI_layout['US Standard'] = { + 'name': "US Standard", 'keys': [ + [["`", "~"], ["1", "!"], ["2", "@"], ["3", "#"], ["4", "$"], ["5", "%"], ["6", "^"], ["7", "&"], ["8", "*"], ["9", "("], ["0", ")"], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["[", "{"], ["]", "}"], ["\\", "|"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], [";", ":"], ["'", '"'], ["Enter", "Enter"]], + [["Shift", "Shift"], ["z", "Z"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M"], [",", "<"], [".", ">"], ["/", "?"], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["en-us"] }; + + this.VKI_layout['US International'] = { + 'name': "US International", 'keys': [ + [["`", "~"], ["1", "!", "\u00a1", "\u00b9"], ["2", "@", "\u00b2"], ["3", "#", "\u00b3"], ["4", "$", "\u00a4", "\u00a3"], ["5", "%", "\u20ac"], ["6", "^", "\u00bc"], ["7", "&", "\u00bd"], ["8", "*", "\u00be"], ["9", "(", "\u2018"], ["0", ")", "\u2019"], ["-", "_", "\u00a5"], ["=", "+", "\u00d7", "\u00f7"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q", "\u00e4", "\u00c4"], ["w", "W", "\u00e5", "\u00c5"], ["e", "E", "\u00e9", "\u00c9"], ["r", "R", "\u00ae"], ["t", "T", "\u00fe", "\u00de"], ["y", "Y", "\u00fc", "\u00dc"], ["u", "U", "\u00fa", "\u00da"], ["i", "I", "\u00ed", "\u00cd"], ["o", "O", "\u00f3", "\u00d3"], ["p", "P", "\u00f6", "\u00d6"], ["[", "{", "\u00ab"], ["]", "}", "\u00bb"], ["\\", "|", "\u00ac", "\u00a6"]], + [["Caps", "Caps"], ["a", "A", "\u00e1", "\u00c1"], ["s", "S", "\u00df", "\u00a7"], ["d", "D", "\u00f0", "\u00d0"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L", "\u00f8", "\u00d8"], [";", ":", "\u00b6", "\u00b0"], ["'", '"', "\u00b4", "\u00a8"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["z", "Z", "\u00e6", "\u00c6"], ["x", "X"], ["c", "C", "\u00a9", "\u00a2"], ["v", "V"], ["b", "B"], ["n", "N", "\u00f1", "\u00d1"], ["m", "M", "\u00b5"], [",", "<", "\u00e7", "\u00c7"], [".", ">"], ["/", "?", "\u00bf"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["Alt", "Alt"]] + ], 'lang': ["en"] }; + + this.VKI_layout['\u040e\u0437\u0431\u0435\u043a\u0447\u0430'] = { + 'name': "Uzbek Cyrillic", 'keys': [ + [["\u0451", "\u0401"], ["1", "!"], ["2", '"'], ["3", "\u2116"], ["4", ";"], ["5", "%"], ["6", ":"], ["7", "?"], ["8", "*"], ["9", "("], ["0", ")"], ["\u0493", "\u0492"], ["\u04B3", "\u04B2"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0439", "\u0419"], ["\u0446", "\u0426"], ["\u0443", "\u0423"], ["\u043A", "\u041A"], ["\u0435", "\u0415"], ["\u043D", "\u041D"], ["\u0433", "\u0413"], ["\u0448", "\u0428"], ["\u045E", "\u040E"], ["\u0437", "\u0417"], ["\u0445", "\u0425"], ["\u044A", "\u042A"], ["\\", "/"]], + [["Caps", "Caps"], ["\u0444", "\u0424"], ["\u049B", "\u049A"], ["\u0432", "\u0412"], ["\u0430", "\u0410"], ["\u043F", "\u041F"], ["\u0440", "\u0420"], ["\u043E", "\u041E"], ["\u043B", "\u041B"], ["\u0434", "\u0414"], ["\u0436", "\u0416"], ["\u044D", "\u042D"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u044F", "\u042F"], ["\u0447", "\u0427"], ["\u0441", "\u0421"], ["\u043C", "\u041C"], ["\u0438", "\u0418"], ["\u0442", "\u0422"], ["\u044C", "\u042C"], ["\u0431", "\u0411"], ["\u044E", "\u042E"], [".", ","], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["uz"] }; + + this.VKI_layout['\u05d9\u05d9\u05b4\u05d3\u05d9\u05e9'] = { // from http://www.yv.org/uyip/hebyidkbd.txt http://uyip.org/keyboards.html + 'name': "Yiddish", 'keys': [ + [[";", "~", "\u05B0"], ["1", "!", "\u05B1"], ["2", "@", "\u05B2"], ["3", "#", "\u05B3"], ["4", "$", "\u05B4"], ["5", "%", "\u05B5"], ["6", "^", "\u05B6"], ["7", "*", "\u05B7"], ["8", "&", "\u05B8"], ["9", "(", "\u05C2"], ["0", ")", "\u05C1"], ["-", "_", "\u05B9"], ["=", "+", "\u05BC"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["/", "\u201F", "\u201F"], ["'", "\u201E", "\u201E"], ["\u05E7", "`", "`"], ["\u05E8", "\uFB2F", "\uFB2F"], ["\u05D0", "\uFB2E", "\uFB2E"], ["\u05D8", "\u05F0", "\u05F0"], ["\u05D5", "\uFB35", "\uFB35"], ["\u05DF", "\uFB4B", "\uFB4B"], ["\u05DD", "\uFB4E", "\uFB4E"], ["\u05E4", "\uFB44", "\uFB44"], ["[", "{", "\u05BD"], ["]", "}", "\u05BF"], ["\\", "|", "\u05BB"]], + [["Caps", "Caps"], ["\u05E9", "\uFB2A", "\uFB2A"], ["\u05D3", "\uFB2B", "\uFB2B"], ["\u05D2"], ["\u05DB", "\uFB3B", "\uFB3B"], ["\u05E2", "\u05F1", "\u05F1"], ["\u05D9", "\uFB1D", "\uFB1D"], ["\u05D7", "\uFF1F", "\uFF1F"], ["\u05DC", "\u05F2", "\u05F2"], ["\u05DA"], ["\u05E3", ":", "\u05C3"], [",", '"', "\u05C0"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u05D6", "\u2260", "\u2260"], ["\u05E1", "\uFB4C", "\uFB4C"], ["\u05D1", "\uFB31", "\uFB31"], ["\u05D4", "\u05BE", "\u05BE"], ["\u05E0", "\u2013", "\u2013"], ["\u05DE", "\u2014", "\u2014"], ["\u05E6", "\uFB4A", "\uFB4A"], ["\u05EA", "<", "\u05F3"], ["\u05E5", ">", "\u05F4"], [".", "?", "\u20AA"], ["Shift", "Shift"]], + [[" ", " "], ["Alt", "Alt"]] + ], 'lang': ["yi"] }; + + this.VKI_layout['\u05d9\u05d9\u05b4\u05d3\u05d9\u05e9 \u05dc\u05e2\u05d1\u05d8'] = { // from http://jidysz.net/ + 'name': "Yiddish (Yidish Lebt)", 'keys': [ + [[";", "~"], ["1", "!", "\u05B2", "\u05B2"], ["2", "@", "\u05B3", "\u05B3"], ["3", "#", "\u05B1", "\u05B1"], ["4", "$", "\u05B4", "\u05B4"], ["5", "%", "\u05B5", "\u05B5"], ["6", "^", "\u05B7", "\u05B7"], ["7", "&", "\u05B8", "\u05B8"], ["8", "*", "\u05BB", "\u05BB"], ["9", ")", "\u05B6", "\u05B6"], ["0", "(", "\u05B0", "\u05B0"], ["-", "_", "\u05BF", "\u05BF"], ["=", "+", "\u05B9", "\u05B9"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["/", "", "\u05F4", "\u05F4"], ["'", "", "\u05F3", "\u05F3"], ["\u05E7", "", "\u20AC"], ["\u05E8"], ["\u05D0", "", "\u05D0\u05B7", "\uFB2E"], ["\u05D8", "", "\u05D0\u05B8", "\uFB2F"], ["\u05D5", "\u05D5\u05B9", "\u05D5\u05BC", "\uFB35"], ["\u05DF", "", "\u05D5\u05D5", "\u05F0"], ["\u05DD", "", "\u05BC"], ["\u05E4", "", "\u05E4\u05BC", "\uFB44"], ["]", "}", "\u201E", "\u201D"], ["[", "{", "\u201A", "\u2019"], ["\\", "|", "\u05BE", "\u05BE"]], + [["Caps", "Caps"], ["\u05E9", "\u05E9\u05C1", "\u05E9\u05C2", "\uFB2B"], ["\u05D3", "", "\u20AA"], ["\u05D2", "\u201E"], ["\u05DB", "", "\u05DB\u05BC", "\uFB3B"], ["\u05E2", "", "", "\uFB20"], ["\u05D9", "", "\u05D9\u05B4", "\uFB1D"], ["\u05D7", "", "\u05F2\u05B7", "\uFB1F"], ["\u05DC", "\u05DC\u05B9", "\u05D5\u05D9", "\u05F1"], ["\u05DA", "", "", "\u05F2"], ["\u05E3", ":", "\u05E4\u05BF", "\uFB4E"], [",", '"', ";", "\u05B2"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u05D6", "", "\u2013", "\u2013"], ["\u05E1", "", "\u2014", "\u2014"], ["\u05D1", "\u05DC\u05B9", "\u05D1\u05BF", "\uFB4C"], ["\u05D4", "", "\u201D", "\u201C"], ["\u05E0", "", "\u059C", "\u059E"], ["\u05DE", "", "\u2019", "\u2018"], ["\u05E6", "", "\u05E9\u05C1", "\uFB2A"], ["\u05EA", ">", "\u05EA\u05BC", "\uFB4A"], ["\u05E5", "<"], [".", "?", "\u2026"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["Alt", "Alt"]] + ], 'lang': ["yi"] }; + + this.VKI_layout['\u4e2d\u6587\u6ce8\u97f3\u7b26\u53f7'] = { + 'name': "Chinese Bopomofo IME", 'keys': [ + [["\u20AC", "~"], ["\u3105", "!"], ["\u3109", "@"], ["\u02C7", "#"], ["\u02CB", "$"], ["\u3113", "%"], ["\u02CA", "^"], ["\u02D9", "&"], ["\u311A", "*"], ["\u311E", ")"], ["\u3122", "("], ["\u3126", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u3106", "q"], ["\u310A", "w"], ["\u310D", "e"], ["\u3110", "r"], ["\u3114", "t"], ["\u3117", "y"], ["\u3127", "u"], ["\u311B", "i"], ["\u311F", "o"], ["\u3123", "p"], ["[", "{"], ["]", "}"], ["\\", "|"]], + [["Caps", "Caps"], ["\u3107", "a"], ["\u310B", "s"], ["\u310E", "d"], ["\u3111", "f"], ["\u3115", "g"], ["\u3118", "h"], ["\u3128", "j"], ["\u311C", "k"], ["\u3120", "l"], ["\u3124", ":"], ["'", '"'], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u3108", "z"], ["\u310C", "x"], ["\u310F", "c"], ["\u3112", "v"], ["\u3116", "b"], ["\u3119", "n"], ["\u3129", "m"], ["\u311D", "<"], ["\u3121", ">"], ["\u3125", "?"], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["zh-Bopo"] }; + + this.VKI_layout['\u4e2d\u6587\u4ed3\u9889\u8f93\u5165\u6cd5'] = { + 'name': "Chinese Cangjie IME", 'keys': [ + [["\u20AC", "~"], ["1", "!"], ["2", "@"], ["3", "#"], ["4", "$"], ["5", "%"], ["6", "^"], ["7", "&"], ["8", "*"], ["9", ")"], ["0", "("], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u624B", "q"], ["\u7530", "w"], ["\u6C34", "e"], ["\u53E3", "r"], ["\u5EFF", "t"], ["\u535C", "y"], ["\u5C71", "u"], ["\u6208", "i"], ["\u4EBA", "o"], ["\u5FC3", "p"], ["[", "{"], ["]", "}"], ["\\", "|"]], + [["Caps", "Caps"], ["\u65E5", "a"], ["\u5C38", "s"], ["\u6728", "d"], ["\u706B", "f"], ["\u571F", "g"], ["\u7AF9", "h"], ["\u5341", "j"], ["\u5927", "k"], ["\u4E2D", "l"], [";", ":"], ["'", '"'], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\uFF3A", "z"], ["\u96E3", "x"], ["\u91D1", "c"], ["\u5973", "v"], ["\u6708", "b"], ["\u5F13", "n"], ["\u4E00", "m"], [",", "<"], [".", ">"], ["/", "?"], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["zh"] }; + + + /* ***** Define Dead Keys ************************************** */ + this.VKI_deadkey = {}; + + // - Lay out each dead key set as an object of property/value + // pairs. The rows below are wrapped so uppercase letters are + // below their lowercase equivalents. + // + // - The property name is the letter pressed after the diacritic. + // The property value is the letter this key-combo will generate. + // + // - Note that if you have created a new keyboard layout and want + // it included in the distributed script, PLEASE TELL ME if you + // have added additional dead keys to the ones below. + + this.VKI_deadkey['"'] = this.VKI_deadkey['\u00a8'] = this.VKI_deadkey['\u309B'] = { // Umlaut / Diaeresis / Greek Dialytika / Hiragana/Katakana Voiced Sound Mark + 'a': "\u00e4", 'e': "\u00eb", 'i': "\u00ef", 'o': "\u00f6", 'u': "\u00fc", 'y': "\u00ff", '\u03b9': "\u03ca", '\u03c5': "\u03cb", '\u016B': "\u01D6", '\u00FA': "\u01D8", '\u01D4': "\u01DA", '\u00F9': "\u01DC", + 'A': "\u00c4", 'E': "\u00cb", 'I': "\u00cf", 'O': "\u00d6", 'U': "\u00dc", 'Y': "\u0178", '\u0399': "\u03aa", '\u03a5': "\u03ab", '\u016A': "\u01D5", '\u00DA': "\u01D7", '\u01D3': "\u01D9", '\u00D9': "\u01DB", + '\u304b': "\u304c", '\u304d': "\u304e", '\u304f': "\u3050", '\u3051': "\u3052", '\u3053': "\u3054", '\u305f': "\u3060", '\u3061': "\u3062", '\u3064': "\u3065", '\u3066': "\u3067", '\u3068': "\u3069", + '\u3055': "\u3056", '\u3057': "\u3058", '\u3059': "\u305a", '\u305b': "\u305c", '\u305d': "\u305e", '\u306f': "\u3070", '\u3072': "\u3073", '\u3075': "\u3076", '\u3078': "\u3079", '\u307b': "\u307c", + '\u30ab': "\u30ac", '\u30ad': "\u30ae", '\u30af': "\u30b0", '\u30b1': "\u30b2", '\u30b3': "\u30b4", '\u30bf': "\u30c0", '\u30c1': "\u30c2", '\u30c4': "\u30c5", '\u30c6': "\u30c7", '\u30c8': "\u30c9", + '\u30b5': "\u30b6", '\u30b7': "\u30b8", '\u30b9': "\u30ba", '\u30bb': "\u30bc", '\u30bd': "\u30be", '\u30cf': "\u30d0", '\u30d2': "\u30d3", '\u30d5': "\u30d6", '\u30d8': "\u30d9", '\u30db': "\u30dc" + }; + this.VKI_deadkey['~'] = { // Tilde / Stroke + 'a': "\u00e3", 'l': "\u0142", 'n': "\u00f1", 'o': "\u00f5", + 'A': "\u00c3", 'L': "\u0141", 'N': "\u00d1", 'O': "\u00d5" + }; + this.VKI_deadkey['^'] = { // Circumflex + 'a': "\u00e2", 'e': "\u00ea", 'i': "\u00ee", 'o': "\u00f4", 'u': "\u00fb", 'w': "\u0175", 'y': "\u0177", + 'A': "\u00c2", 'E': "\u00ca", 'I': "\u00ce", 'O': "\u00d4", 'U': "\u00db", 'W': "\u0174", 'Y': "\u0176" + }; + this.VKI_deadkey['\u02c7'] = { // Baltic caron + 'c': "\u010D", 'd': "\u010f", 'e': "\u011b", 's': "\u0161", 'l': "\u013e", 'n': "\u0148", 'r': "\u0159", 't': "\u0165", 'u': "\u01d4", 'z': "\u017E", '\u00fc': "\u01da", + 'C': "\u010C", 'D': "\u010e", 'E': "\u011a", 'S': "\u0160", 'L': "\u013d", 'N': "\u0147", 'R': "\u0158", 'T': "\u0164", 'U': "\u01d3", 'Z': "\u017D", '\u00dc': "\u01d9" + }; + this.VKI_deadkey['\u02d8'] = { // Romanian and Turkish breve + 'a': "\u0103", 'g': "\u011f", + 'A': "\u0102", 'G': "\u011e" + }; + this.VKI_deadkey['-'] = this.VKI_deadkey['\u00af'] = { // Macron + 'a': "\u0101", 'e': "\u0113", 'i': "\u012b", 'o': "\u014d", 'u': "\u016B", 'y': "\u0233", '\u00fc': "\u01d6", + 'A': "\u0100", 'E': "\u0112", 'I': "\u012a", 'O': "\u014c", 'U': "\u016A", 'Y': "\u0232", '\u00dc': "\u01d5" + }; + this.VKI_deadkey['`'] = { // Grave + 'a': "\u00e0", 'e': "\u00e8", 'i': "\u00ec", 'o': "\u00f2", 'u': "\u00f9", '\u00fc': "\u01dc", + 'A': "\u00c0", 'E': "\u00c8", 'I': "\u00cc", 'O': "\u00d2", 'U': "\u00d9", '\u00dc': "\u01db" + }; + this.VKI_deadkey["'"] = this.VKI_deadkey['\u00b4'] = this.VKI_deadkey['\u0384'] = { // Acute / Greek Tonos + 'a': "\u00e1", 'e': "\u00e9", 'i': "\u00ed", 'o': "\u00f3", 'u': "\u00fa", 'y': "\u00fd", '\u03b1': "\u03ac", '\u03b5': "\u03ad", '\u03b7': "\u03ae", '\u03b9': "\u03af", '\u03bf': "\u03cc", '\u03c5': "\u03cd", '\u03c9': "\u03ce", '\u00fc': "\u01d8", + 'A': "\u00c1", 'E': "\u00c9", 'I': "\u00cd", 'O': "\u00d3", 'U': "\u00da", 'Y': "\u00dd", '\u0391': "\u0386", '\u0395': "\u0388", '\u0397': "\u0389", '\u0399': "\u038a", '\u039f': "\u038c", '\u03a5': "\u038e", '\u03a9': "\u038f", '\u00dc': "\u01d7" + }; + this.VKI_deadkey['\u02dd'] = { // Hungarian Double Acute Accent + 'o': "\u0151", 'u': "\u0171", + 'O': "\u0150", 'U': "\u0170" + }; + this.VKI_deadkey['\u0385'] = { // Greek Dialytika + Tonos + '\u03b9': "\u0390", '\u03c5': "\u03b0" + }; + this.VKI_deadkey['\u00b0'] = this.VKI_deadkey['\u00ba'] = { // Ring + 'a': "\u00e5", 'u': "\u016f", + 'A': "\u00c5", 'U': "\u016e" + }; + this.VKI_deadkey['\u02DB'] = { // Ogonek + 'a': "\u0106", 'e': "\u0119", 'i': "\u012f", 'o': "\u01eb", 'u': "\u0173", 'y': "\u0177", + 'A': "\u0105", 'E': "\u0118", 'I': "\u012e", 'O': "\u01ea", 'U': "\u0172", 'Y': "\u0176" + }; + this.VKI_deadkey['\u02D9'] = { // Dot-above + 'c': "\u010B", 'e': "\u0117", 'g': "\u0121", 'z': "\u017C", + 'C': "\u010A", 'E': "\u0116", 'G': "\u0120", 'Z': "\u017B" + }; + this.VKI_deadkey['\u00B8'] = this.VKI_deadkey['\u201a'] = { // Cedilla + 'c': "\u00e7", 's': "\u015F", + 'C': "\u00c7", 'S': "\u015E" + }; + this.VKI_deadkey[','] = { // Comma + 's': (this.VKI_isIElt8) ? "\u015F" : "\u0219", 't': (this.VKI_isIElt8) ? "\u0163" : "\u021B", + 'S': (this.VKI_isIElt8) ? "\u015E" : "\u0218", 'T': (this.VKI_isIElt8) ? "\u0162" : "\u021A" + }; + this.VKI_deadkey['\u3002'] = { // Hiragana/Katakana Point + '\u306f': "\u3071", '\u3072': "\u3074", '\u3075': "\u3077", '\u3078': "\u307a", '\u307b': "\u307d", + '\u30cf': "\u30d1", '\u30d2': "\u30d4", '\u30d5': "\u30d7", '\u30d8': "\u30da", '\u30db': "\u30dd" + }; + + + /* ***** Define Symbols **************************************** */ + this.VKI_symbol = { + '\u00a0': "NB\nSP", '\u200b': "ZW\nSP", '\u200c': "ZW\nNJ", '\u200d': "ZW\nJ" + }; + + + /* ***** Layout Number Pad ************************************* */ + this.VKI_numpad = [ + [["$"], ["\u00a3"], ["\u20ac"], ["\u00a5"]], + [["7"], ["8"], ["9"], ["/"]], + [["4"], ["5"], ["6"], ["*"]], + [["1"], ["2"], ["3"], ["-"]], + [["0"], ["."], ["="], ["+"]] + ]; + + + /* **************************************************************** + * Attach the keyboard to an element + * + */ + VKI_attach = function(elem) { + if (elem.getAttribute("VKI_attached")) return false; + if (self.VKI_imageURI) { + var keybut = document.createElement('img'); + keybut.src = self.VKI_imageURI; + keybut.alt = self.VKI_i18n['01']; + keybut.className = "keyboardInputInitiator"; + keybut.title = self.VKI_i18n['01']; + keybut.elem = elem; + keybut.onclick = function(e) { + e = e || event; + if (e.stopPropagation) { e.stopPropagation(); } else e.cancelBubble = true; + self.VKI_show(this.elem); + }; + elem.parentNode.insertBefore(keybut, (elem.dir == "rtl") ? elem : elem.nextSibling); + } else { + elem.onfocus = function() { + if (self.VKI_target != this) { + if (self.VKI_target) self.VKI_close(); + self.VKI_show(this); + } + }; + elem.onclick = function() { + if (!self.VKI_target) self.VKI_show(this); + } + } + elem.setAttribute("VKI_attached", 'true'); + if (self.VKI_isIE) { + elem.onclick = elem.onselect = elem.onkeyup = function(e) { + if ((e || event).type != "keyup" || !this.readOnly) + this.range = document.selection.createRange(); + }; + } + VKI_addListener(elem, 'click', function(e) { + if (self.VKI_target == this) { + e = e || event; + if (e.stopPropagation) { e.stopPropagation(); } else e.cancelBubble = true; + } return false; + }, false); + if (self.VKI_isMoz) + elem.addEventListener('blur', function() { this.setAttribute('_scrollTop', this.scrollTop); }, false); + }; + + + /* ***** Find tagged input & textarea elements ***************** */ + function VKI_buildKeyboardInputs() { + var inputElems = [ + document.getElementsByTagName('input'), + document.getElementsByTagName('textarea') + ]; + for (var x = 0, elem; elem = inputElems[x++];) + for (var y = 0, ex; ex = elem[y++];) + if (ex.nodeName == "TEXTAREA" || ex.type == "text" || ex.type == "password") + if (ex.className.indexOf("keyboardInput") > -1) VKI_attach(ex); + + VKI_addListener(document.documentElement, 'click', function(e) { self.VKI_close(); }, false); + } + + + /* **************************************************************** + * Common mouse event actions + * + */ + function VKI_mouseEvents(elem) { + if (elem.nodeName == "TD") { + if (!elem.click) elem.click = function() { + var evt = this.ownerDocument.createEvent('MouseEvents'); + evt.initMouseEvent('click', true, true, this.ownerDocument.defaultView, 1, 0, 0, 0, 0, false, false, false, false, 0, null); + this.dispatchEvent(evt); + }; + elem.VKI_clickless = 0; + VKI_addListener(elem, 'dblclick', function() { return false; }, false); + } + VKI_addListener(elem, 'mouseover', function() { + if (this.nodeName == "TD" && self.VKI_clickless) { + var _self = this; + clearTimeout(this.VKI_clickless); + this.VKI_clickless = setTimeout(function() { _self.click(); }, self.VKI_clickless); + } + if (self.VKI_isIE) this.className += " hover"; + }, false); + VKI_addListener(elem, 'mouseout', function() { + if (this.nodeName == "TD") clearTimeout(this.VKI_clickless); + if (self.VKI_isIE) this.className = this.className.replace(/ ?(hover|pressed) ?/g, ""); + }, false); + VKI_addListener(elem, 'mousedown', function() { + if (this.nodeName == "TD") clearTimeout(this.VKI_clickless); + if (self.VKI_isIE) this.className += " pressed"; + }, false); + VKI_addListener(elem, 'mouseup', function() { + if (this.nodeName == "TD") clearTimeout(this.VKI_clickless); + if (self.VKI_isIE) this.className = this.className.replace(/ ?pressed ?/g, ""); + }, false); + } + + + /* ***** Build the keyboard interface ************************** */ + this.VKI_keyboard = document.createElement('table'); + this.VKI_keyboard.id = "keyboardInputMaster"; + this.VKI_keyboard.dir = "ltr"; + this.VKI_keyboard.cellSpacing = "0"; + this.VKI_keyboard.reflow = function() { + this.style.width = "50px"; + var foo = this.offsetWidth; + this.style.width = ""; + }; + VKI_addListener(this.VKI_keyboard, 'click', function(e) { + e = e || event; + if (e.stopPropagation) { e.stopPropagation(); } else e.cancelBubble = true; + return false; + }, false); + + if (!this.VKI_layout[this.VKI_kt]) + return alert('No keyboard named "' + this.VKI_kt + '"'); + + this.VKI_langCode = {}; + var thead = document.createElement('thead'); + var tr = document.createElement('tr'); + var th = document.createElement('th'); + th.colSpan = "2"; + + var kbSelect = document.createElement('div'); + kbSelect.title = this.VKI_i18n['02']; + VKI_addListener(kbSelect, 'click', function() { + var ol = this.getElementsByTagName('ol')[0]; + if (!ol.style.display) { + ol.style.display = "block"; + var li = ol.getElementsByTagName('li'); + for (var x = 0, scr = 0; x < li.length; x++) { + if (VKI_kt == li[x].firstChild.nodeValue) { + li[x].className = "selected"; + scr = li[x].offsetTop - li[x].offsetHeight * 2; + } else li[x].className = ""; + } setTimeout(function() { ol.scrollTop = scr; }, 0); + } else ol.style.display = ""; + }, false); + kbSelect.appendChild(document.createTextNode(this.VKI_kt)); + kbSelect.appendChild(document.createTextNode(this.VKI_isIElt8 ? " \u2193" : " \u25be")); + kbSelect.langCount = 0; + var ol = document.createElement('ol'); + for (ktype in this.VKI_layout) { + if (typeof this.VKI_layout[ktype] == "object") { + if (!this.VKI_layout[ktype].lang) this.VKI_layout[ktype].lang = []; + for (var x = 0; x < this.VKI_layout[ktype].lang.length; x++) + this.VKI_langCode[this.VKI_layout[ktype].lang[x].toLowerCase().replace(/-/g, "_")] = ktype; + var li = document.createElement('li'); + li.title = this.VKI_layout[ktype].name; + VKI_addListener(li, 'click', function(e) { + e = e || event; + if (e.stopPropagation) { e.stopPropagation(); } else e.cancelBubble = true; + this.parentNode.style.display = ""; + self.VKI_kts = self.VKI_kt = kbSelect.firstChild.nodeValue = this.firstChild.nodeValue; + self.VKI_buildKeys(); + self.VKI_position(true); + }, false); + VKI_mouseEvents(li); + li.appendChild(document.createTextNode(ktype)); + ol.appendChild(li); + kbSelect.langCount++; + } + } kbSelect.appendChild(ol); + if (kbSelect.langCount > 1) th.appendChild(kbSelect); + this.VKI_langCode.index = []; + for (prop in this.VKI_langCode) + if (prop != "index" && typeof this.VKI_langCode[prop] == "string") + this.VKI_langCode.index.push(prop); + this.VKI_langCode.index.sort(); + this.VKI_langCode.index.reverse(); + + if (this.VKI_numberPad) { + var span = document.createElement('span'); + span.appendChild(document.createTextNode("#")); + span.title = this.VKI_i18n['00']; + VKI_addListener(span, 'click', function() { + kbNumpad.style.display = (!kbNumpad.style.display) ? "none" : ""; + self.VKI_position(true); + }, false); + VKI_mouseEvents(span); + th.appendChild(span); + } + + this.VKI_kbsize = function(e) { + self.VKI_size = Math.min(5, Math.max(1, self.VKI_size)); + self.VKI_keyboard.className = self.VKI_keyboard.className.replace(/ ?keyboardInputSize\d ?/, ""); + if (self.VKI_size != 2) self.VKI_keyboard.className += " keyboardInputSize" + self.VKI_size; + self.VKI_position(true); + if (self.VKI_isOpera) self.VKI_keyboard.reflow(); + }; + if (this.VKI_sizeAdj) { + var small = document.createElement('small'); + small.title = this.VKI_i18n['10']; + VKI_addListener(small, 'click', function() { + --self.VKI_size; + self.VKI_kbsize(); + }, false); + VKI_mouseEvents(small); + small.appendChild(document.createTextNode(this.VKI_isIElt8 ? "\u2193" : "\u21d3")); + th.appendChild(small); + var big = document.createElement('big'); + big.title = this.VKI_i18n['11']; + VKI_addListener(big, 'click', function() { + ++self.VKI_size; + self.VKI_kbsize(); + }, false); + VKI_mouseEvents(big); + big.appendChild(document.createTextNode(this.VKI_isIElt8 ? "\u2191" : "\u21d1")); + th.appendChild(big); + } + + var span = document.createElement('span'); + span.appendChild(document.createTextNode(this.VKI_i18n['07'])); + span.title = this.VKI_i18n['08']; + VKI_addListener(span, 'click', function() { + self.VKI_target.value = ""; + self.VKI_target.focus(); + return false; + }, false); + VKI_mouseEvents(span); + th.appendChild(span); + + var strong = document.createElement('strong'); + strong.appendChild(document.createTextNode('X')); + strong.title = this.VKI_i18n['06']; + VKI_addListener(strong, 'click', function() { self.VKI_close(); }, false); + VKI_mouseEvents(strong); + th.appendChild(strong); + + tr.appendChild(th); + thead.appendChild(tr); + this.VKI_keyboard.appendChild(thead); + + var tbody = document.createElement('tbody'); + var tr = document.createElement('tr'); + var td = document.createElement('td'); + var div = document.createElement('div'); + + if (this.VKI_deadBox) { + var label = document.createElement('label'); + var checkbox = document.createElement('input'); + checkbox.type = "checkbox"; + checkbox.title = this.VKI_i18n['03'] + ": " + ((this.VKI_deadkeysOn) ? this.VKI_i18n['04'] : this.VKI_i18n['05']); + checkbox.defaultChecked = this.VKI_deadkeysOn; + VKI_addListener(checkbox, 'click', function() { + this.title = self.VKI_i18n['03'] + ": " + ((this.checked) ? self.VKI_i18n['04'] : self.VKI_i18n['05']); + self.VKI_modify(""); + return true; + }, false); + label.appendChild(checkbox); + checkbox.checked = this.VKI_deadkeysOn; + div.appendChild(label); + this.VKI_deadkeysOn = checkbox; + } else this.VKI_deadkeysOn.checked = this.VKI_deadkeysOn; + + if (this.VKI_showVersion) { + var vr = document.createElement('var'); + vr.title = this.VKI_i18n['09'] + " " + this.VKI_version; + vr.appendChild(document.createTextNode("v" + this.VKI_version)); + div.appendChild(vr); + } td.appendChild(div); + tr.appendChild(td); + + var kbNumpad = document.createElement('td'); + kbNumpad.id = "keyboardInputNumpad"; + if (!this.VKI_numberPadOn) kbNumpad.style.display = "none"; + var ntable = document.createElement('table'); + ntable.cellSpacing = "0"; + var ntbody = document.createElement('tbody'); + for (var x = 0; x < this.VKI_numpad.length; x++) { + var ntr = document.createElement('tr'); + for (var y = 0; y < this.VKI_numpad[x].length; y++) { + var ntd = document.createElement('td'); + VKI_addListener(ntd, 'click', VKI_keyClick, false); + VKI_mouseEvents(ntd); + ntd.appendChild(document.createTextNode(this.VKI_numpad[x][y])); + ntr.appendChild(ntd); + } ntbody.appendChild(ntr); + } ntable.appendChild(ntbody); + kbNumpad.appendChild(ntable); + tr.appendChild(kbNumpad); + tbody.appendChild(tr); + this.VKI_keyboard.appendChild(tbody); + + if (this.VKI_isIE6) { + this.VKI_iframe = document.createElement('iframe'); + this.VKI_iframe.style.position = "absolute"; + this.VKI_iframe.style.border = "0px none"; + this.VKI_iframe.style.filter = "mask()"; + this.VKI_iframe.style.zIndex = "999999"; + this.VKI_iframe.src = this.VKI_imageURI; + } + + + /* **************************************************************** + * Private table cell attachment function for generic characters + * + */ + function VKI_keyClick() { + var done = false, character = "\xa0"; + if (this.firstChild.nodeName.toLowerCase() != "small") { + if ((character = this.firstChild.nodeValue) == "\xa0") return false; + } else character = this.firstChild.getAttribute('char'); + if (self.VKI_deadkeysOn.checked && self.VKI_dead) { + if (self.VKI_dead != character) { + if (character != " ") { + if (self.VKI_deadkey[self.VKI_dead][character]) { + self.VKI_insert(self.VKI_deadkey[self.VKI_dead][character]); + done = true; + } + } else { + self.VKI_insert(self.VKI_dead); + done = true; + } + } else done = true; + } self.VKI_dead = false; + + if (!done) { + if (self.VKI_deadkeysOn.checked && self.VKI_deadkey[character]) { + self.VKI_dead = character; + this.className += " dead"; + if (self.VKI_shift) self.VKI_modify("Shift"); + if (self.VKI_altgr) self.VKI_modify("AltGr"); + } else self.VKI_insert(character); + } self.VKI_modify(""); + return false; + } + + + /* **************************************************************** + * Build or rebuild the keyboard keys + * + */ + this.VKI_buildKeys = function() { + this.VKI_shift = this.VKI_shiftlock = this.VKI_altgr = this.VKI_altgrlock = this.VKI_dead = false; + var container = this.VKI_keyboard.tBodies[0].getElementsByTagName('div')[0]; + var tables = container.getElementsByTagName('table'); + for (var x = tables.length - 1; x >= 0; x--) container.removeChild(tables[x]); + + for (var x = 0, hasDeadKey = false, lyt; lyt = this.VKI_layout[this.VKI_kt].keys[x++];) { + var table = document.createElement('table'); + table.cellSpacing = "0"; + if (lyt.length <= this.VKI_keyCenter) table.className = "keyboardInputCenter"; + var tbody = document.createElement('tbody'); + var tr = document.createElement('tr'); + for (var y = 0, lkey; lkey = lyt[y++];) { + var td = document.createElement('td'); + if (this.VKI_symbol[lkey[0]]) { + var text = this.VKI_symbol[lkey[0]].split("\n"); + var small = document.createElement('small'); + small.setAttribute('char', lkey[0]); + for (var z = 0; z < text.length; z++) { + if (z) small.appendChild(document.createElement("br")); + small.appendChild(document.createTextNode(text[z])); + } td.appendChild(small); + } else td.appendChild(document.createTextNode(lkey[0] || "\xa0")); + + var className = []; + if (this.VKI_deadkeysOn.checked) + for (key in this.VKI_deadkey) + if (key === lkey[0]) { className.push("deadkey"); break; } + if (lyt.length > this.VKI_keyCenter && y == lyt.length) className.push("last"); + if (lkey[0] == " " || lkey[1] == " ") className.push("space"); + td.className = className.join(" "); + + switch (lkey[1]) { + case "Caps": case "Shift": + case "Alt": case "AltGr": case "AltLk": + VKI_addListener(td, 'click', (function(type) { return function() { self.VKI_modify(type); return false; }})(lkey[1]), false); + break; + case "Tab": + VKI_addListener(td, 'click', function() { + if (self.VKI_activeTab) { + if (self.VKI_target.form) { + var target = self.VKI_target, elems = target.form.elements; + self.VKI_close(); + for (var z = 0, me = false, j = -1; z < elems.length; z++) { + if (j == -1 && elems[z].getAttribute("VKI_attached")) j = z; + if (me) { + if (self.VKI_activeTab == 1 && elems[z]) break; + if (elems[z].getAttribute("VKI_attached")) break; + } else if (elems[z] == target) me = true; + } if (z == elems.length) z = Math.max(j, 0); + if (elems[z].getAttribute("VKI_attached")) { + self.VKI_show(elems[z]); + } else elems[z].focus(); + } else self.VKI_target.focus(); + } else self.VKI_insert("\t"); + return false; + }, false); + break; + case "Bksp": + VKI_addListener(td, 'click', function() { + self.VKI_target.focus(); + if (self.VKI_target.setSelectionRange && !self.VKI_target.readOnly) { + var rng = [self.VKI_target.selectionStart, self.VKI_target.selectionEnd]; + if (rng[0] < rng[1]) rng[0]++; + self.VKI_target.value = self.VKI_target.value.substr(0, rng[0] - 1) + self.VKI_target.value.substr(rng[1]); + self.VKI_target.setSelectionRange(rng[0] - 1, rng[0] - 1); + } else if (self.VKI_target.createTextRange && !self.VKI_target.readOnly) { + try { + self.VKI_target.range.select(); + } catch(e) { self.VKI_target.range = document.selection.createRange(); } + if (!self.VKI_target.range.text.length) self.VKI_target.range.moveStart('character', -1); + self.VKI_target.range.text = ""; + } else self.VKI_target.value = self.VKI_target.value.substr(0, self.VKI_target.value.length - 1); + if (self.VKI_shift) self.VKI_modify("Shift"); + if (self.VKI_altgr) self.VKI_modify("AltGr"); + self.VKI_target.focus(); + return true; + }, false); + break; + case "Enter": + VKI_addListener(td, 'click', function() { + if (self.VKI_target.nodeName != "TEXTAREA") { + if (self.VKI_enterSubmit && self.VKI_target.form) { + for (var z = 0, subm = false; z < self.VKI_target.form.elements.length; z++) + if (self.VKI_target.form.elements[z].type == "submit") subm = true; + if (!subm) self.VKI_target.form.submit(); + } + self.VKI_close(); + } else self.VKI_insert("\n"); + return true; + }, false); + break; + default: + VKI_addListener(td, 'click', VKI_keyClick, false); + + } VKI_mouseEvents(td); + tr.appendChild(td); + for (var z = 0; z < 4; z++) + if (this.VKI_deadkey[lkey[z] = lkey[z] || ""]) hasDeadKey = true; + } tbody.appendChild(tr); + table.appendChild(tbody); + container.appendChild(table); + } + if (this.VKI_deadBox) + this.VKI_deadkeysOn.style.display = (hasDeadKey) ? "inline" : "none"; + if (this.VKI_isIE6) { + this.VKI_iframe.style.width = this.VKI_keyboard.offsetWidth + "px"; + this.VKI_iframe.style.height = this.VKI_keyboard.offsetHeight + "px"; + } + }; + + this.VKI_buildKeys(); + VKI_addListener(this.VKI_keyboard, 'selectstart', function() { return false; }, false); + this.VKI_keyboard.unselectable = "on"; + if (this.VKI_isOpera) + VKI_addListener(this.VKI_keyboard, 'mousedown', function() { return false; }, false); + + + /* **************************************************************** + * Controls modifier keys + * + */ + this.VKI_modify = function(type) { + switch (type) { + case "Alt": + case "AltGr": this.VKI_altgr = !this.VKI_altgr; break; + case "AltLk": this.VKI_altgr = 0; this.VKI_altgrlock = !this.VKI_altgrlock; break; + case "Caps": this.VKI_shift = 0; this.VKI_shiftlock = !this.VKI_shiftlock; break; + case "Shift": this.VKI_shift = !this.VKI_shift; break; + } var vchar = 0; + if (!this.VKI_shift != !this.VKI_shiftlock) vchar += 1; + if (!this.VKI_altgr != !this.VKI_altgrlock) vchar += 2; + + var tables = this.VKI_keyboard.tBodies[0].getElementsByTagName('div')[0].getElementsByTagName('table'); + for (var x = 0; x < tables.length; x++) { + var tds = tables[x].getElementsByTagName('td'); + for (var y = 0; y < tds.length; y++) { + var className = [], lkey = this.VKI_layout[this.VKI_kt].keys[x][y]; + + switch (lkey[1]) { + case "Alt": + case "AltGr": + if (this.VKI_altgr) className.push("pressed"); + break; + case "AltLk": + if (this.VKI_altgrlock) className.push("pressed"); + break; + case "Shift": + if (this.VKI_shift) className.push("pressed"); + break; + case "Caps": + if (this.VKI_shiftlock) className.push("pressed"); + break; + case "Tab": case "Enter": case "Bksp": break; + default: + if (type) { + tds[y].removeChild(tds[y].firstChild); + if (this.VKI_symbol[lkey[vchar]]) { + var text = this.VKI_symbol[lkey[vchar]].split("\n"); + var small = document.createElement('small'); + small.setAttribute('char', lkey[vchar]); + for (var z = 0; z < text.length; z++) { + if (z) small.appendChild(document.createElement("br")); + small.appendChild(document.createTextNode(text[z])); + } tds[y].appendChild(small); + } else tds[y].appendChild(document.createTextNode(lkey[vchar] || "\xa0")); + } + if (this.VKI_deadkeysOn.checked) { + var character = tds[y].firstChild.nodeValue || tds[y].firstChild.className; + if (this.VKI_dead) { + if (character == this.VKI_dead) className.push("pressed"); + if (this.VKI_deadkey[this.VKI_dead][character]) className.push("target"); + } + if (this.VKI_deadkey[character]) className.push("deadkey"); + } + } + + if (y == tds.length - 1 && tds.length > this.VKI_keyCenter) className.push("last"); + if (lkey[0] == " " || lkey[1] == " ") className.push("space"); + tds[y].className = className.join(" "); + } + } + }; + + + /* **************************************************************** + * Insert text at the cursor + * + */ + this.VKI_insert = function(text) { + /*this.VKI_target.focus(); + if (this.VKI_target.maxLength) this.VKI_target.maxlength = this.VKI_target.maxLength; + if (typeof this.VKI_target.maxlength == "undefined" || + this.VKI_target.maxlength < 0 || + this.VKI_target.value.length < this.VKI_target.maxlength) { + if (this.VKI_target.setSelectionRange && !this.VKI_target.readOnly && !this.VKI_isIE) { + var rng = [this.VKI_target.selectionStart, this.VKI_target.selectionEnd]; + this.VKI_target.value = this.VKI_target.value.substr(0, rng[0]) + text + this.VKI_target.value.substr(rng[1]); + if (text == "\n" && this.VKI_isOpera) rng[0]++; + this.VKI_target.setSelectionRange(rng[0] + text.length, rng[0] + text.length); + } else if (this.VKI_target.createTextRange && !this.VKI_target.readOnly) { + try { + this.VKI_target.range.select(); + } catch(e) { this.VKI_target.range = document.selection.createRange(); } + this.VKI_target.range.text = text; + this.VKI_target.range.collapse(true); + this.VKI_target.range.select(); + } else this.VKI_target.value += text; + if (this.VKI_shift) this.VKI_modify("Shift"); + if (this.VKI_altgr) this.VKI_modify("AltGr"); + this.VKI_target.focus(); + } else if (this.VKI_target.createTextRange && this.VKI_target.range) + this.VKI_target.range.select(); + */ + //alert(text); + document.getElementById("keyboardText").value =text; + document.getElementById("keyboardText").focus(); + }; + + + /* **************************************************************** + * Show the keyboard interface + * + */ + this.VKI_show = function(elem) { + if (!this.VKI_target) { + this.VKI_target = elem; + if (this.VKI_langAdapt && this.VKI_target.lang) { + var chg = false, sub = [], lang = this.VKI_target.lang.toLowerCase().replace(/-/g, "_"); + for (var x = 0, chg = false; !chg && x < this.VKI_langCode.index.length; x++) + if (lang.indexOf(this.VKI_langCode.index[x]) == 0) + chg = kbSelect.firstChild.nodeValue = this.VKI_kt = this.VKI_langCode[this.VKI_langCode.index[x]]; + if (chg) this.VKI_buildKeys(); + } + if (this.VKI_isIE) { + if (!this.VKI_target.range) { + this.VKI_target.range = this.VKI_target.createTextRange(); + this.VKI_target.range.moveStart('character', this.VKI_target.value.length); + } this.VKI_target.range.select(); + } + try { this.VKI_keyboard.parentNode.removeChild(this.VKI_keyboard); } catch (e) {} + if (this.VKI_clearPasswords && this.VKI_target.type == "password") this.VKI_target.value = ""; + + var elem = this.VKI_target; + this.VKI_target.keyboardPosition = "absolute"; + do { + if (VKI_getStyle(elem, "position") == "fixed") { + this.VKI_target.keyboardPosition = "fixed"; + break; + } + } while (elem = elem.offsetParent); + + if (this.VKI_isIE6) document.body.appendChild(this.VKI_iframe); + document.body.appendChild(this.VKI_keyboard); + this.VKI_keyboard.style.position = this.VKI_target.keyboardPosition; + if (this.VKI_isOpera) this.VKI_keyboard.reflow(); + + this.VKI_position(true); + if (self.VKI_isMoz || self.VKI_isWebKit) this.VKI_position(true); + this.VKI_target.blur(); + this.VKI_target.focus(); + } else this.VKI_close(); + }; + + + /* **************************************************************** + * Position the keyboard + * + */ + this.VKI_position = function(force) { + if (self.VKI_target) { + var kPos = VKI_findPos(self.VKI_keyboard), wDim = VKI_innerDimensions(), sDis = VKI_scrollDist(); + var place = false, fudge = self.VKI_target.offsetHeight + 3; + if (force !== true) { + if (kPos[1] + self.VKI_keyboard.offsetHeight - sDis[1] - wDim[1] > 0) { + place = true; + fudge = -self.VKI_keyboard.offsetHeight - 3; + } else if (kPos[1] - sDis[1] < 0) place = true; + } + if (place || force === true) { + var iPos = VKI_findPos(self.VKI_target), scr = self.VKI_target; + while (scr = scr.parentNode) { + if (scr == document.body) break; + if (scr.scrollHeight > scr.offsetHeight || scr.scrollWidth > scr.offsetWidth) { + if (!scr.getAttribute("VKI_scrollListener")) { + scr.setAttribute("VKI_scrollListener", true); + VKI_addListener(scr, 'scroll', function() { self.VKI_position(true); }, false); + } // Check if the input is in view + var pPos = VKI_findPos(scr), oTop = iPos[1] - pPos[1], oLeft = iPos[0] - pPos[0]; + var top = oTop + self.VKI_target.offsetHeight; + var left = oLeft + self.VKI_target.offsetWidth; + var bottom = scr.offsetHeight - oTop - self.VKI_target.offsetHeight; + var right = scr.offsetWidth - oLeft - self.VKI_target.offsetWidth; + self.VKI_keyboard.style.display = (top < 0 || left < 0 || bottom < 0 || right < 0) ? "none" : ""; + if (self.VKI_isIE6) self.VKI_iframe.style.display = (top < 0 || left < 0 || bottom < 0 || right < 0) ? "none" : ""; + } + } + self.VKI_keyboard.style.top = iPos[1] - ((self.VKI_target.keyboardPosition == "fixed" && !self.VKI_isIE && !self.VKI_isMoz) ? sDis[1] : 0) + fudge + "px"; + self.VKI_keyboard.style.left = Math.max(10, Math.min(wDim[0] - self.VKI_keyboard.offsetWidth - 25, iPos[0])) + "px"; + if (self.VKI_isIE6) { + self.VKI_iframe.style.width = self.VKI_keyboard.offsetWidth + "px"; + self.VKI_iframe.style.height = self.VKI_keyboard.offsetHeight + "px"; + self.VKI_iframe.style.top = self.VKI_keyboard.style.top; + self.VKI_iframe.style.left = self.VKI_keyboard.style.left; + } + } + if (force === true) self.VKI_position(); + } + }; + + + /* **************************************************************** + * Close the keyboard interface + * + */ + this.VKI_close = VKI_close = function() { + if (this.VKI_target) { + try { + this.VKI_keyboard.parentNode.removeChild(this.VKI_keyboard); + if (this.VKI_isIE6) this.VKI_iframe.parentNode.removeChild(this.VKI_iframe); + } catch (e) {} + if (this.VKI_kt != this.VKI_kts) { + kbSelect.firstChild.nodeValue = this.VKI_kt = this.VKI_kts; + this.VKI_buildKeys(); + } kbSelect.getElementsByTagName('ol')[0].style.display = "";; + this.VKI_target.focus(); + if (this.VKI_isIE) { + setTimeout(function() { self.VKI_target = false; }, 0); + } else this.VKI_target = false; + } + }; + + + /* ***** Private functions *************************************** */ + function VKI_addListener(elem, type, func, cap) { + if (elem.addEventListener) { + elem.addEventListener(type, function(e) { func.call(elem, e); }, cap); + } else if (elem.attachEvent) + elem.attachEvent('on' + type, function() { func.call(elem); }); + } + + function VKI_findPos(obj) { + var curleft = curtop = 0, scr = obj; + while ((scr = scr.parentNode) && scr != document.body) { + curleft -= scr.scrollLeft || 0; + curtop -= scr.scrollTop || 0; + } + do { + curleft += obj.offsetLeft; + curtop += obj.offsetTop; + } while (obj = obj.offsetParent); + return [curleft, curtop]; + } + + function VKI_innerDimensions() { + if (self.innerHeight) { + return [self.innerWidth, self.innerHeight]; + } else if (document.documentElement && document.documentElement.clientHeight) { + return [document.documentElement.clientWidth, document.documentElement.clientHeight]; + } else if (document.body) + return [document.body.clientWidth, document.body.clientHeight]; + return [0, 0]; + } + + function VKI_scrollDist() { + var html = document.getElementsByTagName('html')[0]; + if (html.scrollTop && document.documentElement.scrollTop) { + return [html.scrollLeft, html.scrollTop]; + } else if (html.scrollTop || document.documentElement.scrollTop) { + return [html.scrollLeft + document.documentElement.scrollLeft, html.scrollTop + document.documentElement.scrollTop]; + } else if (document.body.scrollTop) + return [document.body.scrollLeft, document.body.scrollTop]; + return [0, 0]; + } + + function VKI_getStyle(obj, styleProp) { + if (obj.currentStyle) { + var y = obj.currentStyle[styleProp]; + } else if (window.getComputedStyle) + var y = window.getComputedStyle(obj, null)[styleProp]; + return y; + } + + + VKI_addListener(window, 'resize', this.VKI_position, false); + VKI_addListener(window, 'scroll', this.VKI_position, false); + this.VKI_kbsize(); + VKI_addListener(window, 'load', VKI_buildKeyboardInputs, false); + // VKI_addListener(window, 'load', function() { + // setTimeout(VKI_buildKeyboardInputs, 5); + // }, false); +})(); diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/keyboard.css b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/keyboard.css new file mode 100644 index 0000000000000000000000000000000000000000..3106fae2e8c63104975a137f2d0e88de428e7a76 --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/keyboard.css @@ -0,0 +1,822 @@ + + + + + +<!DOCTYPE html> +<html class=" "> + <head prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# object: http://ogp.me/ns/object# article: http://ogp.me/ns/article# profile: http://ogp.me/ns/profile#"> + <meta charset='utf-8'> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + + + <title>novnclient/noVNC/keyboard.css at master · mpastyl/novnclient · GitHub</title> + <link rel="search" type="application/opensearchdescription+xml" href="/opensearch.xml" title="GitHub" /> + <link rel="fluid-icon" href="https://github.com/fluidicon.png" title="GitHub" /> + <link rel="apple-touch-icon" sizes="57x57" href="/apple-touch-icon-114.png" /> + <link rel="apple-touch-icon" sizes="114x114" href="/apple-touch-icon-114.png" /> + <link rel="apple-touch-icon" sizes="72x72" href="/apple-touch-icon-144.png" /> + <link rel="apple-touch-icon" sizes="144x144" href="/apple-touch-icon-144.png" /> + <meta property="fb:app_id" content="1401488693436528"/> + + <meta content="@github" name="twitter:site" /><meta content="summary" name="twitter:card" /><meta content="mpastyl/novnclient" name="twitter:title" /><meta content="Contribute to novnclient development by creating an account on GitHub." name="twitter:description" /><meta content="https://avatars1.githubusercontent.com/u/5429831?s=400" name="twitter:image:src" /> +<meta content="GitHub" property="og:site_name" /><meta content="object" property="og:type" /><meta content="https://avatars1.githubusercontent.com/u/5429831?s=400" property="og:image" /><meta content="mpastyl/novnclient" property="og:title" /><meta content="https://github.com/mpastyl/novnclient" property="og:url" /><meta content="Contribute to novnclient development by creating an account on GitHub." property="og:description" /> + + <link rel="assets" href="https://assets-cdn.github.com/"> + <link rel="conduit-xhr" href="https://ghconduit.com:25035"> + + + <meta name="msapplication-TileImage" content="/windows-tile.png" /> + <meta name="msapplication-TileColor" content="#ffffff" /> + <meta name="selected-link" value="repo_source" data-pjax-transient /> + <meta name="google-analytics" content="UA-3769691-2"> + + <meta content="collector.githubapp.com" name="octolytics-host" /><meta content="collector-cdn.github.com" name="octolytics-script-host" /><meta content="github" name="octolytics-app-id" /><meta content="53D466BB:410C:67762E4:53A7FBF6" name="octolytics-dimension-request_id" /> + + + + + <link rel="icon" type="image/x-icon" href="https://assets-cdn.github.com/favicon.ico" /> + + + <meta content="authenticity_token" name="csrf-param" /> +<meta content="otzxs2nywWKWtPlXLaFoTb6yrwrwrLKPXjDF617yAT2FSUD+EWAvHWXGGSm3HIzZEdvx5mztPRaTF+ZdpgL25g==" name="csrf-token" /> + + <link href="https://assets-cdn.github.com/assets/github-eb5c3c423cbc57fa389bbe6f9a4bb3a6ce0cf4cf.css" media="all" rel="stylesheet" type="text/css" /> + <link href="https://assets-cdn.github.com/assets/github2-56e008b7d97b268cc33e7f96ed49822d7fc3367f.css" media="all" rel="stylesheet" type="text/css" /> + + + + <meta http-equiv="x-pjax-version" content="996bc2eb6e3462b6270ddd64255dc644"> + + + <meta name="description" content="Contribute to novnclient development by creating an account on GitHub." /> + + + <meta content="5429831" name="octolytics-dimension-user_id" /><meta content="mpastyl" name="octolytics-dimension-user_login" /><meta content="13266122" name="octolytics-dimension-repository_id" /><meta content="mpastyl/novnclient" name="octolytics-dimension-repository_nwo" /><meta content="true" name="octolytics-dimension-repository_public" /><meta content="false" name="octolytics-dimension-repository_is_fork" /><meta content="13266122" name="octolytics-dimension-repository_network_root_id" /><meta content="mpastyl/novnclient" name="octolytics-dimension-repository_network_root_nwo" /> + <link href="https://github.com/mpastyl/novnclient/commits/master.atom" rel="alternate" title="Recent Commits to novnclient:master" type="application/atom+xml" /> + + </head> + + + <body class="logged_out env-production vis-public page-blob"> + <a href="#start-of-content" tabindex="1" class="accessibility-aid js-skip-to-content">Skip to content</a> + <div class="wrapper"> + + + + + + + + <div class="header header-logged-out"> + <div class="container clearfix"> + + <a class="header-logo-wordmark" href="https://github.com/"> + <span class="mega-octicon octicon-logo-github"></span> + </a> + + <div class="header-actions"> + <a class="button primary" href="/join">Sign up</a> + <a class="button signin" href="/login?return_to=%2Fmpastyl%2Fnovnclient%2Fblob%2Fmaster%2FnoVNC%2Fkeyboard.css">Sign in</a> + </div> + + <div class="command-bar js-command-bar in-repository"> + + <ul class="top-nav"> + <li class="explore"><a href="/explore">Explore</a></li> + <li class="features"><a href="/features">Features</a></li> + <li class="enterprise"><a href="https://enterprise.github.com/">Enterprise</a></li> + <li class="blog"><a href="/blog">Blog</a></li> + </ul> + <form accept-charset="UTF-8" action="/search" class="command-bar-form" id="top_search_form" method="get"> + +<div class="commandbar"> + <span class="message"></span> + <input type="text" data-hotkey="s, /" name="q" id="js-command-bar-field" placeholder="Search or type a command" tabindex="1" autocapitalize="off" + + + data-repo="mpastyl/novnclient" + data-branch="master" + data-sha="6b5946495653a0a5176a290bce020146f45ea428" + > + <div class="display hidden"></div> +</div> + + <input type="hidden" name="nwo" value="mpastyl/novnclient" /> + + <div class="select-menu js-menu-container js-select-menu search-context-select-menu"> + <span class="minibutton select-menu-button js-menu-target" role="button" aria-haspopup="true"> + <span class="js-select-button">This repository</span> + </span> + + <div class="select-menu-modal-holder js-menu-content js-navigation-container" aria-hidden="true"> + <div class="select-menu-modal"> + + <div class="select-menu-item js-navigation-item js-this-repository-navigation-item selected"> + <span class="select-menu-item-icon octicon octicon-check"></span> + <input type="radio" class="js-search-this-repository" name="search_target" value="repository" checked="checked" /> + <div class="select-menu-item-text js-select-button-text">This repository</div> + </div> <!-- /.select-menu-item --> + + <div class="select-menu-item js-navigation-item js-all-repositories-navigation-item"> + <span class="select-menu-item-icon octicon octicon-check"></span> + <input type="radio" name="search_target" value="global" /> + <div class="select-menu-item-text js-select-button-text">All repositories</div> + </div> <!-- /.select-menu-item --> + + </div> + </div> + </div> + + <span class="help tooltipped tooltipped-s" aria-label="Show command bar help"> + <span class="octicon octicon-question"></span> + </span> + + + <input type="hidden" name="ref" value="cmdform"> + +</form> + </div> + + </div> +</div> + + + + <div id="start-of-content" class="accessibility-aid"></div> + <div class="site" itemscope itemtype="http://schema.org/WebPage"> + <div id="js-flash-container"> + + </div> + <div class="pagehead repohead instapaper_ignore readability-menu"> + <div class="container"> + + +<ul class="pagehead-actions"> + + + <li> + <a href="/login?return_to=%2Fmpastyl%2Fnovnclient" + class="minibutton with-count star-button tooltipped tooltipped-n" + aria-label="You must be signed in to star a repository" rel="nofollow"> + <span class="octicon octicon-star"></span> + Star + </a> + + <a class="social-count js-social-count" href="/mpastyl/novnclient/stargazers"> + 0 + </a> + + </li> + + <li> + <a href="/login?return_to=%2Fmpastyl%2Fnovnclient" + class="minibutton with-count js-toggler-target fork-button tooltipped tooltipped-n" + aria-label="You must be signed in to fork a repository" rel="nofollow"> + <span class="octicon octicon-repo-forked"></span> + Fork + </a> + <a href="/mpastyl/novnclient/network" class="social-count"> + 0 + </a> + </li> +</ul> + + <h1 itemscope itemtype="http://data-vocabulary.org/Breadcrumb" class="entry-title public"> + <span class="repo-label"><span>public</span></span> + <span class="mega-octicon octicon-repo"></span> + <span class="author"><a href="/mpastyl" class="url fn" itemprop="url" rel="author"><span itemprop="title">mpastyl</span></a></span><!-- + --><span class="path-divider">/</span><!-- + --><strong><a href="/mpastyl/novnclient" class="js-current-repository js-repo-home-link">novnclient</a></strong> + + <span class="page-context-loader"> + <img alt="" height="16" src="https://assets-cdn.github.com/images/spinners/octocat-spinner-32.gif" width="16" /> + </span> + + </h1> + </div><!-- /.container --> + </div><!-- /.repohead --> + + <div class="container"> + <div class="repository-with-sidebar repo-container new-discussion-timeline js-new-discussion-timeline "> + <div class="repository-sidebar clearfix"> + + +<div class="sunken-menu vertical-right repo-nav js-repo-nav js-repository-container-pjax js-octicon-loaders"> + <div class="sunken-menu-contents"> + <ul class="sunken-menu-group"> + <li class="tooltipped tooltipped-w" aria-label="Code"> + <a href="/mpastyl/novnclient" aria-label="Code" class="selected js-selected-navigation-item sunken-menu-item" data-hotkey="g c" data-pjax="true" data-selected-links="repo_source repo_downloads repo_commits repo_releases repo_tags repo_branches /mpastyl/novnclient"> + <span class="octicon octicon-code"></span> <span class="full-word">Code</span> + <img alt="" class="mini-loader" height="16" src="https://assets-cdn.github.com/images/spinners/octocat-spinner-32.gif" width="16" /> +</a> </li> + + <li class="tooltipped tooltipped-w" aria-label="Issues"> + <a href="/mpastyl/novnclient/issues" aria-label="Issues" class="js-selected-navigation-item sunken-menu-item js-disable-pjax" data-hotkey="g i" data-selected-links="repo_issues /mpastyl/novnclient/issues"> + <span class="octicon octicon-issue-opened"></span> <span class="full-word">Issues</span> + <span class='counter'>0</span> + <img alt="" class="mini-loader" height="16" src="https://assets-cdn.github.com/images/spinners/octocat-spinner-32.gif" width="16" /> +</a> </li> + + <li class="tooltipped tooltipped-w" aria-label="Pull Requests"> + <a href="/mpastyl/novnclient/pulls" aria-label="Pull Requests" class="js-selected-navigation-item sunken-menu-item js-disable-pjax" data-hotkey="g p" data-selected-links="repo_pulls /mpastyl/novnclient/pulls"> + <span class="octicon octicon-git-pull-request"></span> <span class="full-word">Pull Requests</span> + <span class='counter'>0</span> + <img alt="" class="mini-loader" height="16" src="https://assets-cdn.github.com/images/spinners/octocat-spinner-32.gif" width="16" /> +</a> </li> + + + </ul> + <div class="sunken-menu-separator"></div> + <ul class="sunken-menu-group"> + + <li class="tooltipped tooltipped-w" aria-label="Pulse"> + <a href="/mpastyl/novnclient/pulse" aria-label="Pulse" class="js-selected-navigation-item sunken-menu-item" data-pjax="true" data-selected-links="pulse /mpastyl/novnclient/pulse"> + <span class="octicon octicon-pulse"></span> <span class="full-word">Pulse</span> + <img alt="" class="mini-loader" height="16" src="https://assets-cdn.github.com/images/spinners/octocat-spinner-32.gif" width="16" /> +</a> </li> + + <li class="tooltipped tooltipped-w" aria-label="Graphs"> + <a href="/mpastyl/novnclient/graphs" aria-label="Graphs" class="js-selected-navigation-item sunken-menu-item" data-pjax="true" data-selected-links="repo_graphs repo_contributors /mpastyl/novnclient/graphs"> + <span class="octicon octicon-graph"></span> <span class="full-word">Graphs</span> + <img alt="" class="mini-loader" height="16" src="https://assets-cdn.github.com/images/spinners/octocat-spinner-32.gif" width="16" /> +</a> </li> + + <li class="tooltipped tooltipped-w" aria-label="Network"> + <a href="/mpastyl/novnclient/network" aria-label="Network" class="js-selected-navigation-item sunken-menu-item js-disable-pjax" data-selected-links="repo_network /mpastyl/novnclient/network"> + <span class="octicon octicon-repo-forked"></span> <span class="full-word">Network</span> + <img alt="" class="mini-loader" height="16" src="https://assets-cdn.github.com/images/spinners/octocat-spinner-32.gif" width="16" /> +</a> </li> + </ul> + + + </div> +</div> + + <div class="only-with-full-nav"> + + + + +<div class="clone-url open" + data-protocol-type="http" + data-url="/users/set_protocol?protocol_selector=http&protocol_type=clone"> + <h3><strong>HTTPS</strong> clone URL</h3> + <div class="clone-url-box"> + <input type="text" class="clone js-url-field" + value="https://github.com/mpastyl/novnclient.git" readonly="readonly"> + <span class="url-box-clippy"> + <button aria-label="copy to clipboard" class="js-zeroclipboard minibutton zeroclipboard-button" data-clipboard-text="https://github.com/mpastyl/novnclient.git" data-copied-hint="copied!" type="button"><span class="octicon octicon-clippy"></span></button> + </span> + </div> +</div> + + + +<div class="clone-url " + data-protocol-type="subversion" + data-url="/users/set_protocol?protocol_selector=subversion&protocol_type=clone"> + <h3><strong>Subversion</strong> checkout URL</h3> + <div class="clone-url-box"> + <input type="text" class="clone js-url-field" + value="https://github.com/mpastyl/novnclient" readonly="readonly"> + <span class="url-box-clippy"> + <button aria-label="copy to clipboard" class="js-zeroclipboard minibutton zeroclipboard-button" data-clipboard-text="https://github.com/mpastyl/novnclient" data-copied-hint="copied!" type="button"><span class="octicon octicon-clippy"></span></button> + </span> + </div> +</div> + + +<p class="clone-options">You can clone with + <a href="#" class="js-clone-selector" data-protocol="http">HTTPS</a> + or <a href="#" class="js-clone-selector" data-protocol="subversion">Subversion</a>. + <a href="https://help.github.com/articles/which-remote-url-should-i-use" class="help tooltipped tooltipped-n" aria-label="Get help on which URL is right for you."> + <span class="octicon octicon-question"></span> + </a> +</p> + + + + <a href="/mpastyl/novnclient/archive/master.zip" + class="minibutton sidebar-button" + aria-label="Download mpastyl/novnclient as a zip file" + title="Download mpastyl/novnclient as a zip file" + rel="nofollow"> + <span class="octicon octicon-cloud-download"></span> + Download ZIP + </a> + </div> + </div><!-- /.repository-sidebar --> + + <div id="js-repo-pjax-container" class="repository-content context-loader-container" data-pjax-container> + + + +<a href="/mpastyl/novnclient/blob/dc9ea8e453b9babfbae0dc3058fdd833d1b07c62/noVNC/keyboard.css" class="hidden js-permalink-shortcut" data-hotkey="y">Permalink</a> + +<!-- blob contrib key: blob_contributors:v21:c384cae487a618d42139aee378285a57 --> + +<p title="This is a placeholder element" class="js-history-link-replace hidden"></p> + +<div class="file-navigation"> + + +<div class="select-menu js-menu-container js-select-menu" > + <span class="minibutton select-menu-button js-menu-target css-truncate" data-hotkey="w" + data-master-branch="master" + data-ref="master" + title="master" + role="button" aria-label="Switch branches or tags" tabindex="0" aria-haspopup="true"> + <span class="octicon octicon-git-branch"></span> + <i>branch:</i> + <span class="js-select-button css-truncate-target">master</span> + </span> + + <div class="select-menu-modal-holder js-menu-content js-navigation-container" data-pjax aria-hidden="true"> + + <div class="select-menu-modal"> + <div class="select-menu-header"> + <span class="select-menu-title">Switch branches/tags</span> + <span class="octicon octicon-x js-menu-close"></span> + </div> <!-- /.select-menu-header --> + + <div class="select-menu-filters"> + <div class="select-menu-text-filter"> + <input type="text" aria-label="Filter branches/tags" id="context-commitish-filter-field" class="js-filterable-field js-navigation-enable" placeholder="Filter branches/tags"> + </div> + <div class="select-menu-tabs"> + <ul> + <li class="select-menu-tab"> + <a href="#" data-tab-filter="branches" class="js-select-menu-tab">Branches</a> + </li> + <li class="select-menu-tab"> + <a href="#" data-tab-filter="tags" class="js-select-menu-tab">Tags</a> + </li> + </ul> + </div><!-- /.select-menu-tabs --> + </div><!-- /.select-menu-filters --> + + <div class="select-menu-list select-menu-tab-bucket js-select-menu-tab-bucket" data-tab-filter="branches"> + + <div data-filterable-for="context-commitish-filter-field" data-filterable-type="substring"> + + + <div class="select-menu-item js-navigation-item selected"> + <span class="select-menu-item-icon octicon octicon-check"></span> + <a href="/mpastyl/novnclient/blob/master/noVNC/keyboard.css" + data-name="master" + data-skip-pjax="true" + rel="nofollow" + class="js-navigation-open select-menu-item-text js-select-button-text css-truncate-target" + title="master">master</a> + </div> <!-- /.select-menu-item --> + </div> + + <div class="select-menu-no-results">Nothing to show</div> + </div> <!-- /.select-menu-list --> + + <div class="select-menu-list select-menu-tab-bucket js-select-menu-tab-bucket" data-tab-filter="tags"> + <div data-filterable-for="context-commitish-filter-field" data-filterable-type="substring"> + + + </div> + + <div class="select-menu-no-results">Nothing to show</div> + </div> <!-- /.select-menu-list --> + + </div> <!-- /.select-menu-modal --> + </div> <!-- /.select-menu-modal-holder --> +</div> <!-- /.select-menu --> + + <div class="button-group right"> + <a href="/mpastyl/novnclient/find/master" + class="js-show-file-finder minibutton empty-icon tooltipped tooltipped-s" + data-pjax + data-hotkey="t" + aria-label="Quickly jump between files"> + <span class="octicon octicon-list-unordered"></span> + </a> + <button class="js-zeroclipboard minibutton zeroclipboard-button" + data-clipboard-text="noVNC/keyboard.css" + aria-label="Copy to clipboard" + data-copied-hint="Copied!"> + <span class="octicon octicon-clippy"></span> + </button> + </div> + + <div class="breadcrumb"> + <span class='repo-root js-repo-root'><span itemscope="" itemtype="http://data-vocabulary.org/Breadcrumb"><a href="/mpastyl/novnclient" data-branch="master" data-direction="back" data-pjax="true" itemscope="url"><span itemprop="title">novnclient</span></a></span></span><span class="separator"> / </span><span itemscope="" itemtype="http://data-vocabulary.org/Breadcrumb"><a href="/mpastyl/novnclient/tree/master/noVNC" data-branch="master" data-direction="back" data-pjax="true" itemscope="url"><span itemprop="title">noVNC</span></a></span><span class="separator"> / </span><strong class="final-path">keyboard.css</strong> + </div> +</div> + + + <div class="commit file-history-tease"> + <img alt="" class="main-avatar" height="24" src="https://2.gravatar.com/avatar/1274b26614f39b75fd216865f17419d6?d=https%3A%2F%2Fassets-cdn.github.com%2Fimages%2Fgravatars%2Fgravatar-user-420.png&r=x&s=140" width="24" /> + <span class="author"><span>mpampis</span></span> + <time datetime="2013-10-02T11:32:48+03:00" is="relative-time">October 02, 2013</time> + <div class="commit-title"> + <a href="/mpastyl/novnclient/commit/1dd442d07cc549e8934979274e76cab78ac384ae" class="message" data-pjax="true" title="just a copy of the client">just a copy of the client</a> + </div> + + <div class="participation"> + <p class="quickstat"><a href="#blob_contributors_box" rel="facebox"><strong>0</strong> contributors</a></p> + + </div> + <div id="blob_contributors_box" style="display:none"> + <h2 class="facebox-header">Users who have contributed to this file</h2> + <ul class="facebox-user-list"> + </ul> + </div> + </div> + +<div class="file-box"> + <div class="file"> + <div class="meta clearfix"> + <div class="info file-name"> + <span class="icon"><b class="octicon octicon-file-text"></b></span> + <span class="mode" title="File Mode">file</span> + <span class="meta-divider"></span> + <span>272 lines (266 sloc)</span> + <span class="meta-divider"></span> + <span>7.009 kb</span> + </div> + <div class="actions"> + <div class="button-group"> + <a class="minibutton disabled tooltipped tooltipped-w" href="#" + aria-label="You must be signed in to make or propose changes">Edit</a> + <a href="/mpastyl/novnclient/raw/master/noVNC/keyboard.css" class="minibutton " id="raw-url">Raw</a> + <a href="/mpastyl/novnclient/blame/master/noVNC/keyboard.css" class="minibutton js-update-url-with-hash">Blame</a> + <a href="/mpastyl/novnclient/commits/master/noVNC/keyboard.css" class="minibutton " rel="nofollow">History</a> + </div><!-- /.button-group --> + <a class="minibutton danger disabled empty-icon tooltipped tooltipped-w" href="#" + aria-label="You must be signed in to make or propose changes"> + Delete + </a> + </div><!-- /.actions --> + </div> + + <div class="blob-wrapper data type-css js-blob-data"> + <table class="file-code file-diff tab-size-8"> + <tr class="file-code-line"> + <td class="blob-line-nums"> + <span id="L1" rel="#L1">1</span> +<span id="L2" rel="#L2">2</span> +<span id="L3" rel="#L3">3</span> +<span id="L4" rel="#L4">4</span> +<span id="L5" rel="#L5">5</span> +<span id="L6" rel="#L6">6</span> +<span id="L7" rel="#L7">7</span> +<span id="L8" rel="#L8">8</span> +<span id="L9" rel="#L9">9</span> +<span id="L10" rel="#L10">10</span> +<span id="L11" rel="#L11">11</span> +<span id="L12" rel="#L12">12</span> +<span id="L13" rel="#L13">13</span> +<span id="L14" rel="#L14">14</span> +<span id="L15" rel="#L15">15</span> +<span id="L16" rel="#L16">16</span> +<span id="L17" rel="#L17">17</span> +<span id="L18" rel="#L18">18</span> +<span id="L19" rel="#L19">19</span> +<span id="L20" rel="#L20">20</span> +<span id="L21" rel="#L21">21</span> +<span id="L22" rel="#L22">22</span> +<span id="L23" rel="#L23">23</span> +<span id="L24" rel="#L24">24</span> +<span id="L25" rel="#L25">25</span> +<span id="L26" rel="#L26">26</span> +<span id="L27" rel="#L27">27</span> +<span id="L28" rel="#L28">28</span> +<span id="L29" rel="#L29">29</span> +<span id="L30" rel="#L30">30</span> +<span id="L31" rel="#L31">31</span> +<span id="L32" rel="#L32">32</span> +<span id="L33" rel="#L33">33</span> +<span id="L34" rel="#L34">34</span> +<span id="L35" rel="#L35">35</span> +<span id="L36" rel="#L36">36</span> +<span id="L37" rel="#L37">37</span> +<span id="L38" rel="#L38">38</span> +<span id="L39" rel="#L39">39</span> +<span id="L40" rel="#L40">40</span> +<span id="L41" rel="#L41">41</span> +<span id="L42" rel="#L42">42</span> +<span id="L43" rel="#L43">43</span> +<span id="L44" rel="#L44">44</span> +<span id="L45" rel="#L45">45</span> +<span id="L46" rel="#L46">46</span> +<span id="L47" rel="#L47">47</span> +<span id="L48" rel="#L48">48</span> +<span id="L49" rel="#L49">49</span> +<span id="L50" rel="#L50">50</span> +<span id="L51" rel="#L51">51</span> +<span id="L52" rel="#L52">52</span> +<span id="L53" rel="#L53">53</span> +<span id="L54" rel="#L54">54</span> +<span id="L55" rel="#L55">55</span> +<span id="L56" rel="#L56">56</span> +<span id="L57" rel="#L57">57</span> +<span id="L58" rel="#L58">58</span> +<span id="L59" rel="#L59">59</span> +<span id="L60" rel="#L60">60</span> +<span id="L61" rel="#L61">61</span> +<span id="L62" rel="#L62">62</span> +<span id="L63" rel="#L63">63</span> +<span id="L64" rel="#L64">64</span> +<span id="L65" rel="#L65">65</span> +<span id="L66" rel="#L66">66</span> +<span id="L67" rel="#L67">67</span> +<span id="L68" rel="#L68">68</span> +<span id="L69" rel="#L69">69</span> +<span id="L70" rel="#L70">70</span> +<span id="L71" rel="#L71">71</span> +<span id="L72" rel="#L72">72</span> +<span id="L73" rel="#L73">73</span> +<span id="L74" rel="#L74">74</span> +<span id="L75" rel="#L75">75</span> +<span id="L76" rel="#L76">76</span> +<span id="L77" rel="#L77">77</span> +<span id="L78" rel="#L78">78</span> +<span id="L79" rel="#L79">79</span> +<span id="L80" rel="#L80">80</span> +<span id="L81" rel="#L81">81</span> +<span id="L82" rel="#L82">82</span> +<span id="L83" rel="#L83">83</span> +<span id="L84" rel="#L84">84</span> +<span id="L85" rel="#L85">85</span> +<span id="L86" rel="#L86">86</span> +<span id="L87" rel="#L87">87</span> +<span id="L88" rel="#L88">88</span> +<span id="L89" rel="#L89">89</span> +<span id="L90" rel="#L90">90</span> +<span id="L91" rel="#L91">91</span> +<span id="L92" rel="#L92">92</span> +<span id="L93" rel="#L93">93</span> +<span id="L94" rel="#L94">94</span> +<span id="L95" rel="#L95">95</span> +<span id="L96" rel="#L96">96</span> +<span id="L97" rel="#L97">97</span> +<span id="L98" rel="#L98">98</span> +<span id="L99" rel="#L99">99</span> +<span id="L100" rel="#L100">100</span> +<span id="L101" rel="#L101">101</span> +<span id="L102" rel="#L102">102</span> +<span id="L103" rel="#L103">103</span> +<span id="L104" rel="#L104">104</span> +<span id="L105" rel="#L105">105</span> +<span id="L106" rel="#L106">106</span> +<span id="L107" rel="#L107">107</span> +<span id="L108" rel="#L108">108</span> +<span id="L109" rel="#L109">109</span> +<span id="L110" rel="#L110">110</span> +<span id="L111" rel="#L111">111</span> +<span id="L112" rel="#L112">112</span> +<span id="L113" rel="#L113">113</span> +<span id="L114" rel="#L114">114</span> +<span id="L115" rel="#L115">115</span> +<span id="L116" rel="#L116">116</span> +<span id="L117" rel="#L117">117</span> +<span id="L118" rel="#L118">118</span> +<span id="L119" rel="#L119">119</span> +<span id="L120" rel="#L120">120</span> +<span id="L121" rel="#L121">121</span> +<span id="L122" rel="#L122">122</span> +<span id="L123" rel="#L123">123</span> +<span id="L124" rel="#L124">124</span> +<span id="L125" rel="#L125">125</span> +<span id="L126" rel="#L126">126</span> +<span id="L127" rel="#L127">127</span> +<span id="L128" rel="#L128">128</span> +<span id="L129" rel="#L129">129</span> +<span id="L130" rel="#L130">130</span> +<span id="L131" rel="#L131">131</span> +<span id="L132" rel="#L132">132</span> +<span id="L133" rel="#L133">133</span> +<span id="L134" rel="#L134">134</span> +<span id="L135" rel="#L135">135</span> +<span id="L136" rel="#L136">136</span> +<span id="L137" rel="#L137">137</span> +<span id="L138" rel="#L138">138</span> +<span id="L139" rel="#L139">139</span> +<span id="L140" rel="#L140">140</span> +<span id="L141" rel="#L141">141</span> +<span id="L142" rel="#L142">142</span> +<span id="L143" rel="#L143">143</span> +<span id="L144" rel="#L144">144</span> +<span id="L145" rel="#L145">145</span> +<span id="L146" rel="#L146">146</span> +<span id="L147" rel="#L147">147</span> +<span id="L148" rel="#L148">148</span> +<span id="L149" rel="#L149">149</span> +<span id="L150" rel="#L150">150</span> +<span id="L151" rel="#L151">151</span> +<span id="L152" rel="#L152">152</span> +<span id="L153" rel="#L153">153</span> +<span id="L154" rel="#L154">154</span> +<span id="L155" rel="#L155">155</span> +<span id="L156" rel="#L156">156</span> +<span id="L157" rel="#L157">157</span> +<span id="L158" rel="#L158">158</span> +<span id="L159" rel="#L159">159</span> +<span id="L160" rel="#L160">160</span> +<span id="L161" rel="#L161">161</span> +<span id="L162" rel="#L162">162</span> +<span id="L163" rel="#L163">163</span> +<span id="L164" rel="#L164">164</span> +<span id="L165" rel="#L165">165</span> +<span id="L166" rel="#L166">166</span> +<span id="L167" rel="#L167">167</span> +<span id="L168" rel="#L168">168</span> +<span id="L169" rel="#L169">169</span> +<span id="L170" rel="#L170">170</span> +<span id="L171" rel="#L171">171</span> +<span id="L172" rel="#L172">172</span> +<span id="L173" rel="#L173">173</span> +<span id="L174" rel="#L174">174</span> +<span id="L175" rel="#L175">175</span> +<span id="L176" rel="#L176">176</span> +<span id="L177" rel="#L177">177</span> +<span id="L178" rel="#L178">178</span> +<span id="L179" rel="#L179">179</span> +<span id="L180" rel="#L180">180</span> +<span id="L181" rel="#L181">181</span> +<span id="L182" rel="#L182">182</span> +<span id="L183" rel="#L183">183</span> +<span id="L184" rel="#L184">184</span> +<span id="L185" rel="#L185">185</span> +<span id="L186" rel="#L186">186</span> +<span id="L187" rel="#L187">187</span> +<span id="L188" rel="#L188">188</span> +<span id="L189" rel="#L189">189</span> +<span id="L190" rel="#L190">190</span> +<span id="L191" rel="#L191">191</span> +<span id="L192" rel="#L192">192</span> +<span id="L193" rel="#L193">193</span> +<span id="L194" rel="#L194">194</span> +<span id="L195" rel="#L195">195</span> +<span id="L196" rel="#L196">196</span> +<span id="L197" rel="#L197">197</span> +<span id="L198" rel="#L198">198</span> +<span id="L199" rel="#L199">199</span> +<span id="L200" rel="#L200">200</span> +<span id="L201" rel="#L201">201</span> +<span id="L202" rel="#L202">202</span> +<span id="L203" rel="#L203">203</span> +<span id="L204" rel="#L204">204</span> +<span id="L205" rel="#L205">205</span> +<span id="L206" rel="#L206">206</span> +<span id="L207" rel="#L207">207</span> +<span id="L208" rel="#L208">208</span> +<span id="L209" rel="#L209">209</span> +<span id="L210" rel="#L210">210</span> +<span id="L211" rel="#L211">211</span> +<span id="L212" rel="#L212">212</span> +<span id="L213" rel="#L213">213</span> +<span id="L214" rel="#L214">214</span> +<span id="L215" rel="#L215">215</span> +<span id="L216" rel="#L216">216</span> +<span id="L217" rel="#L217">217</span> +<span id="L218" rel="#L218">218</span> +<span id="L219" rel="#L219">219</span> +<span id="L220" rel="#L220">220</span> +<span id="L221" rel="#L221">221</span> +<span id="L222" rel="#L222">222</span> +<span id="L223" rel="#L223">223</span> +<span id="L224" rel="#L224">224</span> +<span id="L225" rel="#L225">225</span> +<span id="L226" rel="#L226">226</span> +<span id="L227" rel="#L227">227</span> +<span id="L228" rel="#L228">228</span> +<span id="L229" rel="#L229">229</span> +<span id="L230" rel="#L230">230</span> +<span id="L231" rel="#L231">231</span> +<span id="L232" rel="#L232">232</span> +<span id="L233" rel="#L233">233</span> +<span id="L234" rel="#L234">234</span> +<span id="L235" rel="#L235">235</span> +<span id="L236" rel="#L236">236</span> +<span id="L237" rel="#L237">237</span> +<span id="L238" rel="#L238">238</span> +<span id="L239" rel="#L239">239</span> +<span id="L240" rel="#L240">240</span> +<span id="L241" rel="#L241">241</span> +<span id="L242" rel="#L242">242</span> +<span id="L243" rel="#L243">243</span> +<span id="L244" rel="#L244">244</span> +<span id="L245" rel="#L245">245</span> +<span id="L246" rel="#L246">246</span> +<span id="L247" rel="#L247">247</span> +<span id="L248" rel="#L248">248</span> +<span id="L249" rel="#L249">249</span> +<span id="L250" rel="#L250">250</span> +<span id="L251" rel="#L251">251</span> +<span id="L252" rel="#L252">252</span> +<span id="L253" rel="#L253">253</span> +<span id="L254" rel="#L254">254</span> +<span id="L255" rel="#L255">255</span> +<span id="L256" rel="#L256">256</span> +<span id="L257" rel="#L257">257</span> +<span id="L258" rel="#L258">258</span> +<span id="L259" rel="#L259">259</span> +<span id="L260" rel="#L260">260</span> +<span id="L261" rel="#L261">261</span> +<span id="L262" rel="#L262">262</span> +<span id="L263" rel="#L263">263</span> +<span id="L264" rel="#L264">264</span> +<span id="L265" rel="#L265">265</span> +<span id="L266" rel="#L266">266</span> +<span id="L267" rel="#L267">267</span> +<span id="L268" rel="#L268">268</span> +<span id="L269" rel="#L269">269</span> +<span id="L270" rel="#L270">270</span> +<span id="L271" rel="#L271">271</span> + + </td> + <td class="blob-line-code"><div class="code-body highlight"><pre><div class='line' id='LC1'><span class="nf">#keyboardInputMaster</span> <span class="p">{</span></div><div class='line' id='LC2'> <span class="k">position</span><span class="o">:</span><span class="k">absolute</span><span class="p">;</span></div><div class='line' id='LC3'> <span class="k">font</span><span class="o">:</span><span class="k">normal</span> <span class="m">11px</span> <span class="n">Arial</span><span class="o">,</span><span class="k">sans-serif</span><span class="p">;</span></div><div class='line' id='LC4'> <span class="k">border-top</span><span class="o">:</span><span class="m">1px</span> <span class="k">solid</span> <span class="m">#eeeeee</span><span class="p">;</span></div><div class='line' id='LC5'> <span class="k">border-right</span><span class="o">:</span><span class="m">1px</span> <span class="k">solid</span> <span class="m">#888888</span><span class="p">;</span></div><div class='line' id='LC6'> <span class="k">border-bottom</span><span class="o">:</span><span class="m">1px</span> <span class="k">solid</span> <span class="m">#444444</span><span class="p">;</span></div><div class='line' id='LC7'> <span class="k">border-left</span><span class="o">:</span><span class="m">1px</span> <span class="k">solid</span> <span class="m">#cccccc</span><span class="p">;</span></div><div class='line' id='LC8'> <span class="o">-</span><span class="n">webkit</span><span class="o">-</span><span class="k">border</span><span class="o">-</span><span class="n">radius</span><span class="o">:</span><span class="m">0.6em</span><span class="p">;</span></div><div class='line' id='LC9'> <span class="o">-</span><span class="n">moz</span><span class="o">-</span><span class="k">border</span><span class="o">-</span><span class="n">radius</span><span class="o">:</span><span class="m">0.6em</span><span class="p">;</span></div><div class='line' id='LC10'> <span class="k">border</span><span class="o">-</span><span class="n">radius</span><span class="o">:</span><span class="m">0.6em</span><span class="p">;</span></div><div class='line' id='LC11'> <span class="o">-</span><span class="n">webkit</span><span class="o">-</span><span class="n">box</span><span class="o">-</span><span class="n">shadow</span><span class="o">:</span><span class="m">0px</span> <span class="m">2px</span> <span class="m">10px</span> <span class="m">#444444</span><span class="p">;</span></div><div class='line' id='LC12'> <span class="o">-</span><span class="n">moz</span><span class="o">-</span><span class="n">box</span><span class="o">-</span><span class="n">shadow</span><span class="o">:</span><span class="m">0px</span> <span class="m">2px</span> <span class="m">10px</span> <span class="m">#444444</span><span class="p">;</span></div><div class='line' id='LC13'> <span class="n">box</span><span class="o">-</span><span class="n">shadow</span><span class="o">:</span><span class="m">0px</span> <span class="m">2px</span> <span class="m">10px</span> <span class="m">#444444</span><span class="p">;</span></div><div class='line' id='LC14'> <span class="k">opacity</span><span class="o">:</span><span class="m">0</span><span class="o">.</span><span class="m">95</span><span class="p">;</span></div><div class='line' id='LC15'> <span class="n">filter</span><span class="o">:</span><span class="n">alpha</span><span class="p">(</span><span class="k">opacity</span><span class="o">=</span><span class="m">95</span><span class="p">);</span></div><div class='line' id='LC16'> <span class="k">background-color</span><span class="o">:</span><span class="m">#dddddd</span><span class="p">;</span></div><div class='line' id='LC17'> <span class="k">text-align</span><span class="o">:</span><span class="k">left</span><span class="p">;</span></div><div class='line' id='LC18'> <span class="k">z-index</span><span class="o">:</span><span class="m">1000000</span><span class="p">;</span></div><div class='line' id='LC19'> <span class="k">width</span><span class="o">:</span><span class="k">auto</span><span class="p">;</span></div><div class='line' id='LC20'> <span class="k">height</span><span class="o">:</span><span class="k">auto</span><span class="p">;</span></div><div class='line' id='LC21'> <span class="k">min-width</span><span class="o">:</span><span class="m">0</span><span class="p">;</span></div><div class='line' id='LC22'> <span class="k">min-height</span><span class="o">:</span><span class="m">0</span><span class="p">;</span></div><div class='line' id='LC23'> <span class="k">margin</span><span class="o">:</span><span class="m">0px</span><span class="p">;</span></div><div class='line' id='LC24'> <span class="k">padding</span><span class="o">:</span><span class="m">0px</span><span class="p">;</span></div><div class='line' id='LC25'> <span class="k">line-height</span><span class="o">:</span><span class="k">normal</span><span class="p">;</span></div><div class='line' id='LC26'> <span class="o">-</span><span class="n">moz</span><span class="o">-</span><span class="n">user</span><span class="o">-</span><span class="n">select</span><span class="o">:</span><span class="k">none</span><span class="p">;</span></div><div class='line' id='LC27'> <span class="k">cursor</span><span class="o">:</span><span class="k">default</span><span class="p">;</span></div><div class='line' id='LC28'><span class="p">}</span></div><div class='line' id='LC29'><span class="nf">#keyboardInputMaster</span> <span class="o">*</span> <span class="p">{</span></div><div class='line' id='LC30'> <span class="k">position</span><span class="o">:</span><span class="k">static</span><span class="p">;</span></div><div class='line' id='LC31'> <span class="k">color</span><span class="o">:</span><span class="m">#000000</span><span class="p">;</span></div><div class='line' id='LC32'> <span class="k">background</span><span class="o">:</span><span class="k">transparent</span><span class="p">;</span></div><div class='line' id='LC33'> <span class="k">font</span><span class="o">:</span><span class="k">normal</span> <span class="m">11px</span> <span class="n">Arial</span><span class="o">,</span><span class="k">sans-serif</span><span class="p">;</span></div><div class='line' id='LC34'> <span class="k">width</span><span class="o">:</span><span class="k">auto</span><span class="p">;</span></div><div class='line' id='LC35'> <span class="k">height</span><span class="o">:</span><span class="k">auto</span><span class="p">;</span></div><div class='line' id='LC36'> <span class="k">min-width</span><span class="o">:</span><span class="m">0</span><span class="p">;</span></div><div class='line' id='LC37'> <span class="k">min-height</span><span class="o">:</span><span class="m">0</span><span class="p">;</span></div><div class='line' id='LC38'> <span class="k">margin</span><span class="o">:</span><span class="m">0px</span><span class="p">;</span></div><div class='line' id='LC39'> <span class="k">padding</span><span class="o">:</span><span class="m">0px</span><span class="p">;</span></div><div class='line' id='LC40'> <span class="k">border</span><span class="o">:</span><span class="m">0px</span> <span class="k">none</span><span class="p">;</span></div><div class='line' id='LC41'> <span class="k">outline</span><span class="o">:</span><span class="m">0px</span><span class="p">;</span></div><div class='line' id='LC42'> <span class="k">vertical-align</span><span class="o">:</span><span class="k">baseline</span><span class="p">;</span></div><div class='line' id='LC43'> <span class="k">line-height</span><span class="o">:</span><span class="m">1.3em</span><span class="p">;</span></div><div class='line' id='LC44'><span class="p">}</span></div><div class='line' id='LC45'><span class="nf">#keyboardInputMaster</span> <span class="nt">table</span> <span class="p">{</span></div><div class='line' id='LC46'> <span class="k">table-layout</span><span class="o">:</span><span class="k">auto</span><span class="p">;</span></div><div class='line' id='LC47'><span class="p">}</span></div><div class='line' id='LC48'><span class="nf">#keyboardInputMaster</span><span class="nc">.keyboardInputSize1</span><span class="o">,</span></div><div class='line' id='LC49'><span class="nf">#keyboardInputMaster</span><span class="nc">.keyboardInputSize1</span> <span class="o">*</span> <span class="p">{</span></div><div class='line' id='LC50'> <span class="k">font-size</span><span class="o">:</span><span class="m">9px</span><span class="p">;</span></div><div class='line' id='LC51'><span class="p">}</span></div><div class='line' id='LC52'><span class="nf">#keyboardInputMaster</span><span class="nc">.keyboardInputSize3</span><span class="o">,</span></div><div class='line' id='LC53'><span class="nf">#keyboardInputMaster</span><span class="nc">.keyboardInputSize3</span> <span class="o">*</span> <span class="p">{</span></div><div class='line' id='LC54'> <span class="k">font-size</span><span class="o">:</span><span class="m">13px</span><span class="p">;</span></div><div class='line' id='LC55'><span class="p">}</span></div><div class='line' id='LC56'><span class="nf">#keyboardInputMaster</span><span class="nc">.keyboardInputSize4</span><span class="o">,</span></div><div class='line' id='LC57'><span class="nf">#keyboardInputMaster</span><span class="nc">.keyboardInputSize4</span> <span class="o">*</span> <span class="p">{</span></div><div class='line' id='LC58'> <span class="k">font-size</span><span class="o">:</span><span class="m">16px</span><span class="p">;</span></div><div class='line' id='LC59'><span class="p">}</span></div><div class='line' id='LC60'><span class="nf">#keyboardInputMaster</span><span class="nc">.keyboardInputSize5</span><span class="o">,</span></div><div class='line' id='LC61'><span class="nf">#keyboardInputMaster</span><span class="nc">.keyboardInputSize5</span> <span class="o">*</span> <span class="p">{</span></div><div class='line' id='LC62'> <span class="k">font-size</span><span class="o">:</span><span class="m">20px</span><span class="p">;</span></div><div class='line' id='LC63'><span class="p">}</span></div><div class='line' id='LC64'><br/></div><div class='line' id='LC65'><span class="nf">#keyboardInputMaster</span> <span class="nt">thead</span> <span class="nt">tr</span> <span class="nt">th</span> <span class="p">{</span></div><div class='line' id='LC66'> <span class="k">padding</span><span class="o">:</span><span class="m">0.3em</span> <span class="m">0.3em</span> <span class="m">0.1em</span> <span class="m">0.3em</span><span class="p">;</span></div><div class='line' id='LC67'> <span class="k">background-color</span><span class="o">:</span><span class="m">#999999</span><span class="p">;</span></div><div class='line' id='LC68'> <span class="k">white-space</span><span class="o">:</span><span class="k">nowrap</span><span class="p">;</span></div><div class='line' id='LC69'> <span class="k">text-align</span><span class="o">:</span><span class="k">right</span><span class="p">;</span></div><div class='line' id='LC70'> <span class="o">-</span><span class="n">webkit</span><span class="o">-</span><span class="k">border</span><span class="o">-</span><span class="n">radius</span><span class="o">:</span><span class="m">0.6em</span> <span class="m">0.6em</span> <span class="m">0px</span> <span class="m">0px</span><span class="p">;</span></div><div class='line' id='LC71'> <span class="o">-</span><span class="n">moz</span><span class="o">-</span><span class="k">border</span><span class="o">-</span><span class="n">radius</span><span class="o">:</span><span class="m">0.6em</span> <span class="m">0.6em</span> <span class="m">0px</span> <span class="m">0px</span><span class="p">;</span></div><div class='line' id='LC72'> <span class="k">border</span><span class="o">-</span><span class="n">radius</span><span class="o">:</span><span class="m">0.6em</span> <span class="m">0.6em</span> <span class="m">0px</span> <span class="m">0px</span><span class="p">;</span></div><div class='line' id='LC73'><span class="p">}</span></div><div class='line' id='LC74'><span class="nf">#keyboardInputMaster</span> <span class="nt">thead</span> <span class="nt">tr</span> <span class="nt">th</span> <span class="nt">div</span> <span class="p">{</span></div><div class='line' id='LC75'> <span class="k">float</span><span class="o">:</span><span class="k">left</span><span class="p">;</span></div><div class='line' id='LC76'> <span class="k">font-size</span><span class="o">:</span><span class="m">130%</span> <span class="cp">!important</span><span class="p">;</span></div><div class='line' id='LC77'> <span class="k">height</span><span class="o">:</span><span class="m">1.3em</span><span class="p">;</span></div><div class='line' id='LC78'> <span class="k">font-weight</span><span class="o">:</span><span class="k">bold</span><span class="p">;</span></div><div class='line' id='LC79'> <span class="k">position</span><span class="o">:</span><span class="k">relative</span><span class="p">;</span></div><div class='line' id='LC80'> <span class="k">z-index</span><span class="o">:</span><span class="m">1</span><span class="p">;</span></div><div class='line' id='LC81'> <span class="k">margin-right</span><span class="o">:</span><span class="m">0.5em</span><span class="p">;</span></div><div class='line' id='LC82'> <span class="k">cursor</span><span class="o">:</span><span class="k">pointer</span><span class="p">;</span></div><div class='line' id='LC83'> <span class="k">background-color</span><span class="o">:</span><span class="k">transparent</span><span class="p">;</span></div><div class='line' id='LC84'><span class="p">}</span></div><div class='line' id='LC85'><span class="nf">#keyboardInputMaster</span> <span class="nt">thead</span> <span class="nt">tr</span> <span class="nt">th</span> <span class="nt">div</span> <span class="nt">ol</span> <span class="p">{</span></div><div class='line' id='LC86'> <span class="k">position</span><span class="o">:</span><span class="k">absolute</span><span class="p">;</span></div><div class='line' id='LC87'> <span class="k">left</span><span class="o">:</span><span class="m">0px</span><span class="p">;</span></div><div class='line' id='LC88'> <span class="k">top</span><span class="o">:</span><span class="m">90%</span><span class="p">;</span></div><div class='line' id='LC89'> <span class="k">list-style-type</span><span class="o">:</span><span class="k">none</span><span class="p">;</span></div><div class='line' id='LC90'> <span class="k">height</span><span class="o">:</span><span class="m">9.4em</span><span class="p">;</span></div><div class='line' id='LC91'> <span class="k">overflow-y</span><span class="o">:</span><span class="k">auto</span><span class="p">;</span></div><div class='line' id='LC92'> <span class="k">overflow-x</span><span class="o">:</span><span class="k">hidden</span><span class="p">;</span></div><div class='line' id='LC93'> <span class="k">background-color</span><span class="o">:</span><span class="m">#f6f6f6</span><span class="p">;</span></div><div class='line' id='LC94'> <span class="k">border</span><span class="o">:</span><span class="m">1px</span> <span class="k">solid</span> <span class="m">#999999</span><span class="p">;</span></div><div class='line' id='LC95'> <span class="k">display</span><span class="o">:</span><span class="k">none</span><span class="p">;</span></div><div class='line' id='LC96'> <span class="k">text-align</span><span class="o">:</span><span class="k">left</span><span class="p">;</span></div><div class='line' id='LC97'> <span class="k">width</span><span class="o">:</span><span class="m">12em</span><span class="p">;</span></div><div class='line' id='LC98'><span class="p">}</span></div><div class='line' id='LC99'><span class="nf">#keyboardInputMaster</span> <span class="nt">thead</span> <span class="nt">tr</span> <span class="nt">th</span> <span class="nt">div</span> <span class="nt">ol</span> <span class="nt">li</span> <span class="p">{</span></div><div class='line' id='LC100'> <span class="k">padding</span><span class="o">:</span><span class="m">0.2em</span> <span class="m">0.4em</span><span class="p">;</span></div><div class='line' id='LC101'> <span class="k">cursor</span><span class="o">:</span><span class="k">pointer</span><span class="p">;</span></div><div class='line' id='LC102'> <span class="k">white-space</span><span class="o">:</span><span class="k">nowrap</span><span class="p">;</span></div><div class='line' id='LC103'> <span class="k">width</span><span class="o">:</span><span class="m">12em</span><span class="p">;</span></div><div class='line' id='LC104'><span class="p">}</span></div><div class='line' id='LC105'><span class="nf">#keyboardInputMaster</span> <span class="nt">thead</span> <span class="nt">tr</span> <span class="nt">th</span> <span class="nt">div</span> <span class="nt">ol</span> <span class="nt">li</span><span class="nc">.selected</span> <span class="p">{</span></div><div class='line' id='LC106'> <span class="k">background-color</span><span class="o">:</span><span class="m">#ffffcc</span><span class="p">;</span></div><div class='line' id='LC107'><span class="p">}</span></div><div class='line' id='LC108'><span class="nf">#keyboardInputMaster</span> <span class="nt">thead</span> <span class="nt">tr</span> <span class="nt">th</span> <span class="nt">div</span> <span class="nt">ol</span> <span class="nt">li</span><span class="nd">:hover</span><span class="o">,</span></div><div class='line' id='LC109'><span class="nf">#keyboardInputMaster</span> <span class="nt">thead</span> <span class="nt">tr</span> <span class="nt">th</span> <span class="nt">div</span> <span class="nt">ol</span> <span class="nt">li</span><span class="nc">.hover</span> <span class="p">{</span></div><div class='line' id='LC110'> <span class="k">background-color</span><span class="o">:</span><span class="m">#dddddd</span><span class="p">;</span></div><div class='line' id='LC111'><span class="p">}</span></div><div class='line' id='LC112'><span class="nf">#keyboardInputMaster</span> <span class="nt">thead</span> <span class="nt">tr</span> <span class="nt">th</span> <span class="nt">span</span><span class="o">,</span></div><div class='line' id='LC113'><span class="nf">#keyboardInputMaster</span> <span class="nt">thead</span> <span class="nt">tr</span> <span class="nt">th</span> <span class="nt">strong</span><span class="o">,</span></div><div class='line' id='LC114'><span class="nf">#keyboardInputMaster</span> <span class="nt">thead</span> <span class="nt">tr</span> <span class="nt">th</span> <span class="nt">small</span><span class="o">,</span></div><div class='line' id='LC115'><span class="nf">#keyboardInputMaster</span> <span class="nt">thead</span> <span class="nt">tr</span> <span class="nt">th</span> <span class="nt">big</span> <span class="p">{</span></div><div class='line' id='LC116'> <span class="k">display</span><span class="o">:</span><span class="k">inline</span><span class="o">-</span><span class="k">block</span><span class="p">;</span></div><div class='line' id='LC117'> <span class="k">padding</span><span class="o">:</span><span class="m">0px</span> <span class="m">0.4em</span><span class="p">;</span></div><div class='line' id='LC118'> <span class="k">height</span><span class="o">:</span><span class="m">1.4em</span><span class="p">;</span></div><div class='line' id='LC119'> <span class="k">line-height</span><span class="o">:</span><span class="m">1.4em</span><span class="p">;</span></div><div class='line' id='LC120'> <span class="k">border-top</span><span class="o">:</span><span class="m">1px</span> <span class="k">solid</span> <span class="m">#e5e5e5</span><span class="p">;</span></div><div class='line' id='LC121'> <span class="k">border-right</span><span class="o">:</span><span class="m">1px</span> <span class="k">solid</span> <span class="m">#5d5d5d</span><span class="p">;</span></div><div class='line' id='LC122'> <span class="k">border-bottom</span><span class="o">:</span><span class="m">1px</span> <span class="k">solid</span> <span class="m">#5d5d5d</span><span class="p">;</span></div><div class='line' id='LC123'> <span class="k">border-left</span><span class="o">:</span><span class="m">1px</span> <span class="k">solid</span> <span class="m">#e5e5e5</span><span class="p">;</span></div><div class='line' id='LC124'> <span class="k">background-color</span><span class="o">:</span><span class="m">#cccccc</span><span class="p">;</span></div><div class='line' id='LC125'> <span class="k">cursor</span><span class="o">:</span><span class="k">pointer</span><span class="p">;</span></div><div class='line' id='LC126'> <span class="k">margin</span><span class="o">:</span><span class="m">0px</span> <span class="m">0px</span> <span class="m">0px</span> <span class="m">0.3em</span><span class="p">;</span></div><div class='line' id='LC127'> <span class="o">-</span><span class="n">webkit</span><span class="o">-</span><span class="k">border</span><span class="o">-</span><span class="n">radius</span><span class="o">:</span><span class="m">0.3em</span><span class="p">;</span></div><div class='line' id='LC128'> <span class="o">-</span><span class="n">moz</span><span class="o">-</span><span class="k">border</span><span class="o">-</span><span class="n">radius</span><span class="o">:</span><span class="m">0.3em</span><span class="p">;</span></div><div class='line' id='LC129'> <span class="k">border</span><span class="o">-</span><span class="n">radius</span><span class="o">:</span><span class="m">0.3em</span><span class="p">;</span></div><div class='line' id='LC130'> <span class="k">vertical-align</span><span class="o">:</span><span class="k">middle</span><span class="p">;</span></div><div class='line' id='LC131'> <span class="o">-</span><span class="n">webkit</span><span class="o">-</span><span class="n">transition</span><span class="o">:</span><span class="k">background-color</span> <span class="m">.15s</span> <span class="n">ease</span><span class="o">-</span><span class="n">in</span><span class="o">-</span><span class="n">out</span><span class="p">;</span></div><div class='line' id='LC132'> <span class="o">-</span><span class="n">o</span><span class="o">-</span><span class="n">transition</span><span class="o">:</span><span class="k">background-color</span> <span class="m">.15s</span> <span class="n">ease</span><span class="o">-</span><span class="n">in</span><span class="o">-</span><span class="n">out</span><span class="p">;</span></div><div class='line' id='LC133'> <span class="n">transition</span><span class="o">:</span><span class="k">background-color</span> <span class="m">.15s</span> <span class="n">ease</span><span class="o">-</span><span class="n">in</span><span class="o">-</span><span class="n">out</span><span class="p">;</span></div><div class='line' id='LC134'><span class="p">}</span></div><div class='line' id='LC135'><span class="nf">#keyboardInputMaster</span> <span class="nt">thead</span> <span class="nt">tr</span> <span class="nt">th</span> <span class="nt">strong</span> <span class="p">{</span></div><div class='line' id='LC136'> <span class="k">font-weight</span><span class="o">:</span><span class="k">bold</span><span class="p">;</span></div><div class='line' id='LC137'><span class="p">}</span></div><div class='line' id='LC138'><span class="nf">#keyboardInputMaster</span> <span class="nt">thead</span> <span class="nt">tr</span> <span class="nt">th</span> <span class="nt">small</span> <span class="p">{</span></div><div class='line' id='LC139'> <span class="o">-</span><span class="n">webkit</span><span class="o">-</span><span class="k">border</span><span class="o">-</span><span class="n">radius</span><span class="o">:</span><span class="m">0.3em</span> <span class="m">0px</span> <span class="m">0px</span> <span class="m">0.3em</span><span class="p">;</span></div><div class='line' id='LC140'> <span class="o">-</span><span class="n">moz</span><span class="o">-</span><span class="k">border</span><span class="o">-</span><span class="n">radius</span><span class="o">:</span><span class="m">0.3em</span> <span class="m">0px</span> <span class="m">0px</span> <span class="m">0.3em</span><span class="p">;</span></div><div class='line' id='LC141'> <span class="k">border</span><span class="o">-</span><span class="n">radius</span><span class="o">:</span><span class="m">0.3em</span> <span class="m">0px</span> <span class="m">0px</span> <span class="m">0.3em</span><span class="p">;</span></div><div class='line' id='LC142'> <span class="k">border-right</span><span class="o">:</span><span class="m">1px</span> <span class="k">solid</span> <span class="m">#aaaaaa</span><span class="p">;</span></div><div class='line' id='LC143'> <span class="k">padding</span><span class="o">:</span><span class="m">0px</span> <span class="m">0.2em</span> <span class="m">0px</span> <span class="m">0.3em</span><span class="p">;</span></div><div class='line' id='LC144'><span class="p">}</span></div><div class='line' id='LC145'><span class="nf">#keyboardInputMaster</span> <span class="nt">thead</span> <span class="nt">tr</span> <span class="nt">th</span> <span class="nt">big</span> <span class="p">{</span></div><div class='line' id='LC146'> <span class="o">-</span><span class="n">webkit</span><span class="o">-</span><span class="k">border</span><span class="o">-</span><span class="n">radius</span><span class="o">:</span><span class="m">0px</span> <span class="m">0.3em</span> <span class="m">0.3em</span> <span class="m">0px</span><span class="p">;</span></div><div class='line' id='LC147'> <span class="o">-</span><span class="n">moz</span><span class="o">-</span><span class="k">border</span><span class="o">-</span><span class="n">radius</span><span class="o">:</span><span class="m">0px</span> <span class="m">0.3em</span> <span class="m">0.3em</span> <span class="m">0px</span><span class="p">;</span></div><div class='line' id='LC148'> <span class="k">border</span><span class="o">-</span><span class="n">radius</span><span class="o">:</span><span class="m">0px</span> <span class="m">0.3em</span> <span class="m">0.3em</span> <span class="m">0px</span><span class="p">;</span></div><div class='line' id='LC149'> <span class="k">border-left</span><span class="o">:</span><span class="m">0px</span> <span class="k">none</span><span class="p">;</span></div><div class='line' id='LC150'> <span class="k">margin</span><span class="o">:</span><span class="m">0px</span><span class="p">;</span></div><div class='line' id='LC151'> <span class="k">padding</span><span class="o">:</span><span class="m">0px</span> <span class="m">0.3em</span> <span class="m">0px</span> <span class="m">0.2em</span><span class="p">;</span></div><div class='line' id='LC152'><span class="p">}</span></div><div class='line' id='LC153'><span class="nf">#keyboardInputMaster</span> <span class="nt">thead</span> <span class="nt">tr</span> <span class="nt">th</span> <span class="nt">span</span><span class="nd">:hover</span><span class="o">,</span></div><div class='line' id='LC154'><span class="nf">#keyboardInputMaster</span> <span class="nt">thead</span> <span class="nt">tr</span> <span class="nt">th</span> <span class="nt">span</span><span class="nc">.hover</span><span class="o">,</span></div><div class='line' id='LC155'><span class="nf">#keyboardInputMaster</span> <span class="nt">thead</span> <span class="nt">tr</span> <span class="nt">th</span> <span class="nt">strong</span><span class="nd">:hover</span><span class="o">,</span></div><div class='line' id='LC156'><span class="nf">#keyboardInputMaster</span> <span class="nt">thead</span> <span class="nt">tr</span> <span class="nt">th</span> <span class="nt">strong</span><span class="nc">.hover</span><span class="o">,</span></div><div class='line' id='LC157'><span class="nf">#keyboardInputMaster</span> <span class="nt">thead</span> <span class="nt">tr</span> <span class="nt">th</span> <span class="nt">small</span><span class="nd">:hover</span><span class="o">,</span></div><div class='line' id='LC158'><span class="nf">#keyboardInputMaster</span> <span class="nt">thead</span> <span class="nt">tr</span> <span class="nt">th</span> <span class="nt">small</span><span class="nc">.hover</span><span class="o">,</span></div><div class='line' id='LC159'><span class="nf">#keyboardInputMaster</span> <span class="nt">thead</span> <span class="nt">tr</span> <span class="nt">th</span> <span class="nt">big</span><span class="nd">:hover</span><span class="o">,</span></div><div class='line' id='LC160'><span class="nf">#keyboardInputMaster</span> <span class="nt">thead</span> <span class="nt">tr</span> <span class="nt">th</span> <span class="nt">big</span><span class="nc">.hover</span> <span class="p">{</span></div><div class='line' id='LC161'> <span class="k">background-color</span><span class="o">:</span><span class="m">#dddddd</span><span class="p">;</span></div><div class='line' id='LC162'><span class="p">}</span></div><div class='line' id='LC163'><br/></div><div class='line' id='LC164'><span class="nf">#keyboardInputMaster</span> <span class="nt">tbody</span> <span class="nt">tr</span> <span class="nt">td</span> <span class="p">{</span></div><div class='line' id='LC165'> <span class="k">text-align</span><span class="o">:</span><span class="k">left</span><span class="p">;</span></div><div class='line' id='LC166'> <span class="k">padding</span><span class="o">:</span><span class="m">0.2em</span> <span class="m">0.3em</span> <span class="m">0.3em</span> <span class="m">0.3em</span><span class="p">;</span></div><div class='line' id='LC167'> <span class="k">vertical-align</span><span class="o">:</span><span class="k">top</span><span class="p">;</span></div><div class='line' id='LC168'><span class="p">}</span></div><div class='line' id='LC169'><span class="nf">#keyboardInputMaster</span> <span class="nt">tbody</span> <span class="nt">tr</span> <span class="nt">td</span> <span class="nt">div</span> <span class="p">{</span></div><div class='line' id='LC170'> <span class="k">text-align</span><span class="o">:</span><span class="k">center</span><span class="p">;</span></div><div class='line' id='LC171'> <span class="k">position</span><span class="o">:</span><span class="k">relative</span><span class="p">;</span></div><div class='line' id='LC172'> <span class="n">zoom</span><span class="o">:</span><span class="m">1</span><span class="p">;</span></div><div class='line' id='LC173'><span class="p">}</span></div><div class='line' id='LC174'><span class="nf">#keyboardInputMaster</span> <span class="nt">tbody</span> <span class="nt">tr</span> <span class="nt">td</span> <span class="nt">table</span> <span class="p">{</span></div><div class='line' id='LC175'> <span class="k">white-space</span><span class="o">:</span><span class="k">nowrap</span><span class="p">;</span></div><div class='line' id='LC176'> <span class="k">width</span><span class="o">:</span><span class="m">100%</span><span class="p">;</span></div><div class='line' id='LC177'> <span class="k">border-collapse</span><span class="o">:</span><span class="k">separate</span><span class="p">;</span></div><div class='line' id='LC178'> <span class="k">border-spacing</span><span class="o">:</span><span class="m">0px</span><span class="p">;</span></div><div class='line' id='LC179'><span class="p">}</span></div><div class='line' id='LC180'><span class="nf">#keyboardInputMaster</span> <span class="nt">tbody</span> <span class="nt">tr</span> <span class="nt">td</span><span class="nf">#keyboardInputNumpad</span> <span class="nt">table</span> <span class="p">{</span></div><div class='line' id='LC181'> <span class="k">margin-left</span><span class="o">:</span><span class="m">0.2em</span><span class="p">;</span></div><div class='line' id='LC182'> <span class="k">width</span><span class="o">:</span><span class="k">auto</span><span class="p">;</span></div><div class='line' id='LC183'><span class="p">}</span></div><div class='line' id='LC184'><span class="nf">#keyboardInputMaster</span> <span class="nt">tbody</span> <span class="nt">tr</span> <span class="nt">td</span> <span class="nt">table</span><span class="nc">.keyboardInputCenter</span> <span class="p">{</span></div><div class='line' id='LC185'> <span class="k">width</span><span class="o">:</span><span class="k">auto</span><span class="p">;</span></div><div class='line' id='LC186'> <span class="k">margin</span><span class="o">:</span><span class="m">0px</span> <span class="k">auto</span><span class="p">;</span></div><div class='line' id='LC187'><span class="p">}</span></div><div class='line' id='LC188'><span class="nf">#keyboardInputMaster</span> <span class="nt">tbody</span> <span class="nt">tr</span> <span class="nt">td</span> <span class="nt">table</span> <span class="nt">tbody</span> <span class="nt">tr</span> <span class="nt">td</span> <span class="p">{</span></div><div class='line' id='LC189'> <span class="k">vertical-align</span><span class="o">:</span><span class="k">middle</span><span class="p">;</span></div><div class='line' id='LC190'> <span class="k">padding</span><span class="o">:</span><span class="m">0px</span> <span class="m">0.45em</span><span class="p">;</span></div><div class='line' id='LC191'> <span class="k">white-space</span><span class="o">:</span><span class="n">pre</span><span class="p">;</span></div><div class='line' id='LC192'> <span class="k">height</span><span class="o">:</span><span class="m">1.8em</span><span class="p">;</span></div><div class='line' id='LC193'> <span class="k">font-family</span><span class="o">:</span><span class="s1">'Lucida Console'</span><span class="o">,</span><span class="s1">'Arial Unicode MS'</span><span class="o">,</span><span class="k">monospace</span><span class="p">;</span></div><div class='line' id='LC194'> <span class="k">border-top</span><span class="o">:</span><span class="m">1px</span> <span class="k">solid</span> <span class="m">#e5e5e5</span><span class="p">;</span></div><div class='line' id='LC195'> <span class="k">border-right</span><span class="o">:</span><span class="m">1px</span> <span class="k">solid</span> <span class="m">#5d5d5d</span><span class="p">;</span></div><div class='line' id='LC196'> <span class="k">border-bottom</span><span class="o">:</span><span class="m">1px</span> <span class="k">solid</span> <span class="m">#5d5d5d</span><span class="p">;</span></div><div class='line' id='LC197'> <span class="k">border-left</span><span class="o">:</span><span class="m">1px</span> <span class="k">solid</span> <span class="m">#e5e5e5</span><span class="p">;</span></div><div class='line' id='LC198'> <span class="k">background-color</span><span class="o">:</span><span class="m">#eeeeee</span><span class="p">;</span></div><div class='line' id='LC199'> <span class="k">cursor</span><span class="o">:</span><span class="k">default</span><span class="p">;</span></div><div class='line' id='LC200'> <span class="k">min-width</span><span class="o">:</span><span class="m">0.75em</span><span class="p">;</span></div><div class='line' id='LC201'> <span class="o">-</span><span class="n">webkit</span><span class="o">-</span><span class="k">border</span><span class="o">-</span><span class="n">radius</span><span class="o">:</span><span class="m">0.2em</span><span class="p">;</span></div><div class='line' id='LC202'> <span class="o">-</span><span class="n">moz</span><span class="o">-</span><span class="k">border</span><span class="o">-</span><span class="n">radius</span><span class="o">:</span><span class="m">0.2em</span><span class="p">;</span></div><div class='line' id='LC203'> <span class="k">border</span><span class="o">-</span><span class="n">radius</span><span class="o">:</span><span class="m">0.2em</span><span class="p">;</span></div><div class='line' id='LC204'> <span class="o">-</span><span class="n">webkit</span><span class="o">-</span><span class="n">transition</span><span class="o">:</span><span class="k">background-color</span> <span class="m">.15s</span> <span class="n">ease</span><span class="o">-</span><span class="n">in</span><span class="o">-</span><span class="n">out</span><span class="p">;</span></div><div class='line' id='LC205'> <span class="o">-</span><span class="n">o</span><span class="o">-</span><span class="n">transition</span><span class="o">:</span><span class="k">background-color</span> <span class="m">.15s</span> <span class="n">ease</span><span class="o">-</span><span class="n">in</span><span class="o">-</span><span class="n">out</span><span class="p">;</span></div><div class='line' id='LC206'> <span class="n">transition</span><span class="o">:</span><span class="k">background-color</span> <span class="m">.15s</span> <span class="n">ease</span><span class="o">-</span><span class="n">in</span><span class="o">-</span><span class="n">out</span><span class="p">;</span></div><div class='line' id='LC207'><span class="p">}</span></div><div class='line' id='LC208'><span class="nf">#keyboardInputMaster</span> <span class="nt">tbody</span> <span class="nt">tr</span> <span class="nt">td</span> <span class="nt">table</span> <span class="nt">tbody</span> <span class="nt">tr</span> <span class="nt">td</span><span class="nc">.last</span> <span class="p">{</span></div><div class='line' id='LC209'> <span class="k">width</span><span class="o">:</span><span class="m">99%</span><span class="p">;</span></div><div class='line' id='LC210'><span class="p">}</span></div><div class='line' id='LC211'><span class="nf">#keyboardInputMaster</span> <span class="nt">tbody</span> <span class="nt">tr</span> <span class="nt">td</span> <span class="nt">table</span> <span class="nt">tbody</span> <span class="nt">tr</span> <span class="nt">td</span><span class="nc">.space</span> <span class="p">{</span></div><div class='line' id='LC212'> <span class="k">padding</span><span class="o">:</span><span class="m">0px</span> <span class="m">4em</span><span class="p">;</span></div><div class='line' id='LC213'><span class="p">}</span></div><div class='line' id='LC214'><span class="nf">#keyboardInputMaster</span> <span class="nt">tbody</span> <span class="nt">tr</span> <span class="nt">td</span> <span class="nt">table</span> <span class="nt">tbody</span> <span class="nt">tr</span> <span class="nt">td</span><span class="nc">.deadkey</span> <span class="p">{</span></div><div class='line' id='LC215'> <span class="k">background-color</span><span class="o">:</span><span class="m">#ccccdd</span><span class="p">;</span></div><div class='line' id='LC216'><span class="p">}</span></div><div class='line' id='LC217'><span class="nf">#keyboardInputMaster</span> <span class="nt">tbody</span> <span class="nt">tr</span> <span class="nt">td</span> <span class="nt">table</span> <span class="nt">tbody</span> <span class="nt">tr</span> <span class="nt">td</span><span class="nc">.target</span> <span class="p">{</span></div><div class='line' id='LC218'> <span class="k">background-color</span><span class="o">:</span><span class="m">#ddddcc</span><span class="p">;</span></div><div class='line' id='LC219'><span class="p">}</span></div><div class='line' id='LC220'><span class="nf">#keyboardInputMaster</span> <span class="nt">tbody</span> <span class="nt">tr</span> <span class="nt">td</span> <span class="nt">table</span> <span class="nt">tbody</span> <span class="nt">tr</span> <span class="nt">td</span><span class="nd">:hover</span><span class="o">,</span></div><div class='line' id='LC221'><span class="nf">#keyboardInputMaster</span> <span class="nt">tbody</span> <span class="nt">tr</span> <span class="nt">td</span> <span class="nt">table</span> <span class="nt">tbody</span> <span class="nt">tr</span> <span class="nt">td</span><span class="nc">.hover</span> <span class="p">{</span></div><div class='line' id='LC222'> <span class="k">border-top</span><span class="o">:</span><span class="m">1px</span> <span class="k">solid</span> <span class="m">#d5d5d5</span><span class="p">;</span></div><div class='line' id='LC223'> <span class="k">border-right</span><span class="o">:</span><span class="m">1px</span> <span class="k">solid</span> <span class="m">#555555</span><span class="p">;</span></div><div class='line' id='LC224'> <span class="k">border-bottom</span><span class="o">:</span><span class="m">1px</span> <span class="k">solid</span> <span class="m">#555555</span><span class="p">;</span></div><div class='line' id='LC225'> <span class="k">border-left</span><span class="o">:</span><span class="m">1px</span> <span class="k">solid</span> <span class="m">#d5d5d5</span><span class="p">;</span></div><div class='line' id='LC226'> <span class="k">background-color</span><span class="o">:</span><span class="m">#cccccc</span><span class="p">;</span></div><div class='line' id='LC227'><span class="p">}</span></div><div class='line' id='LC228'><span class="nf">#keyboardInputMaster</span> <span class="nt">thead</span> <span class="nt">tr</span> <span class="nt">th</span> <span class="nt">span</span><span class="nd">:active</span><span class="o">,</span></div><div class='line' id='LC229'><span class="nf">#keyboardInputMaster</span> <span class="nt">thead</span> <span class="nt">tr</span> <span class="nt">th</span> <span class="nt">span</span><span class="nc">.pressed</span><span class="o">,</span></div><div class='line' id='LC230'><span class="nf">#keyboardInputMaster</span> <span class="nt">tbody</span> <span class="nt">tr</span> <span class="nt">td</span> <span class="nt">table</span> <span class="nt">tbody</span> <span class="nt">tr</span> <span class="nt">td</span><span class="nd">:active</span><span class="o">,</span></div><div class='line' id='LC231'><span class="nf">#keyboardInputMaster</span> <span class="nt">tbody</span> <span class="nt">tr</span> <span class="nt">td</span> <span class="nt">table</span> <span class="nt">tbody</span> <span class="nt">tr</span> <span class="nt">td</span><span class="nc">.pressed</span> <span class="p">{</span></div><div class='line' id='LC232'> <span class="k">border-top</span><span class="o">:</span><span class="m">1px</span> <span class="k">solid</span> <span class="m">#555555</span> <span class="cp">!important</span><span class="p">;</span></div><div class='line' id='LC233'> <span class="k">border-right</span><span class="o">:</span><span class="m">1px</span> <span class="k">solid</span> <span class="m">#d5d5d5</span><span class="p">;</span></div><div class='line' id='LC234'> <span class="k">border-bottom</span><span class="o">:</span><span class="m">1px</span> <span class="k">solid</span> <span class="m">#d5d5d5</span><span class="p">;</span></div><div class='line' id='LC235'> <span class="k">border-left</span><span class="o">:</span><span class="m">1px</span> <span class="k">solid</span> <span class="m">#555555</span><span class="p">;</span></div><div class='line' id='LC236'> <span class="k">background-color</span><span class="o">:</span><span class="m">#cccccc</span><span class="p">;</span></div><div class='line' id='LC237'><span class="p">}</span></div><div class='line' id='LC238'><br/></div><div class='line' id='LC239'><span class="nf">#keyboardInputMaster</span> <span class="nt">tbody</span> <span class="nt">tr</span> <span class="nt">td</span> <span class="nt">table</span> <span class="nt">tbody</span> <span class="nt">tr</span> <span class="nt">td</span> <span class="nt">small</span> <span class="p">{</span></div><div class='line' id='LC240'> <span class="k">display</span><span class="o">:</span><span class="k">block</span><span class="p">;</span></div><div class='line' id='LC241'> <span class="k">text-align</span><span class="o">:</span><span class="k">center</span><span class="p">;</span></div><div class='line' id='LC242'> <span class="k">font-size</span><span class="o">:</span><span class="m">0.6em</span> <span class="cp">!important</span><span class="p">;</span></div><div class='line' id='LC243'> <span class="k">line-height</span><span class="o">:</span><span class="m">1.1em</span><span class="p">;</span></div><div class='line' id='LC244'><span class="p">}</span></div><div class='line' id='LC245'><br/></div><div class='line' id='LC246'><span class="nf">#keyboardInputMaster</span> <span class="nt">tbody</span> <span class="nt">tr</span> <span class="nt">td</span> <span class="nt">div</span> <span class="nt">label</span> <span class="p">{</span></div><div class='line' id='LC247'> <span class="k">position</span><span class="o">:</span><span class="k">absolute</span><span class="p">;</span></div><div class='line' id='LC248'> <span class="k">bottom</span><span class="o">:</span><span class="m">0.2em</span><span class="p">;</span></div><div class='line' id='LC249'> <span class="k">left</span><span class="o">:</span><span class="m">0.3em</span><span class="p">;</span></div><div class='line' id='LC250'><span class="p">}</span></div><div class='line' id='LC251'><span class="nf">#keyboardInputMaster</span> <span class="nt">tbody</span> <span class="nt">tr</span> <span class="nt">td</span> <span class="nt">div</span> <span class="nt">label</span> <span class="nt">input</span> <span class="p">{</span></div><div class='line' id='LC252'> <span class="k">background-color</span><span class="o">:</span><span class="m">#f6f6f6</span><span class="p">;</span></div><div class='line' id='LC253'> <span class="k">vertical-align</span><span class="o">:</span><span class="k">middle</span><span class="p">;</span></div><div class='line' id='LC254'> <span class="k">font-size</span><span class="o">:</span><span class="k">inherit</span><span class="p">;</span></div><div class='line' id='LC255'> <span class="k">width</span><span class="o">:</span><span class="m">1.1em</span><span class="p">;</span></div><div class='line' id='LC256'> <span class="k">height</span><span class="o">:</span><span class="m">1.1em</span><span class="p">;</span></div><div class='line' id='LC257'><span class="p">}</span></div><div class='line' id='LC258'><span class="nf">#keyboardInputMaster</span> <span class="nt">tbody</span> <span class="nt">tr</span> <span class="nt">td</span> <span class="nt">div</span> <span class="nt">var</span> <span class="p">{</span></div><div class='line' id='LC259'> <span class="k">position</span><span class="o">:</span><span class="k">absolute</span><span class="p">;</span></div><div class='line' id='LC260'> <span class="k">bottom</span><span class="o">:</span><span class="m">0px</span><span class="p">;</span></div><div class='line' id='LC261'> <span class="k">right</span><span class="o">:</span><span class="m">3px</span><span class="p">;</span></div><div class='line' id='LC262'> <span class="k">font-weight</span><span class="o">:</span><span class="k">bold</span><span class="p">;</span></div><div class='line' id='LC263'> <span class="k">font-style</span><span class="o">:</span><span class="k">italic</span><span class="p">;</span></div><div class='line' id='LC264'> <span class="k">color</span><span class="o">:</span><span class="m">#444444</span><span class="p">;</span></div><div class='line' id='LC265'><span class="p">}</span></div><div class='line' id='LC266'><br/></div><div class='line' id='LC267'><span class="nc">.keyboardInputInitiator</span> <span class="p">{</span></div><div class='line' id='LC268'> <span class="k">margin</span><span class="o">:</span><span class="m">0px</span> <span class="m">3px</span><span class="p">;</span></div><div class='line' id='LC269'> <span class="k">vertical-align</span><span class="o">:</span><span class="k">middle</span><span class="p">;</span></div><div class='line' id='LC270'> <span class="k">cursor</span><span class="o">:</span><span class="k">pointer</span><span class="p">;</span></div><div class='line' id='LC271'><span class="p">}</span></div></pre></div></td> + </tr> + </table> + </div> + + </div> +</div> + +<a href="#jump-to-line" rel="facebox[.linejump]" data-hotkey="l" class="js-jump-to-line" style="display:none">Jump to Line</a> +<div id="jump-to-line" style="display:none"> + <form accept-charset="UTF-8" class="js-jump-to-line-form"> + <input class="linejump-input js-jump-to-line-field" type="text" placeholder="Jump to line…" autofocus> + <button type="submit" class="button">Go</button> + </form> +</div> + + </div> + + </div><!-- /.repo-container --> + <div class="modal-backdrop"></div> + </div><!-- /.container --> + </div><!-- /.site --> + + + </div><!-- /.wrapper --> + + <div class="container"> + <div class="site-footer"> + <ul class="site-footer-links right"> + <li><a href="https://status.github.com/">Status</a></li> + <li><a href="http://developer.github.com">API</a></li> + <li><a href="http://training.github.com">Training</a></li> + <li><a href="http://shop.github.com">Shop</a></li> + <li><a href="/blog">Blog</a></li> + <li><a href="/about">About</a></li> + + </ul> + + <a href="/"> + <span class="mega-octicon octicon-mark-github" title="GitHub"></span> + </a> + + <ul class="site-footer-links"> + <li>© 2014 <span title="0.04003s from github-fe131-cp1-prd.iad.github.net">GitHub</span>, Inc.</li> + <li><a href="/site/terms">Terms</a></li> + <li><a href="/site/privacy">Privacy</a></li> + <li><a href="/security">Security</a></li> + <li><a href="/contact">Contact</a></li> + </ul> + </div><!-- /.site-footer --> +</div><!-- /.container --> + + + <div class="fullscreen-overlay js-fullscreen-overlay" id="fullscreen_overlay"> + <div class="fullscreen-container js-fullscreen-container"> + <div class="textarea-wrap"> + <textarea name="fullscreen-contents" id="fullscreen-contents" class="fullscreen-contents js-fullscreen-contents" placeholder="" data-suggester="fullscreen_suggester"></textarea> + </div> + </div> + <div class="fullscreen-sidebar"> + <a href="#" class="exit-fullscreen js-exit-fullscreen tooltipped tooltipped-w" aria-label="Exit Zen Mode"> + <span class="mega-octicon octicon-screen-normal"></span> + </a> + <a href="#" class="theme-switcher js-theme-switcher tooltipped tooltipped-w" + aria-label="Switch themes"> + <span class="octicon octicon-color-mode"></span> + </a> + </div> +</div> + + + + <div id="ajax-error-message" class="flash flash-error"> + <span class="octicon octicon-alert"></span> + <a href="#" class="octicon octicon-x close js-ajax-error-dismiss" aria-label="Dismiss error"></a> + Something went wrong with that request. Please try again. + </div> + + + <script crossorigin="anonymous" src="https://assets-cdn.github.com/assets/frameworks-51e3b077e56f2f3244290e430b8d05253607065b.js" type="text/javascript"></script> + <script async="async" crossorigin="anonymous" src="https://assets-cdn.github.com/assets/github-93d86f92fbe0e0d33e67339780e369f6c90000f8.js" type="text/javascript"></script> + + + <script async src="https://www.google-analytics.com/analytics.js"></script> + </body> +</html> + diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/keyboard.js b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/keyboard.js new file mode 100644 index 0000000000000000000000000000000000000000..c89411ccf3453a16b363bfd1470edb252f043b45 --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/keyboard.js @@ -0,0 +1,534 @@ +var kbdUtil = (function() { + "use strict"; + + function substituteCodepoint(cp) { + // Any Unicode code points which do not have corresponding keysym entries + // can be swapped out for another code point by adding them to this table + var substitutions = { + // {S,s} with comma below -> {S,s} with cedilla + 0x218 : 0x15e, + 0x219 : 0x15f, + // {T,t} with comma below -> {T,t} with cedilla + 0x21a : 0x162, + 0x21b : 0x163 + }; + + var sub = substitutions[cp]; + return sub ? sub : cp; + }; + + function isMac() { + return navigator && !!(/mac/i).exec(navigator.platform); + } + function isWindows() { + return navigator && !!(/win/i).exec(navigator.platform); + } + function isLinux() { + return navigator && !!(/linux/i).exec(navigator.platform); + } + + // Return true if a modifier which is not the specified char modifier (and is not shift) is down + function hasShortcutModifier(charModifier, currentModifiers) { + var mods = {}; + for (var key in currentModifiers) { + if (parseInt(key) !== 0xffe1) { + mods[key] = currentModifiers[key]; + } + } + + var sum = 0; + for (var k in currentModifiers) { + if (mods[k]) { + ++sum; + } + } + if (hasCharModifier(charModifier, mods)) { + return sum > charModifier.length; + } + else { + return sum > 0; + } + } + + // Return true if the specified char modifier is currently down + function hasCharModifier(charModifier, currentModifiers) { + if (charModifier.length === 0) { return false; } + + for (var i = 0; i < charModifier.length; ++i) { + if (!currentModifiers[charModifier[i]]) { + return false; + } + } + return true; + } + + // Helper object tracking modifier key state + // and generates fake key events to compensate if it gets out of sync + function ModifierSync(charModifier) { + var ctrl = 0xffe3; + var alt = 0xffe9; + var altGr = 0xfe03; + var shift = 0xffe1; + var meta = 0xffe7; + + if (!charModifier) { + if (isMac()) { + // on Mac, Option (AKA Alt) is used as a char modifier + charModifier = [alt]; + } + else if (isWindows()) { + // on Windows, Ctrl+Alt is used as a char modifier + charModifier = [alt, ctrl]; + } + else if (isLinux()) { + // on Linux, AltGr is used as a char modifier + charModifier = [altGr]; + } + else { + charModifier = []; + } + } + + var state = {}; + state[ctrl] = false; + state[alt] = false; + state[altGr] = false; + state[shift] = false; + state[meta] = false; + + function sync(evt, keysym) { + var result = []; + function syncKey(keysym) { + return {keysym: keysyms.lookup(keysym), type: state[keysym] ? 'keydown' : 'keyup'}; + } + + if (evt.ctrlKey !== undefined && evt.ctrlKey !== state[ctrl] && keysym !== ctrl) { + state[ctrl] = evt.ctrlKey; + result.push(syncKey(ctrl)); + } + if (evt.altKey !== undefined && evt.altKey !== state[alt] && keysym !== alt) { + state[alt] = evt.altKey; + result.push(syncKey(alt)); + } + if (evt.altGraphKey !== undefined && evt.altGraphKey !== state[altGr] && keysym !== altGr) { + state[altGr] = evt.altGraphKey; + result.push(syncKey(altGr)); + } + if (evt.shiftKey !== undefined && evt.shiftKey !== state[shift] && keysym !== shift) { + state[shift] = evt.shiftKey; + result.push(syncKey(shift)); + } + if (evt.metaKey !== undefined && evt.metaKey !== state[meta] && keysym !== meta) { + state[meta] = evt.metaKey; + result.push(syncKey(meta)); + } + return result; + } + function syncKeyEvent(evt, down) { + var obj = getKeysym(evt); + var keysym = obj ? obj.keysym : null; + + // first, apply the event itself, if relevant + if (keysym !== null && state[keysym] !== undefined) { + state[keysym] = down; + } + return sync(evt, keysym); + } + + return { + // sync on the appropriate keyboard event + keydown: function(evt) { return syncKeyEvent(evt, true);}, + keyup: function(evt) { return syncKeyEvent(evt, false);}, + // Call this with a non-keyboard event (such as mouse events) to use its modifier state to synchronize anyway + syncAny: function(evt) { return sync(evt);}, + + // is a shortcut modifier down? + hasShortcutModifier: function() { return hasShortcutModifier(charModifier, state); }, + // if a char modifier is down, return the keys it consists of, otherwise return null + activeCharModifier: function() { return hasCharModifier(charModifier, state) ? charModifier : null; } + }; + } + + // Get a key ID from a keyboard event + // May be a string or an integer depending on the available properties + function getKey(evt){ + if ('keyCode' in evt && 'key' in evt) { + return evt.key + ':' + evt.keyCode; + } + else if ('keyCode' in evt) { + return evt.keyCode; + } + else { + return evt.key; + } + } + + // Get the most reliable keysym value we can get from a key event + // if char/charCode is available, prefer those, otherwise fall back to key/keyCode/which + function getKeysym(evt){ + var codepoint; + if (evt.char && evt.char.length === 1) { + codepoint = evt.char.charCodeAt(); + } + else if (evt.charCode) { + codepoint = evt.charCode; + } + else if (evt.keyCode && evt.type === 'keypress') { + // IE10 stores the char code as keyCode, and has no other useful properties + codepoint = evt.keyCode; + } + if (codepoint) { + var res = keysyms.fromUnicode(substituteCodepoint(codepoint)); + if (res) { + return res; + } + } + // we could check evt.key here. + // Legal values are defined in http://www.w3.org/TR/DOM-Level-3-Events/#key-values-list, + // so we "just" need to map them to keysym, but AFAIK this is only available in IE10, which also provides evt.key + // so we don't *need* it yet + if (evt.keyCode) { + return keysyms.lookup(keysymFromKeyCode(evt.keyCode, evt.shiftKey)); + } + if (evt.which) { + return keysyms.lookup(keysymFromKeyCode(evt.which, evt.shiftKey)); + } + return null; + } + + // Given a keycode, try to predict which keysym it might be. + // If the keycode is unknown, null is returned. + function keysymFromKeyCode(keycode, shiftPressed) { + if (typeof(keycode) !== 'number') { + return null; + } + // won't be accurate for azerty + if (keycode >= 0x30 && keycode <= 0x39) { + return keycode; // digit + } + if (keycode >= 0x41 && keycode <= 0x5a) { + // remap to lowercase unless shift is down + return shiftPressed ? keycode : keycode + 32; // A-Z + } + if (keycode >= 0x60 && keycode <= 0x69) { + return 0xffb0 + (keycode - 0x60); // numpad 0-9 + } + + switch(keycode) { + case 0x20: return 0x20; // space + case 0x6a: return 0xffaa; // multiply + case 0x6b: return 0xffab; // add + case 0x6c: return 0xffac; // separator + case 0x6d: return 0xffad; // subtract + case 0x6e: return 0xffae; // decimal + case 0x6f: return 0xffaf; // divide + case 0xbb: return 0x2b; // + + case 0xbc: return 0x2c; // , + case 0xbd: return 0x2d; // - + case 0xbe: return 0x2e; // . + } + + return nonCharacterKey({keyCode: keycode}); + } + + // if the key is a known non-character key (any key which doesn't generate character data) + // return its keysym value. Otherwise return null + function nonCharacterKey(evt) { + // evt.key not implemented yet + if (!evt.keyCode) { return null; } + var keycode = evt.keyCode; + + if (keycode >= 0x70 && keycode <= 0x87) { + return 0xffbe + keycode - 0x70; // F1-F24 + } + switch (keycode) { + + case 8 : return 0xFF08; // BACKSPACE + case 13 : return 0xFF0D; // ENTER + + case 9 : return 0xFF09; // TAB + + case 27 : return 0xFF1B; // ESCAPE + case 46 : return 0xFFFF; // DELETE + + case 36 : return 0xFF50; // HOME + case 35 : return 0xFF57; // END + case 33 : return 0xFF55; // PAGE_UP + case 34 : return 0xFF56; // PAGE_DOWN + case 45 : return 0xFF63; // INSERT + + case 37 : return 0xFF51; // LEFT + case 38 : return 0xFF52; // UP + case 39 : return 0xFF53; // RIGHT + case 40 : return 0xFF54; // DOWN + case 16 : return 0xFFE1; // SHIFT + case 17 : return 0xFFE3; // CONTROL + case 18 : return 0xFFE9; // Left ALT (Mac Option) + + case 224 : return 0xFE07; // Meta + case 225 : return 0xFE03; // AltGr + case 91 : return 0xFFEC; // Super_L (Win Key) + case 92 : return 0xFFED; // Super_R (Win Key) + case 93 : return 0xFF67; // Menu (Win Menu), Mac Command + default: return null; + } + } + return { + hasShortcutModifier : hasShortcutModifier, + hasCharModifier : hasCharModifier, + ModifierSync : ModifierSync, + getKey : getKey, + getKeysym : getKeysym, + keysymFromKeyCode : keysymFromKeyCode, + nonCharacterKey : nonCharacterKey, + substituteCodepoint : substituteCodepoint + }; +})(); + +// Takes a DOM keyboard event and: +// - determines which keysym it represents +// - determines a keyId identifying the key that was pressed (corresponding to the key/keyCode properties on the DOM event) +// - synthesizes events to synchronize modifier key state between which modifiers are actually down, and which we thought were down +// - marks each event with an 'escape' property if a modifier was down which should be "escaped" +// - generates a "stall" event in cases where it might be necessary to wait and see if a keypress event follows a keydown +// This information is collected into an object which is passed to the next() function. (one call per event) +function KeyEventDecoder(modifierState, next) { + "use strict"; + function sendAll(evts) { + for (var i = 0; i < evts.length; ++i) { + next(evts[i]); + } + } + function process(evt, type) { + var result = {type: type}; + var keyId = kbdUtil.getKey(evt); + if (keyId) { + result.keyId = keyId; + } + + var keysym = kbdUtil.getKeysym(evt); + + var hasModifier = modifierState.hasShortcutModifier() || !!modifierState.activeCharModifier(); + // Is this a case where we have to decide on the keysym right away, rather than waiting for the keypress? + // "special" keys like enter, tab or backspace don't send keypress events, + // and some browsers don't send keypresses at all if a modifier is down + if (keysym && (type !== 'keydown' || kbdUtil.nonCharacterKey(evt) || hasModifier)) { + result.keysym = keysym; + } + + var isShift = evt.keyCode === 0x10 || evt.key === 'Shift'; + + // Should we prevent the browser from handling the event? + // Doing so on a keydown (in most browsers) prevents keypress from being generated + // so only do that if we have to. + var suppress = !isShift && (type !== 'keydown' || modifierState.hasShortcutModifier() || !!kbdUtil.nonCharacterKey(evt)); + + // If a char modifier is down on a keydown, we need to insert a stall, + // so VerifyCharModifier knows to wait and see if a keypress is comnig + var stall = type === 'keydown' && modifierState.activeCharModifier() && !kbdUtil.nonCharacterKey(evt); + + // if a char modifier is pressed, get the keys it consists of (on Windows, AltGr is equivalent to Ctrl+Alt) + var active = modifierState.activeCharModifier(); + + // If we have a char modifier down, and we're able to determine a keysym reliably + // then (a) we know to treat the modifier as a char modifier, + // and (b) we'll have to "escape" the modifier to undo the modifier when sending the char. + if (active && keysym) { + var isCharModifier = false; + for (var i = 0; i < active.length; ++i) { + if (active[i] === keysym.keysym) { + isCharModifier = true; + } + } + if (type === 'keypress' && !isCharModifier) { + result.escape = modifierState.activeCharModifier(); + } + } + + if (stall) { + // insert a fake "stall" event + next({type: 'stall'}); + } + next(result); + + return suppress; + } + + return { + keydown: function(evt) { + sendAll(modifierState.keydown(evt)); + return process(evt, 'keydown'); + }, + keypress: function(evt) { + return process(evt, 'keypress'); + }, + keyup: function(evt) { + sendAll(modifierState.keyup(evt)); + return process(evt, 'keyup'); + }, + syncModifiers: function(evt) { + sendAll(modifierState.syncAny(evt)); + }, + releaseAll: function() { next({type: 'releaseall'}); } + }; +} + +// Combines keydown and keypress events where necessary to handle char modifiers. +// On some OS'es, a char modifier is sometimes used as a shortcut modifier. +// For example, on Windows, AltGr is synonymous with Ctrl-Alt. On a Danish keyboard layout, AltGr-2 yields a @, but Ctrl-Alt-D does nothing +// so when used with the '2' key, Ctrl-Alt counts as a char modifier (and should be escaped), but when used with 'D', it does not. +// The only way we can distinguish these cases is to wait and see if a keypress event arrives +// When we receive a "stall" event, wait a few ms before processing the next keydown. If a keypress has also arrived, merge the two +function VerifyCharModifier(next) { + "use strict"; + var queue = []; + var timer = null; + function process() { + if (timer) { + return; + } + while (queue.length !== 0) { + var cur = queue[0]; + queue = queue.splice(1); + switch (cur.type) { + case 'stall': + // insert a delay before processing available events. + timer = setTimeout(function() { + clearTimeout(timer); + timer = null; + process(); + }, 5); + return; + case 'keydown': + // is the next element a keypress? Then we should merge the two + if (queue.length !== 0 && queue[0].type === 'keypress') { + // Firefox sends keypress even when no char is generated. + // so, if keypress keysym is the same as we'd have guessed from keydown, + // the modifier didn't have any effect, and should not be escaped + if (queue[0].escape && (!cur.keysym || cur.keysym.keysym !== queue[0].keysym.keysym)) { + cur.escape = queue[0].escape; + } + cur.keysym = queue[0].keysym; + queue = queue.splice(1); + } + break; + } + + // swallow stall events, and pass all others to the next stage + if (cur.type !== 'stall') { + next(cur); + } + } + } + return function(evt) { + queue.push(evt); + process(); + }; +} + +// Keeps track of which keys we (and the server) believe are down +// When a keyup is received, match it against this list, to determine the corresponding keysym(s) +// in some cases, a single key may produce multiple keysyms, so the corresponding keyup event must release all of these chars +// key repeat events should be merged into a single entry. +// Because we can't always identify which entry a keydown or keyup event corresponds to, we sometimes have to guess +function TrackKeyState(next) { + "use strict"; + var state = []; + + return function (evt) { + var last = state.length !== 0 ? state[state.length-1] : null; + + switch (evt.type) { + case 'keydown': + // insert a new entry if last seen key was different. + if (!last || !evt.keyId || last.keyId !== evt.keyId) { + last = {keyId: evt.keyId, keysyms: {}}; + state.push(last); + } + if (evt.keysym) { + // make sure last event contains this keysym (a single "logical" keyevent + // can cause multiple key events to be sent to the VNC server) + last.keysyms[evt.keysym.keysym] = evt.keysym; + last.ignoreKeyPress = true; + next(evt); + } + break; + case 'keypress': + if (!last) { + last = {keyId: evt.keyId, keysyms: {}}; + state.push(last); + } + if (!evt.keysym) { + console.log('keypress with no keysym:', evt); + } + + // If we didn't expect a keypress, and already sent a keydown to the VNC server + // based on the keydown, make sure to skip this event. + if (evt.keysym && !last.ignoreKeyPress) { + last.keysyms[evt.keysym.keysym] = evt.keysym; + evt.type = 'keydown'; + next(evt); + } + break; + case 'keyup': + if (state.length === 0) { + return; + } + var idx = null; + // do we have a matching key tracked as being down? + for (var i = 0; i !== state.length; ++i) { + if (state[i].keyId === evt.keyId) { + idx = i; + break; + } + } + // if we couldn't find a match (it happens), assume it was the last key pressed + if (idx === null) { + idx = state.length - 1; + } + + var item = state.splice(idx, 1)[0]; + // for each keysym tracked by this key entry, clone the current event and override the keysym + for (var key in item.keysyms) { + var clone = (function(){ + function Clone(){} + return function (obj) { Clone.prototype=obj; return new Clone(); }; + }()); + var out = clone(evt); + out.keysym = item.keysyms[key]; + next(out); + } + break; + case 'releaseall': + for (var i = 0; i < state.length; ++i) { + for (var key in state[i].keysyms) { + var keysym = state[i].keysyms[key]; + next({keyId: 0, keysym: keysym, type: 'keyup'}); + } + } + state = []; + } + }; +} + +// Handles "escaping" of modifiers: if a char modifier is used to produce a keysym (such as AltGr-2 to generate an @), +// then the modifier must be "undone" before sending the @, and "redone" afterwards. +function EscapeModifiers(next) { + "use strict"; + return function(evt) { + if (evt.type !== 'keydown' || evt.escape === undefined) { + next(evt); + return; + } + // undo modifiers + for (var i = 0; i < evt.escape.length; ++i) { + next({type: 'keyup', keyId: 0, keysym: keysyms.lookup(evt.escape[i])}); + } + // send the character event + next(evt); + // redo modifiers + for (var i = 0; i < evt.escape.length; ++i) { + next({type: 'keydown', keyId: 0, keysym: keysyms.lookup(evt.escape[i])}); + } + }; +} diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/keysym.js b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/keysym.js new file mode 100644 index 0000000000000000000000000000000000000000..a00d595e3f454eab4e28ff42d30642cdabc19d70 --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/keysym.js @@ -0,0 +1,376 @@ +var XK_VoidSymbol = 0xffffff, /* Void symbol */ + +XK_BackSpace = 0xff08, /* Back space, back char */ +XK_Tab = 0xff09, +XK_Linefeed = 0xff0a, /* Linefeed, LF */ +XK_Clear = 0xff0b, +XK_Return = 0xff0d, /* Return, enter */ +XK_Pause = 0xff13, /* Pause, hold */ +XK_Scroll_Lock = 0xff14, +XK_Sys_Req = 0xff15, +XK_Escape = 0xff1b, +XK_Delete = 0xffff, /* Delete, rubout */ + +/* Cursor control & motion */ + +XK_Home = 0xff50, +XK_Left = 0xff51, /* Move left, left arrow */ +XK_Up = 0xff52, /* Move up, up arrow */ +XK_Right = 0xff53, /* Move right, right arrow */ +XK_Down = 0xff54, /* Move down, down arrow */ +XK_Prior = 0xff55, /* Prior, previous */ +XK_Page_Up = 0xff55, +XK_Next = 0xff56, /* Next */ +XK_Page_Down = 0xff56, +XK_End = 0xff57, /* EOL */ +XK_Begin = 0xff58, /* BOL */ + + +/* Misc functions */ + +XK_Select = 0xff60, /* Select, mark */ +XK_Print = 0xff61, +XK_Execute = 0xff62, /* Execute, run, do */ +XK_Insert = 0xff63, /* Insert, insert here */ +XK_Undo = 0xff65, +XK_Redo = 0xff66, /* Redo, again */ +XK_Menu = 0xff67, +XK_Find = 0xff68, /* Find, search */ +XK_Cancel = 0xff69, /* Cancel, stop, abort, exit */ +XK_Help = 0xff6a, /* Help */ +XK_Break = 0xff6b, +XK_Mode_switch = 0xff7e, /* Character set switch */ +XK_script_switch = 0xff7e, /* Alias for mode_switch */ +XK_Num_Lock = 0xff7f, + +/* Keypad functions, keypad numbers cleverly chosen to map to ASCII */ + +XK_KP_Space = 0xff80, /* Space */ +XK_KP_Tab = 0xff89, +XK_KP_Enter = 0xff8d, /* Enter */ +XK_KP_F1 = 0xff91, /* PF1, KP_A, ... */ +XK_KP_F2 = 0xff92, +XK_KP_F3 = 0xff93, +XK_KP_F4 = 0xff94, +XK_KP_Home = 0xff95, +XK_KP_Left = 0xff96, +XK_KP_Up = 0xff97, +XK_KP_Right = 0xff98, +XK_KP_Down = 0xff99, +XK_KP_Prior = 0xff9a, +XK_KP_Page_Up = 0xff9a +XK_KP_Next = 0xff9b, +XK_KP_Page_Down = 0xff9b, +XK_KP_End = 0xff9c, +XK_KP_Begin = 0xff9d, +XK_KP_Insert = 0xff9e, +XK_KP_Delete = 0xff9f, +XK_KP_Equal = 0xffbd, /* Equals */ +XK_KP_Multiply = 0xffaa, +XK_KP_Add = 0xffab, +XK_KP_Separator = 0xffac, /* Separator, often comma */ +XK_KP_Subtract = 0xffad, +XK_KP_Decimal = 0xffae, +XK_KP_Divide = 0xffaf, + +XK_KP_0 = 0xffb0, +XK_KP_1 = 0xffb1, +XK_KP_2 = 0xffb2, +XK_KP_3 = 0xffb3, +XK_KP_4 = 0xffb4, +XK_KP_5 = 0xffb5, +XK_KP_6 = 0xffb6, +XK_KP_7 = 0xffb7, +XK_KP_8 = 0xffb8, +XK_KP_9 = 0xffb9, + +/* + * Auxiliary functions; note the duplicate definitions for left and right + * function keys; Sun keyboards and a few other manufacturers have such + * function key groups on the left and/or right sides of the keyboard. + * We've not found a keyboard with more than 35 function keys total. + */ + +XK_F1 = 0xffbe, +XK_F2 = 0xffbf, +XK_F3 = 0xffc0, +XK_F4 = 0xffc1, +XK_F5 = 0xffc2, +XK_F6 = 0xffc3, +XK_F7 = 0xffc4, +XK_F8 = 0xffc5, +XK_F9 = 0xffc6, +XK_F10 = 0xffc7, +XK_F11 = 0xffc8, +XK_L1 = 0xffc8, +XK_F12 = 0xffc9, +XK_L2 = 0xffc9, +XK_F13 = 0xffca, +XK_L3 = 0xffca, +XK_F14 = 0xffcb, +XK_L4 = 0xffcb, +XK_F15 = 0xffcc, +XK_L5 = 0xffcc, +XK_F16 = 0xffcd, +XK_L6 = 0xffcd, +XK_F17 = 0xffce, +XK_L7 = 0xffce, +XK_F18 = 0xffcf, +XK_L8 = 0xffcf, +XK_F19 = 0xffd0, +XK_L9 = 0xffd0, +XK_F20 = 0xffd1, +XK_L10 = 0xffd1, +XK_F21 = 0xffd2, +XK_R1 = 0xffd2, +XK_F22 = 0xffd3, +XK_R2 = 0xffd3, +XK_F23 = 0xffd4, +XK_R3 = 0xffd4, +XK_F24 = 0xffd5, +XK_R4 = 0xffd5, +XK_F25 = 0xffd6, +XK_R5 = 0xffd6, +XK_F26 = 0xffd7, +XK_R6 = 0xffd7, +XK_F27 = 0xffd8, +XK_R7 = 0xffd8, +XK_F28 = 0xffd9, +XK_R8 = 0xffd9, +XK_F29 = 0xffda, +XK_R9 = 0xffda, +XK_F30 = 0xffdb, +XK_R10 = 0xffdb, +XK_F31 = 0xffdc, +XK_R11 = 0xffdc, +XK_F32 = 0xffdd, +XK_R12 = 0xffdd, +XK_F33 = 0xffde, +XK_R13 = 0xffde, +XK_F34 = 0xffdf, +XK_R14 = 0xffdf, +XK_F35 = 0xffe0, +XK_R15 = 0xffe0, + +/* Modifiers */ + +XK_Shift_L = 0xffe1, /* Left shift */ +XK_Shift_R = 0xffe2, /* Right shift */ +XK_Control_L = 0xffe3, /* Left control */ +XK_Control_R = 0xffe4, /* Right control */ +XK_Caps_Lock = 0xffe5, /* Caps lock */ +XK_Shift_Lock = 0xffe6, /* Shift lock */ + +XK_Meta_L = 0xffe7, /* Left meta */ +XK_Meta_R = 0xffe8, /* Right meta */ +XK_Alt_L = 0xffe9, /* Left alt */ +XK_Alt_R = 0xffea, /* Right alt */ +XK_Super_L = 0xffeb, /* Left super */ +XK_Super_R = 0xffec, /* Right super */ +XK_Hyper_L = 0xffed, /* Left hyper */ +XK_Hyper_R = 0xffee, /* Right hyper */ + +/* + * Latin 1 + * (ISO/IEC 8859-1 = Unicode U+0020..U+00FF) + * Byte 3 = 0 + */ + +XK_space = 0x0020, /* U+0020 SPACE */ +XK_exclam = 0x0021, /* U+0021 EXCLAMATION MARK */ +XK_quotedbl = 0x0022, /* U+0022 QUOTATION MARK */ +XK_numbersign = 0x0023, /* U+0023 NUMBER SIGN */ +XK_dollar = 0x0024, /* U+0024 DOLLAR SIGN */ +XK_percent = 0x0025, /* U+0025 PERCENT SIGN */ +XK_ampersand = 0x0026, /* U+0026 AMPERSAND */ +XK_apostrophe = 0x0027, /* U+0027 APOSTROPHE */ +XK_quoteright = 0x0027, /* deprecated */ +XK_parenleft = 0x0028, /* U+0028 LEFT PARENTHESIS */ +XK_parenright = 0x0029, /* U+0029 RIGHT PARENTHESIS */ +XK_asterisk = 0x002a, /* U+002A ASTERISK */ +XK_plus = 0x002b, /* U+002B PLUS SIGN */ +XK_comma = 0x002c, /* U+002C COMMA */ +XK_minus = 0x002d, /* U+002D HYPHEN-MINUS */ +XK_period = 0x002e, /* U+002E FULL STOP */ +XK_slash = 0x002f, /* U+002F SOLIDUS */ +XK_0 = 0x0030, /* U+0030 DIGIT ZERO */ +XK_1 = 0x0031, /* U+0031 DIGIT ONE */ +XK_2 = 0x0032, /* U+0032 DIGIT TWO */ +XK_3 = 0x0033, /* U+0033 DIGIT THREE */ +XK_4 = 0x0034, /* U+0034 DIGIT FOUR */ +XK_5 = 0x0035, /* U+0035 DIGIT FIVE */ +XK_6 = 0x0036, /* U+0036 DIGIT SIX */ +XK_7 = 0x0037, /* U+0037 DIGIT SEVEN */ +XK_8 = 0x0038, /* U+0038 DIGIT EIGHT */ +XK_9 = 0x0039, /* U+0039 DIGIT NINE */ +XK_colon = 0x003a, /* U+003A COLON */ +XK_semicolon = 0x003b, /* U+003B SEMICOLON */ +XK_less = 0x003c, /* U+003C LESS-THAN SIGN */ +XK_equal = 0x003d, /* U+003D EQUALS SIGN */ +XK_greater = 0x003e, /* U+003E GREATER-THAN SIGN */ +XK_question = 0x003f, /* U+003F QUESTION MARK */ +XK_at = 0x0040, /* U+0040 COMMERCIAL AT */ +XK_A = 0x0041, /* U+0041 LATIN CAPITAL LETTER A */ +XK_B = 0x0042, /* U+0042 LATIN CAPITAL LETTER B */ +XK_C = 0x0043, /* U+0043 LATIN CAPITAL LETTER C */ +XK_D = 0x0044, /* U+0044 LATIN CAPITAL LETTER D */ +XK_E = 0x0045, /* U+0045 LATIN CAPITAL LETTER E */ +XK_F = 0x0046, /* U+0046 LATIN CAPITAL LETTER F */ +XK_G = 0x0047, /* U+0047 LATIN CAPITAL LETTER G */ +XK_H = 0x0048, /* U+0048 LATIN CAPITAL LETTER H */ +XK_I = 0x0049, /* U+0049 LATIN CAPITAL LETTER I */ +XK_J = 0x004a, /* U+004A LATIN CAPITAL LETTER J */ +XK_K = 0x004b, /* U+004B LATIN CAPITAL LETTER K */ +XK_L = 0x004c, /* U+004C LATIN CAPITAL LETTER L */ +XK_M = 0x004d, /* U+004D LATIN CAPITAL LETTER M */ +XK_N = 0x004e, /* U+004E LATIN CAPITAL LETTER N */ +XK_O = 0x004f, /* U+004F LATIN CAPITAL LETTER O */ +XK_P = 0x0050, /* U+0050 LATIN CAPITAL LETTER P */ +XK_Q = 0x0051, /* U+0051 LATIN CAPITAL LETTER Q */ +XK_R = 0x0052, /* U+0052 LATIN CAPITAL LETTER R */ +XK_S = 0x0053, /* U+0053 LATIN CAPITAL LETTER S */ +XK_T = 0x0054, /* U+0054 LATIN CAPITAL LETTER T */ +XK_U = 0x0055, /* U+0055 LATIN CAPITAL LETTER U */ +XK_V = 0x0056, /* U+0056 LATIN CAPITAL LETTER V */ +XK_W = 0x0057, /* U+0057 LATIN CAPITAL LETTER W */ +XK_X = 0x0058, /* U+0058 LATIN CAPITAL LETTER X */ +XK_Y = 0x0059, /* U+0059 LATIN CAPITAL LETTER Y */ +XK_Z = 0x005a, /* U+005A LATIN CAPITAL LETTER Z */ +XK_bracketleft = 0x005b, /* U+005B LEFT SQUARE BRACKET */ +XK_backslash = 0x005c, /* U+005C REVERSE SOLIDUS */ +XK_bracketright = 0x005d, /* U+005D RIGHT SQUARE BRACKET */ +XK_asciicircum = 0x005e, /* U+005E CIRCUMFLEX ACCENT */ +XK_underscore = 0x005f, /* U+005F LOW LINE */ +XK_grave = 0x0060, /* U+0060 GRAVE ACCENT */ +XK_quoteleft = 0x0060, /* deprecated */ +XK_a = 0x0061, /* U+0061 LATIN SMALL LETTER A */ +XK_b = 0x0062, /* U+0062 LATIN SMALL LETTER B */ +XK_c = 0x0063, /* U+0063 LATIN SMALL LETTER C */ +XK_d = 0x0064, /* U+0064 LATIN SMALL LETTER D */ +XK_e = 0x0065, /* U+0065 LATIN SMALL LETTER E */ +XK_f = 0x0066, /* U+0066 LATIN SMALL LETTER F */ +XK_g = 0x0067, /* U+0067 LATIN SMALL LETTER G */ +XK_h = 0x0068, /* U+0068 LATIN SMALL LETTER H */ +XK_i = 0x0069, /* U+0069 LATIN SMALL LETTER I */ +XK_j = 0x006a, /* U+006A LATIN SMALL LETTER J */ +XK_k = 0x006b, /* U+006B LATIN SMALL LETTER K */ +XK_l = 0x006c, /* U+006C LATIN SMALL LETTER L */ +XK_m = 0x006d, /* U+006D LATIN SMALL LETTER M */ +XK_n = 0x006e, /* U+006E LATIN SMALL LETTER N */ +XK_o = 0x006f, /* U+006F LATIN SMALL LETTER O */ +XK_p = 0x0070, /* U+0070 LATIN SMALL LETTER P */ +XK_q = 0x0071, /* U+0071 LATIN SMALL LETTER Q */ +XK_r = 0x0072, /* U+0072 LATIN SMALL LETTER R */ +XK_s = 0x0073, /* U+0073 LATIN SMALL LETTER S */ +XK_t = 0x0074, /* U+0074 LATIN SMALL LETTER T */ +XK_u = 0x0075, /* U+0075 LATIN SMALL LETTER U */ +XK_v = 0x0076, /* U+0076 LATIN SMALL LETTER V */ +XK_w = 0x0077, /* U+0077 LATIN SMALL LETTER W */ +XK_x = 0x0078, /* U+0078 LATIN SMALL LETTER X */ +XK_y = 0x0079, /* U+0079 LATIN SMALL LETTER Y */ +XK_z = 0x007a, /* U+007A LATIN SMALL LETTER Z */ +XK_braceleft = 0x007b, /* U+007B LEFT CURLY BRACKET */ +XK_bar = 0x007c, /* U+007C VERTICAL LINE */ +XK_braceright = 0x007d, /* U+007D RIGHT CURLY BRACKET */ +XK_asciitilde = 0x007e, /* U+007E TILDE */ + +XK_nobreakspace = 0x00a0, /* U+00A0 NO-BREAK SPACE */ +XK_exclamdown = 0x00a1, /* U+00A1 INVERTED EXCLAMATION MARK */ +XK_cent = 0x00a2, /* U+00A2 CENT SIGN */ +XK_sterling = 0x00a3, /* U+00A3 POUND SIGN */ +XK_currency = 0x00a4, /* U+00A4 CURRENCY SIGN */ +XK_yen = 0x00a5, /* U+00A5 YEN SIGN */ +XK_brokenbar = 0x00a6, /* U+00A6 BROKEN BAR */ +XK_section = 0x00a7, /* U+00A7 SECTION SIGN */ +XK_diaeresis = 0x00a8, /* U+00A8 DIAERESIS */ +XK_copyright = 0x00a9, /* U+00A9 COPYRIGHT SIGN */ +XK_ordfeminine = 0x00aa, /* U+00AA FEMININE ORDINAL INDICATOR */ +XK_guillemotleft = 0x00ab, /* U+00AB LEFT-POINTING DOUBLE ANGLE QUOTATION MARK */ +XK_notsign = 0x00ac, /* U+00AC NOT SIGN */ +XK_hyphen = 0x00ad, /* U+00AD SOFT HYPHEN */ +XK_registered = 0x00ae, /* U+00AE REGISTERED SIGN */ +XK_macron = 0x00af, /* U+00AF MACRON */ +XK_degree = 0x00b0, /* U+00B0 DEGREE SIGN */ +XK_plusminus = 0x00b1, /* U+00B1 PLUS-MINUS SIGN */ +XK_twosuperior = 0x00b2, /* U+00B2 SUPERSCRIPT TWO */ +XK_threesuperior = 0x00b3, /* U+00B3 SUPERSCRIPT THREE */ +XK_acute = 0x00b4, /* U+00B4 ACUTE ACCENT */ +XK_mu = 0x00b5, /* U+00B5 MICRO SIGN */ +XK_paragraph = 0x00b6, /* U+00B6 PILCROW SIGN */ +XK_periodcentered = 0x00b7, /* U+00B7 MIDDLE DOT */ +XK_cedilla = 0x00b8, /* U+00B8 CEDILLA */ +XK_onesuperior = 0x00b9, /* U+00B9 SUPERSCRIPT ONE */ +XK_masculine = 0x00ba, /* U+00BA MASCULINE ORDINAL INDICATOR */ +XK_guillemotright = 0x00bb, /* U+00BB RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK */ +XK_onequarter = 0x00bc, /* U+00BC VULGAR FRACTION ONE QUARTER */ +XK_onehalf = 0x00bd, /* U+00BD VULGAR FRACTION ONE HALF */ +XK_threequarters = 0x00be, /* U+00BE VULGAR FRACTION THREE QUARTERS */ +XK_questiondown = 0x00bf, /* U+00BF INVERTED QUESTION MARK */ +XK_Agrave = 0x00c0, /* U+00C0 LATIN CAPITAL LETTER A WITH GRAVE */ +XK_Aacute = 0x00c1, /* U+00C1 LATIN CAPITAL LETTER A WITH ACUTE */ +XK_Acircumflex = 0x00c2, /* U+00C2 LATIN CAPITAL LETTER A WITH CIRCUMFLEX */ +XK_Atilde = 0x00c3, /* U+00C3 LATIN CAPITAL LETTER A WITH TILDE */ +XK_Adiaeresis = 0x00c4, /* U+00C4 LATIN CAPITAL LETTER A WITH DIAERESIS */ +XK_Aring = 0x00c5, /* U+00C5 LATIN CAPITAL LETTER A WITH RING ABOVE */ +XK_AE = 0x00c6, /* U+00C6 LATIN CAPITAL LETTER AE */ +XK_Ccedilla = 0x00c7, /* U+00C7 LATIN CAPITAL LETTER C WITH CEDILLA */ +XK_Egrave = 0x00c8, /* U+00C8 LATIN CAPITAL LETTER E WITH GRAVE */ +XK_Eacute = 0x00c9, /* U+00C9 LATIN CAPITAL LETTER E WITH ACUTE */ +XK_Ecircumflex = 0x00ca, /* U+00CA LATIN CAPITAL LETTER E WITH CIRCUMFLEX */ +XK_Ediaeresis = 0x00cb, /* U+00CB LATIN CAPITAL LETTER E WITH DIAERESIS */ +XK_Igrave = 0x00cc, /* U+00CC LATIN CAPITAL LETTER I WITH GRAVE */ +XK_Iacute = 0x00cd, /* U+00CD LATIN CAPITAL LETTER I WITH ACUTE */ +XK_Icircumflex = 0x00ce, /* U+00CE LATIN CAPITAL LETTER I WITH CIRCUMFLEX */ +XK_Idiaeresis = 0x00cf, /* U+00CF LATIN CAPITAL LETTER I WITH DIAERESIS */ +XK_ETH = 0x00d0, /* U+00D0 LATIN CAPITAL LETTER ETH */ +XK_Eth = 0x00d0, /* deprecated */ +XK_Ntilde = 0x00d1, /* U+00D1 LATIN CAPITAL LETTER N WITH TILDE */ +XK_Ograve = 0x00d2, /* U+00D2 LATIN CAPITAL LETTER O WITH GRAVE */ +XK_Oacute = 0x00d3, /* U+00D3 LATIN CAPITAL LETTER O WITH ACUTE */ +XK_Ocircumflex = 0x00d4, /* U+00D4 LATIN CAPITAL LETTER O WITH CIRCUMFLEX */ +XK_Otilde = 0x00d5, /* U+00D5 LATIN CAPITAL LETTER O WITH TILDE */ +XK_Odiaeresis = 0x00d6, /* U+00D6 LATIN CAPITAL LETTER O WITH DIAERESIS */ +XK_multiply = 0x00d7, /* U+00D7 MULTIPLICATION SIGN */ +XK_Oslash = 0x00d8, /* U+00D8 LATIN CAPITAL LETTER O WITH STROKE */ +XK_Ooblique = 0x00d8, /* U+00D8 LATIN CAPITAL LETTER O WITH STROKE */ +XK_Ugrave = 0x00d9, /* U+00D9 LATIN CAPITAL LETTER U WITH GRAVE */ +XK_Uacute = 0x00da, /* U+00DA LATIN CAPITAL LETTER U WITH ACUTE */ +XK_Ucircumflex = 0x00db, /* U+00DB LATIN CAPITAL LETTER U WITH CIRCUMFLEX */ +XK_Udiaeresis = 0x00dc, /* U+00DC LATIN CAPITAL LETTER U WITH DIAERESIS */ +XK_Yacute = 0x00dd, /* U+00DD LATIN CAPITAL LETTER Y WITH ACUTE */ +XK_THORN = 0x00de, /* U+00DE LATIN CAPITAL LETTER THORN */ +XK_Thorn = 0x00de, /* deprecated */ +XK_ssharp = 0x00df, /* U+00DF LATIN SMALL LETTER SHARP S */ +XK_agrave = 0x00e0, /* U+00E0 LATIN SMALL LETTER A WITH GRAVE */ +XK_aacute = 0x00e1, /* U+00E1 LATIN SMALL LETTER A WITH ACUTE */ +XK_acircumflex = 0x00e2, /* U+00E2 LATIN SMALL LETTER A WITH CIRCUMFLEX */ +XK_atilde = 0x00e3, /* U+00E3 LATIN SMALL LETTER A WITH TILDE */ +XK_adiaeresis = 0x00e4, /* U+00E4 LATIN SMALL LETTER A WITH DIAERESIS */ +XK_aring = 0x00e5, /* U+00E5 LATIN SMALL LETTER A WITH RING ABOVE */ +XK_ae = 0x00e6, /* U+00E6 LATIN SMALL LETTER AE */ +XK_ccedilla = 0x00e7, /* U+00E7 LATIN SMALL LETTER C WITH CEDILLA */ +XK_egrave = 0x00e8, /* U+00E8 LATIN SMALL LETTER E WITH GRAVE */ +XK_eacute = 0x00e9, /* U+00E9 LATIN SMALL LETTER E WITH ACUTE */ +XK_ecircumflex = 0x00ea, /* U+00EA LATIN SMALL LETTER E WITH CIRCUMFLEX */ +XK_ediaeresis = 0x00eb, /* U+00EB LATIN SMALL LETTER E WITH DIAERESIS */ +XK_igrave = 0x00ec, /* U+00EC LATIN SMALL LETTER I WITH GRAVE */ +XK_iacute = 0x00ed, /* U+00ED LATIN SMALL LETTER I WITH ACUTE */ +XK_icircumflex = 0x00ee, /* U+00EE LATIN SMALL LETTER I WITH CIRCUMFLEX */ +XK_idiaeresis = 0x00ef, /* U+00EF LATIN SMALL LETTER I WITH DIAERESIS */ +XK_eth = 0x00f0, /* U+00F0 LATIN SMALL LETTER ETH */ +XK_ntilde = 0x00f1, /* U+00F1 LATIN SMALL LETTER N WITH TILDE */ +XK_ograve = 0x00f2, /* U+00F2 LATIN SMALL LETTER O WITH GRAVE */ +XK_oacute = 0x00f3, /* U+00F3 LATIN SMALL LETTER O WITH ACUTE */ +XK_ocircumflex = 0x00f4, /* U+00F4 LATIN SMALL LETTER O WITH CIRCUMFLEX */ +XK_otilde = 0x00f5, /* U+00F5 LATIN SMALL LETTER O WITH TILDE */ +XK_odiaeresis = 0x00f6, /* U+00F6 LATIN SMALL LETTER O WITH DIAERESIS */ +XK_division = 0x00f7, /* U+00F7 DIVISION SIGN */ +XK_oslash = 0x00f8, /* U+00F8 LATIN SMALL LETTER O WITH STROKE */ +XK_ooblique = 0x00f8, /* U+00F8 LATIN SMALL LETTER O WITH STROKE */ +XK_ugrave = 0x00f9, /* U+00F9 LATIN SMALL LETTER U WITH GRAVE */ +XK_uacute = 0x00fa, /* U+00FA LATIN SMALL LETTER U WITH ACUTE */ +XK_ucircumflex = 0x00fb, /* U+00FB LATIN SMALL LETTER U WITH CIRCUMFLEX */ +XK_udiaeresis = 0x00fc, /* U+00FC LATIN SMALL LETTER U WITH DIAERESIS */ +XK_yacute = 0x00fd, /* U+00FD LATIN SMALL LETTER Y WITH ACUTE */ +XK_thorn = 0x00fe, /* U+00FE LATIN SMALL LETTER THORN */ +XK_ydiaeresis = 0x00ff; /* U+00FF LATIN SMALL LETTER Y WITH DIAERESIS */ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/keysymdef.js b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/keysymdef.js new file mode 100644 index 0000000000000000000000000000000000000000..f94445cf3844abc983d1965cf0330cc139cb517f --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/keysymdef.js @@ -0,0 +1,15 @@ +// This file describes mappings from Unicode codepoints to the keysym values +// (and optionally, key names) expected by the RFB protocol +// How this file was generated: +// node /Users/jalf/dev/mi/novnc/utils/parse.js /opt/X11/include/X11/keysymdef.h +var keysyms = (function(){ + "use strict"; + var keynames = null; + var codepoints = {"32":32,"33":33,"34":34,"35":35,"36":36,"37":37,"38":38,"39":39,"40":40,"41":41,"42":42,"43":43,"44":44,"45":45,"46":46,"47":47,"48":48,"49":49,"50":50,"51":51,"52":52,"53":53,"54":54,"55":55,"56":56,"57":57,"58":58,"59":59,"60":60,"61":61,"62":62,"63":63,"64":64,"65":65,"66":66,"67":67,"68":68,"69":69,"70":70,"71":71,"72":72,"73":73,"74":74,"75":75,"76":76,"77":77,"78":78,"79":79,"80":80,"81":81,"82":82,"83":83,"84":84,"85":85,"86":86,"87":87,"88":88,"89":89,"90":90,"91":91,"92":92,"93":93,"94":94,"95":95,"96":96,"97":97,"98":98,"99":99,"100":100,"101":101,"102":102,"103":103,"104":104,"105":105,"106":106,"107":107,"108":108,"109":109,"110":110,"111":111,"112":112,"113":113,"114":114,"115":115,"116":116,"117":117,"118":118,"119":119,"120":120,"121":121,"122":122,"123":123,"124":124,"125":125,"126":126,"160":160,"161":161,"162":162,"163":163,"164":164,"165":165,"166":166,"167":167,"168":168,"169":169,"170":170,"171":171,"172":172,"173":173,"174":174,"175":175,"176":176,"177":177,"178":178,"179":179,"180":180,"181":181,"182":182,"183":183,"184":184,"185":185,"186":186,"187":187,"188":188,"189":189,"190":190,"191":191,"192":192,"193":193,"194":194,"195":195,"196":196,"197":197,"198":198,"199":199,"200":200,"201":201,"202":202,"203":203,"204":204,"205":205,"206":206,"207":207,"208":208,"209":209,"210":210,"211":211,"212":212,"213":213,"214":214,"215":215,"216":216,"217":217,"218":218,"219":219,"220":220,"221":221,"222":222,"223":223,"224":224,"225":225,"226":226,"227":227,"228":228,"229":229,"230":230,"231":231,"232":232,"233":233,"234":234,"235":235,"236":236,"237":237,"238":238,"239":239,"240":240,"241":241,"242":242,"243":243,"244":244,"245":245,"246":246,"247":247,"248":248,"249":249,"250":250,"251":251,"252":252,"253":253,"254":254,"255":255,"256":960,"257":992,"258":451,"259":483,"260":417,"261":433,"262":454,"263":486,"264":710,"265":742,"266":709,"267":741,"268":456,"269":488,"270":463,"271":495,"272":464,"273":496,"274":938,"275":954,"278":972,"279":1004,"280":458,"281":490,"282":460,"283":492,"284":728,"285":760,"286":683,"287":699,"288":725,"289":757,"290":939,"291":955,"292":678,"293":694,"294":673,"295":689,"296":933,"297":949,"298":975,"299":1007,"300":16777516,"301":16777517,"302":967,"303":999,"304":681,"305":697,"308":684,"309":700,"310":979,"311":1011,"312":930,"313":453,"314":485,"315":934,"316":950,"317":421,"318":437,"321":419,"322":435,"323":465,"324":497,"325":977,"326":1009,"327":466,"328":498,"330":957,"331":959,"332":978,"333":1010,"336":469,"337":501,"338":5052,"339":5053,"340":448,"341":480,"342":931,"343":947,"344":472,"345":504,"346":422,"347":438,"348":734,"349":766,"350":426,"351":442,"352":425,"353":441,"354":478,"355":510,"356":427,"357":443,"358":940,"359":956,"360":989,"361":1021,"362":990,"363":1022,"364":733,"365":765,"366":473,"367":505,"368":475,"369":507,"370":985,"371":1017,"372":16777588,"373":16777589,"374":16777590,"375":16777591,"376":5054,"377":428,"378":444,"379":431,"380":447,"381":430,"382":446,"399":16777615,"402":2294,"415":16777631,"416":16777632,"417":16777633,"431":16777647,"432":16777648,"437":16777653,"438":16777654,"439":16777655,"466":16777681,"486":16777702,"487":16777703,"601":16777817,"629":16777845,"658":16777874,"711":439,"728":418,"729":511,"731":434,"733":445,"901":1966,"902":1953,"904":1954,"905":1955,"906":1956,"908":1959,"910":1960,"911":1963,"912":1974,"913":1985,"914":1986,"915":1987,"916":1988,"917":1989,"918":1990,"919":1991,"920":1992,"921":1993,"922":1994,"923":1995,"924":1996,"925":1997,"926":1998,"927":1999,"928":2000,"929":2001,"931":2002,"932":2004,"933":2005,"934":2006,"935":2007,"936":2008,"937":2009,"938":1957,"939":1961,"940":1969,"941":1970,"942":1971,"943":1972,"944":1978,"945":2017,"946":2018,"947":2019,"948":2020,"949":2021,"950":2022,"951":2023,"952":2024,"953":2025,"954":2026,"955":2027,"956":2028,"957":2029,"958":2030,"959":2031,"960":2032,"961":2033,"962":2035,"963":2034,"964":2036,"965":2037,"966":2038,"967":2039,"968":2040,"969":2041,"970":1973,"971":1977,"972":1975,"973":1976,"974":1979,"1025":1715,"1026":1713,"1027":1714,"1028":1716,"1029":1717,"1030":1718,"1031":1719,"1032":1720,"1033":1721,"1034":1722,"1035":1723,"1036":1724,"1038":1726,"1039":1727,"1040":1761,"1041":1762,"1042":1783,"1043":1767,"1044":1764,"1045":1765,"1046":1782,"1047":1786,"1048":1769,"1049":1770,"1050":1771,"1051":1772,"1052":1773,"1053":1774,"1054":1775,"1055":1776,"1056":1778,"1057":1779,"1058":1780,"1059":1781,"1060":1766,"1061":1768,"1062":1763,"1063":1790,"1064":1787,"1065":1789,"1066":1791,"1067":1785,"1068":1784,"1069":1788,"1070":1760,"1071":1777,"1072":1729,"1073":1730,"1074":1751,"1075":1735,"1076":1732,"1077":1733,"1078":1750,"1079":1754,"1080":1737,"1081":1738,"1082":1739,"1083":1740,"1084":1741,"1085":1742,"1086":1743,"1087":1744,"1088":1746,"1089":1747,"1090":1748,"1091":1749,"1092":1734,"1093":1736,"1094":1731,"1095":1758,"1096":1755,"1097":1757,"1098":1759,"1099":1753,"1100":1752,"1101":1756,"1102":1728,"1103":1745,"1105":1699,"1106":1697,"1107":1698,"1108":1700,"1109":1701,"1110":1702,"1111":1703,"1112":1704,"1113":1705,"1114":1706,"1115":1707,"1116":1708,"1118":1710,"1119":1711,"1168":1725,"1169":1709,"1170":16778386,"1171":16778387,"1174":16778390,"1175":16778391,"1178":16778394,"1179":16778395,"1180":16778396,"1181":16778397,"1186":16778402,"1187":16778403,"1198":16778414,"1199":16778415,"1200":16778416,"1201":16778417,"1202":16778418,"1203":16778419,"1206":16778422,"1207":16778423,"1208":16778424,"1209":16778425,"1210":16778426,"1211":16778427,"1240":16778456,"1241":16778457,"1250":16778466,"1251":16778467,"1256":16778472,"1257":16778473,"1262":16778478,"1263":16778479,"1329":16778545,"1330":16778546,"1331":16778547,"1332":16778548,"1333":16778549,"1334":16778550,"1335":16778551,"1336":16778552,"1337":16778553,"1338":16778554,"1339":16778555,"1340":16778556,"1341":16778557,"1342":16778558,"1343":16778559,"1344":16778560,"1345":16778561,"1346":16778562,"1347":16778563,"1348":16778564,"1349":16778565,"1350":16778566,"1351":16778567,"1352":16778568,"1353":16778569,"1354":16778570,"1355":16778571,"1356":16778572,"1357":16778573,"1358":16778574,"1359":16778575,"1360":16778576,"1361":16778577,"1362":16778578,"1363":16778579,"1364":16778580,"1365":16778581,"1366":16778582,"1370":16778586,"1371":16778587,"1372":16778588,"1373":16778589,"1374":16778590,"1377":16778593,"1378":16778594,"1379":16778595,"1380":16778596,"1381":16778597,"1382":16778598,"1383":16778599,"1384":16778600,"1385":16778601,"1386":16778602,"1387":16778603,"1388":16778604,"1389":16778605,"1390":16778606,"1391":16778607,"1392":16778608,"1393":16778609,"1394":16778610,"1395":16778611,"1396":16778612,"1397":16778613,"1398":16778614,"1399":16778615,"1400":16778616,"1401":16778617,"1402":16778618,"1403":16778619,"1404":16778620,"1405":16778621,"1406":16778622,"1407":16778623,"1408":16778624,"1409":16778625,"1410":16778626,"1411":16778627,"1412":16778628,"1413":16778629,"1414":16778630,"1415":16778631,"1417":16778633,"1418":16778634,"1488":3296,"1489":3297,"1490":3298,"1491":3299,"1492":3300,"1493":3301,"1494":3302,"1495":3303,"1496":3304,"1497":3305,"1498":3306,"1499":3307,"1500":3308,"1501":3309,"1502":3310,"1503":3311,"1504":3312,"1505":3313,"1506":3314,"1507":3315,"1508":3316,"1509":3317,"1510":3318,"1511":3319,"1512":3320,"1513":3321,"1514":3322,"1548":1452,"1563":1467,"1567":1471,"1569":1473,"1570":1474,"1571":1475,"1572":1476,"1573":1477,"1574":1478,"1575":1479,"1576":1480,"1577":1481,"1578":1482,"1579":1483,"1580":1484,"1581":1485,"1582":1486,"1583":1487,"1584":1488,"1585":1489,"1586":1490,"1587":1491,"1588":1492,"1589":1493,"1590":1494,"1591":1495,"1592":1496,"1593":1497,"1594":1498,"1600":1504,"1601":1505,"1602":1506,"1603":1507,"1604":1508,"1605":1509,"1606":1510,"1607":1511,"1608":1512,"1609":1513,"1610":1514,"1611":1515,"1612":1516,"1613":1517,"1614":1518,"1615":1519,"1616":1520,"1617":1521,"1618":1522,"1619":16778835,"1620":16778836,"1621":16778837,"1632":16778848,"1633":16778849,"1634":16778850,"1635":16778851,"1636":16778852,"1637":16778853,"1638":16778854,"1639":16778855,"1640":16778856,"1641":16778857,"1642":16778858,"1648":16778864,"1657":16778873,"1662":16778878,"1670":16778886,"1672":16778888,"1681":16778897,"1688":16778904,"1700":16778916,"1705":16778921,"1711":16778927,"1722":16778938,"1726":16778942,"1729":16778945,"1740":16778956,"1746":16778962,"1748":16778964,"1776":16778992,"1777":16778993,"1778":16778994,"1779":16778995,"1780":16778996,"1781":16778997,"1782":16778998,"1783":16778999,"1784":16779000,"1785":16779001,"3458":16780674,"3459":16780675,"3461":16780677,"3462":16780678,"3463":16780679,"3464":16780680,"3465":16780681,"3466":16780682,"3467":16780683,"3468":16780684,"3469":16780685,"3470":16780686,"3471":16780687,"3472":16780688,"3473":16780689,"3474":16780690,"3475":16780691,"3476":16780692,"3477":16780693,"3478":16780694,"3482":16780698,"3483":16780699,"3484":16780700,"3485":16780701,"3486":16780702,"3487":16780703,"3488":16780704,"3489":16780705,"3490":16780706,"3491":16780707,"3492":16780708,"3493":16780709,"3494":16780710,"3495":16780711,"3496":16780712,"3497":16780713,"3498":16780714,"3499":16780715,"3500":16780716,"3501":16780717,"3502":16780718,"3503":16780719,"3504":16780720,"3505":16780721,"3507":16780723,"3508":16780724,"3509":16780725,"3510":16780726,"3511":16780727,"3512":16780728,"3513":16780729,"3514":16780730,"3515":16780731,"3517":16780733,"3520":16780736,"3521":16780737,"3522":16780738,"3523":16780739,"3524":16780740,"3525":16780741,"3526":16780742,"3530":16780746,"3535":16780751,"3536":16780752,"3537":16780753,"3538":16780754,"3539":16780755,"3540":16780756,"3542":16780758,"3544":16780760,"3545":16780761,"3546":16780762,"3547":16780763,"3548":16780764,"3549":16780765,"3550":16780766,"3551":16780767,"3570":16780786,"3571":16780787,"3572":16780788,"3585":3489,"3586":3490,"3587":3491,"3588":3492,"3589":3493,"3590":3494,"3591":3495,"3592":3496,"3593":3497,"3594":3498,"3595":3499,"3596":3500,"3597":3501,"3598":3502,"3599":3503,"3600":3504,"3601":3505,"3602":3506,"3603":3507,"3604":3508,"3605":3509,"3606":3510,"3607":3511,"3608":3512,"3609":3513,"3610":3514,"3611":3515,"3612":3516,"3613":3517,"3614":3518,"3615":3519,"3616":3520,"3617":3521,"3618":3522,"3619":3523,"3620":3524,"3621":3525,"3622":3526,"3623":3527,"3624":3528,"3625":3529,"3626":3530,"3627":3531,"3628":3532,"3629":3533,"3630":3534,"3631":3535,"3632":3536,"3633":3537,"3634":3538,"3635":3539,"3636":3540,"3637":3541,"3638":3542,"3639":3543,"3640":3544,"3641":3545,"3642":3546,"3647":3551,"3648":3552,"3649":3553,"3650":3554,"3651":3555,"3652":3556,"3653":3557,"3654":3558,"3655":3559,"3656":3560,"3657":3561,"3658":3562,"3659":3563,"3660":3564,"3661":3565,"3664":3568,"3665":3569,"3666":3570,"3667":3571,"3668":3572,"3669":3573,"3670":3574,"3671":3575,"3672":3576,"3673":3577,"4304":16781520,"4305":16781521,"4306":16781522,"4307":16781523,"4308":16781524,"4309":16781525,"4310":16781526,"4311":16781527,"4312":16781528,"4313":16781529,"4314":16781530,"4315":16781531,"4316":16781532,"4317":16781533,"4318":16781534,"4319":16781535,"4320":16781536,"4321":16781537,"4322":16781538,"4323":16781539,"4324":16781540,"4325":16781541,"4326":16781542,"4327":16781543,"4328":16781544,"4329":16781545,"4330":16781546,"4331":16781547,"4332":16781548,"4333":16781549,"4334":16781550,"4335":16781551,"4336":16781552,"4337":16781553,"4338":16781554,"4339":16781555,"4340":16781556,"4341":16781557,"4342":16781558,"7682":16784898,"7683":16784899,"7690":16784906,"7691":16784907,"7710":16784926,"7711":16784927,"7734":16784950,"7735":16784951,"7744":16784960,"7745":16784961,"7766":16784982,"7767":16784983,"7776":16784992,"7777":16784993,"7786":16785002,"7787":16785003,"7808":16785024,"7809":16785025,"7810":16785026,"7811":16785027,"7812":16785028,"7813":16785029,"7818":16785034,"7819":16785035,"7840":16785056,"7841":16785057,"7842":16785058,"7843":16785059,"7844":16785060,"7845":16785061,"7846":16785062,"7847":16785063,"7848":16785064,"7849":16785065,"7850":16785066,"7851":16785067,"7852":16785068,"7853":16785069,"7854":16785070,"7855":16785071,"7856":16785072,"7857":16785073,"7858":16785074,"7859":16785075,"7860":16785076,"7861":16785077,"7862":16785078,"7863":16785079,"7864":16785080,"7865":16785081,"7866":16785082,"7867":16785083,"7868":16785084,"7869":16785085,"7870":16785086,"7871":16785087,"7872":16785088,"7873":16785089,"7874":16785090,"7875":16785091,"7876":16785092,"7877":16785093,"7878":16785094,"7879":16785095,"7880":16785096,"7881":16785097,"7882":16785098,"7883":16785099,"7884":16785100,"7885":16785101,"7886":16785102,"7887":16785103,"7888":16785104,"7889":16785105,"7890":16785106,"7891":16785107,"7892":16785108,"7893":16785109,"7894":16785110,"7895":16785111,"7896":16785112,"7897":16785113,"7898":16785114,"7899":16785115,"7900":16785116,"7901":16785117,"7902":16785118,"7903":16785119,"7904":16785120,"7905":16785121,"7906":16785122,"7907":16785123,"7908":16785124,"7909":16785125,"7910":16785126,"7911":16785127,"7912":16785128,"7913":16785129,"7914":16785130,"7915":16785131,"7916":16785132,"7917":16785133,"7918":16785134,"7919":16785135,"7920":16785136,"7921":16785137,"7922":16785138,"7923":16785139,"7924":16785140,"7925":16785141,"7926":16785142,"7927":16785143,"7928":16785144,"7929":16785145,"8194":2722,"8195":2721,"8196":2723,"8197":2724,"8199":2725,"8200":2726,"8201":2727,"8202":2728,"8210":2747,"8211":2730,"8212":2729,"8213":1967,"8215":3295,"8216":2768,"8217":2769,"8218":2813,"8220":2770,"8221":2771,"8222":2814,"8224":2801,"8225":2802,"8226":2790,"8229":2735,"8230":2734,"8240":2773,"8242":2774,"8243":2775,"8248":2812,"8254":1150,"8304":16785520,"8308":16785524,"8309":16785525,"8310":16785526,"8311":16785527,"8312":16785528,"8313":16785529,"8320":16785536,"8321":16785537,"8322":16785538,"8323":16785539,"8324":16785540,"8325":16785541,"8326":16785542,"8327":16785543,"8328":16785544,"8329":16785545,"8352":16785568,"8353":16785569,"8354":16785570,"8355":16785571,"8356":16785572,"8357":16785573,"8358":16785574,"8359":16785575,"8360":16785576,"8361":3839,"8362":16785578,"8363":16785579,"8364":8364,"8453":2744,"8470":1712,"8471":2811,"8478":2772,"8482":2761,"8531":2736,"8532":2737,"8533":2738,"8534":2739,"8535":2740,"8536":2741,"8537":2742,"8538":2743,"8539":2755,"8540":2756,"8541":2757,"8542":2758,"8592":2299,"8593":2300,"8594":2301,"8595":2302,"8658":2254,"8660":2253,"8706":2287,"8709":16785925,"8711":2245,"8712":16785928,"8713":16785929,"8715":16785931,"8728":3018,"8730":2262,"8731":16785947,"8732":16785948,"8733":2241,"8734":2242,"8743":2270,"8744":2271,"8745":2268,"8746":2269,"8747":2239,"8748":16785964,"8749":16785965,"8756":2240,"8757":16785973,"8764":2248,"8771":2249,"8773":16785992,"8775":16785991,"8800":2237,"8801":2255,"8802":16786018,"8803":16786019,"8804":2236,"8805":2238,"8834":2266,"8835":2267,"8866":3068,"8867":3036,"8868":3010,"8869":3022,"8968":3027,"8970":3012,"8981":2810,"8992":2212,"8993":2213,"9109":3020,"9115":2219,"9117":2220,"9118":2221,"9120":2222,"9121":2215,"9123":2216,"9124":2217,"9126":2218,"9128":2223,"9132":2224,"9143":2209,"9146":2543,"9147":2544,"9148":2546,"9149":2547,"9225":2530,"9226":2533,"9227":2537,"9228":2531,"9229":2532,"9251":2732,"9252":2536,"9472":2211,"9474":2214,"9484":2210,"9488":2539,"9492":2541,"9496":2538,"9500":2548,"9508":2549,"9516":2551,"9524":2550,"9532":2542,"9618":2529,"9642":2791,"9643":2785,"9644":2779,"9645":2786,"9646":2783,"9647":2767,"9650":2792,"9651":2787,"9654":2781,"9655":2765,"9660":2793,"9661":2788,"9664":2780,"9665":2764,"9670":2528,"9675":2766,"9679":2782,"9702":2784,"9734":2789,"9742":2809,"9747":2762,"9756":2794,"9758":2795,"9792":2808,"9794":2807,"9827":2796,"9829":2798,"9830":2797,"9837":2806,"9839":2805,"10003":2803,"10007":2804,"10013":2777,"10016":2800,"10216":2748,"10217":2750,"10240":16787456,"10241":16787457,"10242":16787458,"10243":16787459,"10244":16787460,"10245":16787461,"10246":16787462,"10247":16787463,"10248":16787464,"10249":16787465,"10250":16787466,"10251":16787467,"10252":16787468,"10253":16787469,"10254":16787470,"10255":16787471,"10256":16787472,"10257":16787473,"10258":16787474,"10259":16787475,"10260":16787476,"10261":16787477,"10262":16787478,"10263":16787479,"10264":16787480,"10265":16787481,"10266":16787482,"10267":16787483,"10268":16787484,"10269":16787485,"10270":16787486,"10271":16787487,"10272":16787488,"10273":16787489,"10274":16787490,"10275":16787491,"10276":16787492,"10277":16787493,"10278":16787494,"10279":16787495,"10280":16787496,"10281":16787497,"10282":16787498,"10283":16787499,"10284":16787500,"10285":16787501,"10286":16787502,"10287":16787503,"10288":16787504,"10289":16787505,"10290":16787506,"10291":16787507,"10292":16787508,"10293":16787509,"10294":16787510,"10295":16787511,"10296":16787512,"10297":16787513,"10298":16787514,"10299":16787515,"10300":16787516,"10301":16787517,"10302":16787518,"10303":16787519,"10304":16787520,"10305":16787521,"10306":16787522,"10307":16787523,"10308":16787524,"10309":16787525,"10310":16787526,"10311":16787527,"10312":16787528,"10313":16787529,"10314":16787530,"10315":16787531,"10316":16787532,"10317":16787533,"10318":16787534,"10319":16787535,"10320":16787536,"10321":16787537,"10322":16787538,"10323":16787539,"10324":16787540,"10325":16787541,"10326":16787542,"10327":16787543,"10328":16787544,"10329":16787545,"10330":16787546,"10331":16787547,"10332":16787548,"10333":16787549,"10334":16787550,"10335":16787551,"10336":16787552,"10337":16787553,"10338":16787554,"10339":16787555,"10340":16787556,"10341":16787557,"10342":16787558,"10343":16787559,"10344":16787560,"10345":16787561,"10346":16787562,"10347":16787563,"10348":16787564,"10349":16787565,"10350":16787566,"10351":16787567,"10352":16787568,"10353":16787569,"10354":16787570,"10355":16787571,"10356":16787572,"10357":16787573,"10358":16787574,"10359":16787575,"10360":16787576,"10361":16787577,"10362":16787578,"10363":16787579,"10364":16787580,"10365":16787581,"10366":16787582,"10367":16787583,"10368":16787584,"10369":16787585,"10370":16787586,"10371":16787587,"10372":16787588,"10373":16787589,"10374":16787590,"10375":16787591,"10376":16787592,"10377":16787593,"10378":16787594,"10379":16787595,"10380":16787596,"10381":16787597,"10382":16787598,"10383":16787599,"10384":16787600,"10385":16787601,"10386":16787602,"10387":16787603,"10388":16787604,"10389":16787605,"10390":16787606,"10391":16787607,"10392":16787608,"10393":16787609,"10394":16787610,"10395":16787611,"10396":16787612,"10397":16787613,"10398":16787614,"10399":16787615,"10400":16787616,"10401":16787617,"10402":16787618,"10403":16787619,"10404":16787620,"10405":16787621,"10406":16787622,"10407":16787623,"10408":16787624,"10409":16787625,"10410":16787626,"10411":16787627,"10412":16787628,"10413":16787629,"10414":16787630,"10415":16787631,"10416":16787632,"10417":16787633,"10418":16787634,"10419":16787635,"10420":16787636,"10421":16787637,"10422":16787638,"10423":16787639,"10424":16787640,"10425":16787641,"10426":16787642,"10427":16787643,"10428":16787644,"10429":16787645,"10430":16787646,"10431":16787647,"10432":16787648,"10433":16787649,"10434":16787650,"10435":16787651,"10436":16787652,"10437":16787653,"10438":16787654,"10439":16787655,"10440":16787656,"10441":16787657,"10442":16787658,"10443":16787659,"10444":16787660,"10445":16787661,"10446":16787662,"10447":16787663,"10448":16787664,"10449":16787665,"10450":16787666,"10451":16787667,"10452":16787668,"10453":16787669,"10454":16787670,"10455":16787671,"10456":16787672,"10457":16787673,"10458":16787674,"10459":16787675,"10460":16787676,"10461":16787677,"10462":16787678,"10463":16787679,"10464":16787680,"10465":16787681,"10466":16787682,"10467":16787683,"10468":16787684,"10469":16787685,"10470":16787686,"10471":16787687,"10472":16787688,"10473":16787689,"10474":16787690,"10475":16787691,"10476":16787692,"10477":16787693,"10478":16787694,"10479":16787695,"10480":16787696,"10481":16787697,"10482":16787698,"10483":16787699,"10484":16787700,"10485":16787701,"10486":16787702,"10487":16787703,"10488":16787704,"10489":16787705,"10490":16787706,"10491":16787707,"10492":16787708,"10493":16787709,"10494":16787710,"10495":16787711,"12289":1188,"12290":1185,"12300":1186,"12301":1187,"12443":1246,"12444":1247,"12449":1191,"12450":1201,"12451":1192,"12452":1202,"12453":1193,"12454":1203,"12455":1194,"12456":1204,"12457":1195,"12458":1205,"12459":1206,"12461":1207,"12463":1208,"12465":1209,"12467":1210,"12469":1211,"12471":1212,"12473":1213,"12475":1214,"12477":1215,"12479":1216,"12481":1217,"12483":1199,"12484":1218,"12486":1219,"12488":1220,"12490":1221,"12491":1222,"12492":1223,"12493":1224,"12494":1225,"12495":1226,"12498":1227,"12501":1228,"12504":1229,"12507":1230,"12510":1231,"12511":1232,"12512":1233,"12513":1234,"12514":1235,"12515":1196,"12516":1236,"12517":1197,"12518":1237,"12519":1198,"12520":1238,"12521":1239,"12522":1240,"12523":1241,"12524":1242,"12525":1243,"12527":1244,"12530":1190,"12531":1245,"12539":1189,"12540":1200}; + + function lookup(k) { return k ? {keysym: k, keyname: keynames ? keynames[k] : k} : undefined; } + return { + fromUnicode : function(u) { return lookup(codepoints[u]); }, + lookup : lookup + }; +})(); diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/logo.js b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/logo.js new file mode 100644 index 0000000000000000000000000000000000000000..befa598c16c6073a89cf5d72b6f0f7ca92a5eca5 --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/logo.js @@ -0,0 +1 @@ +noVNC_logo = {"width": 640, "height": 435, "data": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAoAAAAGzCAYAAAC/y6a9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAStAAAErQBBHTWggAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHic7N13fBvlwQfw3522ZMm2vPdIGCFkA4GyoYyGsCmjk+7dQksHL2/H2/dtC4W2tLTlfelu2VA2lEILFCgQIHEGJCQkdjzkLdmWZGvfvX8oOkmJEy/pNO73/Xz44DtLzz2RT7qfnnXC8uXLZUxDlqfdnUYQhIP+bjbPn+5xhypzrmUf6rGzOc5cjzVduXN9/nTPyfRrMt/jzOcY05U5n3L2f95s/34LPW4m/p6FbLp/73xe+5nKnWuZs/07ZOOcnusx5nucbJU727LneuxslDmdTBxn/2NmusyEuZS7kHMxG/XP5Gf3TOVmQzY+u/PhPMnkMcSsH5WIiIiI8goDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMADSrMmynOsqEBERUQboc10Bym+SJMHv92NiYgJ+vx+CIECv18NgMCj/N5lMcDgcua4qERERzRIDIE0rEfp8Ph8kSVL2y7KMcDiMcDic9niPx4O6ujqYTCa1q1qU/H4/fD4fBEFQ9iV+NplMKC0tTfsdERHRXDAA0gFcLhcmJibS9pkrAJ1ZgBxD/D9JhhQBwt747wOBALq6uuB0OlFZWQlR5OiCuZJlGV6vF263+4CAvT+3242KigoGQSIimhcGQErT19cHr9erbFuqBSy6SETN8QKmyxnurTJ23iNhalCGLMtwu93wer2oq6uDzWZTseaFS5IkjI+Pw+PxIBqNHvB7vQWwVArwu2TI+xpjI5EIBgcH4Xa74XQ6UVZWxiBIRESzJixfvnzakf2zGfB/qAvObCcM7P+42VzE5jIZ4WCPne3Fcq4TH/Yvdz4TJ7L9mkz3HFmW0d/fr4Q/UxnQdoGIhlNECLoZyokBPc9K6HpMQjSQrHN9fT1KS0szUtf9nzefsDOf42bi7zmT3t5eTE5OKtuliwWULRbgaBVgbwWs1QIgABE/MNIhYXijDM/2eAtsgtlsRktLS8ZD4HT/3kwcYz7n+KGefzAzlZuJv2e23p/ZKne2Zc/12NkoczrZOMez9eVpLuUu5FzMRv2nK1Ot90smZOOzOx/Ok0wegwEwg8eartxCCID7h7+K5QJWfEEH0Ti38sJe4J0/xzC8MVmXhoaGA0IgA2DS0NAQxsbG9h0MWHypiNbzZu4+j4WAgX9L2HmXpLQKOhwO1NfXZ7R+DIALP8Z8j8MAOD0GwIUdZyFl5nOImuk4+Vz3XAVAdgETxsbGlPCntwBHXT338AcARgew7As6bP9dDAP/jr/ZXC4XAEzbEqh14+PjSvjTGYGln9ahes3sPgh0JqDxDBE6k4C3fxcDZMDr9cJisaC8vDyb1Z6TcDiMQCCAQCCAYDDIpYRySKfTQa/XQ6/Xo7S0lBO2iDSOAZDSJnwcdrkI0wLygyAAR31CB0GU0P9SvGnK5XJBlmWUlZUttKpFY2pqCkNDQwDi3e0rvqKDo3Xu3wLrThQQDYrYeWf8tR4eHobZbIbFYslofefC6/XC6/UiEAggFovlrB50cB6PByUlJXA6nbBarbmuDhHlAAOgxoXDYQSDQQBA+RECGk5d+OxdQQCO+lh87KDrhXgw6e/vhyzLedU6lSuSJCmhGABWf10HW/38uwCazhQRCwG7H5AgyzJcLhfa2tqg080weDPDZFnG0NAQxsfHD/idIALWOgGiulWiBBmYGpERCyZ3+f1++P1+mM1mVFRUwG63565+RKQ6BkCNS7T+CSKw5GMikKmhCAKw5CMiBBHoey4eAgcGBgBA8y2BU1NTSstY6WJhQeEvoXWdCPdbMsZ2yIhGo/D7/ap2u0ejUbhcLgQCAWWfrUFAxVECyo8SUH6EAH3uGiUJ8cla47tluLfKcG+T4euNfwEJBoNwuVwoLy9HTU1NjmtJRGphANS4RAC01gqw1mR4IKoAHPnheEtg77PJEChJEpxOZ2aPVUCmpqaUn+tPzNxrXrFUwNiO+EU9EAioFgCnpqbQ39+vLGFjbxGw4ss6mLX7J85Lgi7eyl9+hIDF7wdC48DuB5PjdcfGxmAymTT/BY1IKxgANSwQCCASia8lYsvs5NE0R3wg3hLY8/d4CEyMfdNqCEwEQNEA1ByXuQWzy49MhsnUlrhsCgaD6O3tVbqzyw4TsPJaHVv7CoCpDFi633jdoaEhGI1Gjgsk0gDerkHDUu82UdKQ3Wnoh18pomVd8nQbGhqC2+3O6jHzUSwWU8ZcVq0SoM/gddbRJkBnjv8cCoVUmYDhdruV8FdxtIBV1zH8FZR943XrT46/NxNjSGe6Ew0RFT4GQA1LDQhGFXoLD3u/iLb1yVNueHgYo6Oj2T9wHkltmXMuzWzoFkSg/PBkmYmgmS2hUAg+nw8AULVSwIqv6KCbx/JBlGP7hcBYLIa+vr4cV4qIso0BkABg2tu8ZcOiS0W0X5g87UZGRjQVAlNDmd6U+Rfd3qJeN3Dq323RJSJEDigpXAKw5GpRab0Nh8MIhUK5rRMRZRUDIKmu/SIRiy5JD4EjIyM5rFGOZCF0p962L5uLLqe2/lmqBJQ08T7EhU4Q01ulU29PSETFhwGQcqLtfBGLL0uefqOjoxgeHs5hjWguPB6P8vNs715C+a9yRfJvmTpbnYiKDwMg5UzreSIOuyJ5CrrdbobAApHaPVi1mgGwWFQcnVwLVK2Z5ESUGwyAlFMt54o4/Kr0EJhYJobyV2L5IKM9vpg1FQdTGWBvjv89U2esE1HxYQCknGs+W8QRH0qeih6PhyEwj8myrMwgN1cJqk0gInUkAiCQ/ZnkRJQ7DICUF5rOFHHkR5LdTx6PB4ODg7mtFE1LkiTlZ4a/4pM6mzv1b01ExYUBkPJG4+killydDIFjY2PK/YOJSB2iIflzNmeSE1FuMQBSXmk4RcRRHxeVlqXx8XGGQCIVMQASaQMDIOWd+pNEHPVJHYR9Z+f4+Dj6+/tzWykijUjtAmYAJCpeDICUl+reI2Dpp5MhcGJigiGQSAVsASTSBgZAylu1awUc/dlkCPR6vXC5XLwoEWURAyCRNjAAUl6rOVbAss/rlFuc+Xw+9Pf388JElCXsAibSBgZAynvVawQs/4JOuTD5fD62BBJlCZeBIdIGBkAqCFWrBCz/YnoI7OvrYwgkyjDBkFzcke8vouLFAEgFo3KFgBVf1iljlPx+P0MgUYaxC5hIG/QzP4Qof1QsE7DyKzps/kUMUjgeAnt7e9HU1ASBt6UoCtEA8MLno7muRkFoO1/Eoksy+z2ek0CItIEtgFRwnEsFrLxGB50xvj05OYne3l6OVyLNkbKQk9kCSKQNDIBUkJxLBKz8qg46U3ybIZC0SIpkvky2ABJpA7uAqWCVHyFg1dd06PhpDLEgMDU1pXQHiyK/2xQDQRDQ0tKS62rklXA4rCyKzhZAIpovBkAqaGWHCVi9LwRGA/EQ2NPTg+bmZobAImE2mw/YN9tgMtO40EwEnNmMPZ3PcQ5Wbup+KZL5gMYWQCJt4BWSCl7pYgGrr9NBb41vBwIB9PT0sDuYilJaAMxyCyDfQ0TFiwGQioKjXcDqr+tgsMW3A4EAuru7EYvFclsxogxLbwHMfPlsASTSBgZAKhqOVgGrv6GDoSS+HQwG0dPTwxBIRSX7LYBcCJpICxgAKWv8fTJklXuQ7M0C1nxDB6M9vs0QSMUmNQDKbAEkonliAKSs8eyQsfVXMcgqZ6+SJgFrvqmD0RHfDgaD7A6mopH1FkAGQCJNYACkrBrZJGPrL2NZuVAdiq1hXwgsjW+HQiH09PQgGuUdJqiwpc5uz8oYQC4DQ6QJDICUdSObZWy9LQchsF7AMd/SwVQW32YIpGKTjQAo6ACkrEDDEEhUnBgASRWjW2Vs+XksKxesQ7HWClhzvQ6m8vh2OBxmCKSCl2gFzNaXKrYCEhU/BkBSjfstGZtvjSEWVve41moBx1yvg7kivh0Oh9Hd3Y1IROU0SpQhiXGAUjQ74YzjAImKHwMgqcqzXcbmn8UQC6l7XEuVgGOu18NSFb9wRiIR9PT0MARSQVICYJZOXy4GTVT8GABJFTqdTvl57B05fv9elUOguQJY8y0dLNUMgVTYsh4A2QJIVPQYAEkVDocD1dXVyvb4Lhmbbonfv1dNZidwzLd0sNYkQ2B3dzfCYZX7pYkWINkFnJ3yuRg0UfFjACTVVFZWpoXAid0yOnIQAk3l8ZZAa238IheNRtHT08MQSAUj6wGQLYBERY8BkFR1QAjslLHp5hgik+rWw1QGHHO9DrZ6hkAqPMpi0DKystA6ZwETFT8GQFJdRUUFampqlG1vV25CoNEBrPmmDiUNyRDY3d2NUEjlwYlEc5R2NxDeDo6I5oEBkHLC6XSitrZW2fZ1y9h0UwwRv7r1MDri3cH2pvgFNRaLoaenhyGQ8lra3UCycTs4tgASFT0GQMqZ8vLy9BDYK2PjTTGEverWw1ACrP6mDvaW9BAYDAbVrQjRLGW7BVBIaQHkMjBExYkBkHKqvLwcdXV1yra/L0ch0Aas/roOjtZkCOzt7WUIpLyUFgCzsBi0jl3AREWPAZByrqysLC0ETvbL2HhjDKFxdethsAGrv6FDaXt6S2AgoPI0ZaIZZL0FkF3AREWPAZDyQllZGerr65XtyYF9IXBM3XroLcCq63QoXcwQSPkr65NAGACJih4DIOWN0tLStBA4NSTjzRtjCHrUrYfeAqy+Toeyw/ettSZJ6OnpwdTUlLoVITqI9C7gzJcvGrgQNFGxYwCkvFJaWoqGhgblAhcYjrcEBt3q1kNnAlZ9VYfyI5MhsLe3lyGQ8kL2A2DyZwZAouLEAEh5x+FwoL6+PhkCR2S8+aMoAiPqXoh0JmDltTo4j0qGwL6+PoZAyjl2ARPRQjEAUl5yOBxpLYFBN/Dmj2KYGlY5BBqBldfoUHF0ekvg5KTKq1YTpVAzAHIZGKLixABIectut6eFwNAYsPFHMUwNqhsCRQOw4is6VC6P10OWZfT19TEEUs5kfSFodgETFT0GQMprdrsdjY2NyRA4Drx5YwyT/SqHQD2w/Es6VK1MD4F+v8q3LiGCCmMA2QVMVPQYACnvlZSUoKmpSbnohSeAjTfFMOnKQQj8og5Vq5Mh0OVyMQSS6tK7gDP/PmALIFHxYwCkgmCz2dJaAsPeeAj096p7cRJ0wPIv6FB9THoI9Pl8qtaDtI2TQIhooRgAqWDYbDY0NTUp45/CPmDjj2Pw9agcAkVg2ed0qDkuGQL7+/sZAkk1qQFQ5jqARDQPDIBUUKxWKxobG5UQGPEDm34cg3ev+iHw6M/oUHtCekug16vyTYxJkzgGkIgWigGQCo7Vak1rCYxMAptujsHbqX4IXPopHepOTF6M+/v7GQIp67LeBcwxgERFjwGQCpLVakVzc7MSAqNTwKZbYpjYrXIIFICjPqFD/cnJt1J/fz8mJiZUrQdpS9oyMBwDSETzwABIBctisaSHwACw6ScxjL+bgxD4MRENpyXfTgMDAxgfH1e1HqQd2e4CFlJaALkQNFFxYgCkgmaxWNDS0gKdTgcAiAWBjp/EMLZT5VYLAVjyERGNZyTfUoODgwyBlBWcBUxEC8UASAXPbDajubk5GQJDwOafxjC2Q/0QeOSHRTSdlR4Cx8bG1K0HFT2OASSihWIApKJwQAgMAx23xuB5W/2L1xEfENF8TvKtNTQ0xBBIGZXeBcyFoIlo7hgAqWiYzWa0tLRAr4/3X0lhYPPPY3BvU/8CdviVIlrWpYdAj8ejej2oOHEZGCJaKAZAKiomkwnNzc3JEBgBtvwihtEt6l/EDnu/iLb1ybfY8PAwl4ihjMj+GEAuBE1U7BgAqeiYTKb0lsAosPWXMYx0qH8hW3SpiPYLk2+zcDiseh2o+GS9BZBdwERFjwGQipLRaDwwBP4qhuGN6l/M2i8SsegSvtUoc9ScBMJlYIiKE69KVLQSIdBgiF/N5Biw7dcxDL2hfghsO1/E4sv4dqPMSFsImmMAiWgeeEWionZACJSAt/43hsEN6l/UWs8TcdgVfMvRwqW2AMpZXgcQYAgkKka8GlHRMxgMB4TAt++IYeAV9S9qLeeKOPwqvu1o4RIhMBstgBAAQZfcZAAkKj76mR9CVPgMBgOam5vR09ODSCQCWQK2/zYGWRJRf5K6gaz5bDHt4ko0H4IgQJblrIwBBOKtgLFY/GcGQKLiw6YI0oxES6DRaAQAyDKw/fcSXC+qP8i96UwRVauEmR9IdBDJFsDshDPOBCYqbgyApCl6vR7Nzc1KCIQM7PijhL7n1Q+BqRdYorlSAmC2WgAZAImKGgMgaU4iBJpMpvgOGXjnLxJ6/8nlLqhwZHUMILgYNFGxYwAkTZouBO68U0LPMwyBVBjUbAHkWoBExYcBkDRLp9Olh0AAu+6R0P00L3aU/xIBUJbi/2Uau4CJihsDIGlaIgSazWZl37v3Sdj7JEMg5bes3w6Oi0ETFTUGQNI8nU6HpqamtBC4+0EJXY8zBFL+SrsbSJYXg2YAJCo+DIBEmD4E7nlIQucjDIGUn9S8HzADIFHxYQAk2ifRHWyxWJR9nY9K2PNXhkDKP+ldwJkPaAyARMWNAZAohSiKaGpqSguBXU9IePcBhkDKL2n3A+YYQCKaIwZAov0kQqDValX2dT8lYde9DIGUP7LeBZwSALkMDFHxYQAkmoYoimhsbEwLgT1/l7Dzbl4IKT9kOwAKBi4ETVTMGACJDkIURTQ3N8Nmsyn7ep+V8M5fJIDXQ8oxLgNDRAvBAEh0CIIgoKmpKS0E9j0nYcefGQIpt7IeADkJhKioMQASzSARAktKSpR9rhckbP+DBF4XKVe4DiARLQQDINEsCIKAxsbGtBDY/5KE7b+LMQRSTnAdQCJaCP3MDyEiIH7BbWhoQH9/P3w+HwBg4N8yZCmGpZ/UQeDXqayIRA5MN7MNJKkhaTqZCDYzHWO+x5mp3NSZuRwDSERzxQBINAeCIKC+vj4tBA6+KkOOxXD0ZxgCM02WZezZsyfX1ch78YWgZw6ic8EWQKLixssV0RwlQqDD4VD2Db0uY9vtMchcJYZygF3ARDRXbAEkmodECAQAr9cLABh+U8bWX8Ww/PM6CLpc1q6wiQag/SJ+N50Le0tmW/8ALgRNVOwYAIkWoL6+HoIgYGJiAgAwsknG1l/GsOwLurQLKM2eqAfaL2QAzDVRz4WgiYoZP2WJFqiurg6lpaXK9shmGVtvi2VlYD6RWtgFTFTcGACJMqCurg5lZWXK9uhWGVt+HsvK2CwiNTAAEhU3BkCiDKmtrU0Lge63ZGy+NYZYOIeVIponLgNDVNwYAIkyqLa2FuXl5cq2Z7uMzT+LIRbKYaWI5kFgACQqagyAGpZ6K6mgJ/Ply7Hkz7NZLLdY1NTUwOl0Kttj78jo+Kk6ITD1NSdaCHYBExU3BkANs1gsys+eHZn/gA+MJH82m80ZLz+fVVdXp4XA8V0yNt0SQzSQ3eNODSf/jloK3ZR5qQGQy8AQFR8GQA0zmUzQ6eIL1nm75Iy2UElRYGhD8qJhtVozV3iBqK6uRkVFhbI9sVtGR5ZD4NRA8meDwXDwBxLNgGMAiYobA6DGJVoB5Rgw/m7mPuRHOmREJuM/GwwGGI3GjJVdSKqqqtJDYKeMTT+OKa9NJskyMDUY/xsKgoCSkpLMH4Q0gwGQqLgxAGpctrqB+19Otv7ZbLaMlVuIqqqqUFlZqWx798rYdHPmQ2Dfc5Iy49hmsymtu0TzIRq4EDRRMWMA1LjUrtnBVyWExhZeZmgM8LyVvGBosft3f5WVlaiqqlK2fd0yNt0UQ8SfmfKDbmD3A8nQnbowdaalTh4KjTEYFK2UP23q35yIigPf1RpnsViUCRqhMWDjj2MIe+dfXiwE7PhjDPK+LCKKIrsi96moqEgPgb0yNt64sNc7YfsfkrOMs/2aC4KgnDNBT/rEEyoenneSf1e9nvc1JCo2DIAaJwgCGhsble7CqcH4bNX5dE+GxoA3fxjD6NbkhaOxsZEXjxQVFRWorq5Wtv0uGRtvWlgI7H9Jguft5GvucDiyPgM4dejAWBZmkFPujWxKtijzPUxUfBgACQaDAfX19cq2v3ffunXB2Zfh65bx+vej8PUkw0BNTQ1b/6bhdDrTQuBkv4wN34ti8FU5rdttJrIMDLwqY9c9yQu10WhMG2+YLdleQohyS4oA7m3Jv6tWJ3ERFTNdTU3N9+b75Gy0Mqi1dlm2jlOor4nRaIQoipicjDf9hcaAgX9LCIzE1wMzOwUI+31dkCVg0iVj6HUZb90hpbUalpWVobq6uqBeZzWPY7FYoNPplNc7FgSGN8pwvy3D3iTAVH7o4450yNj2KwmuFyRI0fg+o9GI5ubmrLfWCIIAnU4Hjye+enjEB7S8j98li8noNhkD/07OKK+vr59xHGAhvyf5OaVOmdksV43jFPJrMt0x2K5PisrKSgQCAfh8PgBAaDw+s7TvOUBvBapWCCg9TMDUYHzdQF+3PO19bq1WK2pra1WufeEpLy+HKIoYGhpSFtqd2C3j9f+Oof5EETVrBYg6QEj5L+wFuh6TMLEnvdVNrfCXoNfrYTAYEIlEEPbGZ33Xn8QQWCxS1/AsLS1lFzBRERKWL18+bf/NbKb9Hyq1znbZgP0fN5skPJclCQ722Nkm7rkuf7B/ufNZPiHbr8lMx/H5fBgZGUEwOIc+YMS7kp1OJ8rKypTWgunqPt8lJVKfN59vTPM5bib+njOJRqMYHh6G1zu/gYDZDH/T/XsTr4nb7cbISPx2L4IILPucDtXH8O4jhW7PwxK6HosHQEEQ0N7ePu2i4vP5nJrJQq878z1mPrTsLOSzRa3Wrkx8/uWqBTCf684WQMobdrsddrsdPp8Po6OjCAQOfesKk8mEiooKVSYfFCO9Xo/6+nqUlpZicHAQkUhkVs8zGo1wOBwoLy/PyZp/FRUVCAaD8Pl8kCXgrf+LYYVRh4rlPAcK1a57JPQ8k976xzvKEBUntgBm8FjTlVuILYD78/v9CAQCkGVZeU7i/zab7ZATPdgCODeyLMPj8SAYDCISiSAajSIajSq/NxgMsNvtcDgcqtxf+VAtgED8HrF79+5FOBwfCyAagVXX6lB+JENgIZFl4J0/SXD9Kxn+DtX6F38OWwBnwhbA2ZWbDWwBnPkYDIAZPNZ05RZDAFzIMRgAMyMajSIWi8FkMql63JkCIACEw2Hs3btXGcco6gHnUgHVawRUrhRhtKtSVZqHoBvw7JAwtEGGO2Xx9kSr9KEWcWcAnBkD4OzKzQYGwJmPwS5gogKg1+vzdiC+0WhEXV0dXC4XAECKAqNbZIxukSEIEsoOF1C1SoClSgDYMJhz0SlgfJcMzw4ZgZEDL4oWiwUNDQ15e74RUWbwHU5EC2a329He3g63242JiQllvywDYztljO3kWoGFoLy8PKvLNxFR/mAAJKKMSLQEVlRUHBAEKX+JogiLxYLS0lI4HI5cV4eIVMIASEQZlRoEJyYmEA6HD5jMQrkjCAIsFovyn9rjSokoPzAAElFWGI1GVFVVTfu7hU4gmO2A7mxMbJrrMeZ7nGyVO9uyiai4cel+IiIiIo1hACQiIiLSGAZAIiIiIo1hACQiIiLSGAZAIiIiIo1hACQiIiLSGAZAIiIiIo1hACQiIiLSGAZAIiIiIo1hACQiIiLSGAZAIiIiIo3hvYBp3iRJQigUUu4rmnp/UaPRyPuN0rQkSYLX64XP50MkEoFer1f+MxgM0Ov1sNlsEEV+PyUiyhYGQJqzWCwGj8eDsbExxGKxaR9jMBhQUVGB8vJyBsEFkGUZExMTymudGrYFQYAoiigvL4fD4chxTQ9NkiT4/X54vV5MTk5ClmXld+Fw+IDH6/V61NTUwG63q1nNopUI3YlzKPU8EkURdru9oAJ3NBpFIBBAIBBAJBJJO5/yyVw++xbyb8jlZ6zZbIbVaoXFYuFnfYERli9fPu1ZN5uT8VB/7NmezPs/bjYn0FzeKAd77GxP1Lm+Kfcvdz5v6my/JvM9Tjgchtvtxvj4+KyPZzAYUFlZibKyMqX8+X7QpT5vPh808zluJv6e8yFJEsbHx+HxeBCNRmd8vNFoREVFBRwOR0Y/hKf79861/HA4jJ6engP+HSUlOsSiMiJRGdHo9K9rSUkJampqYDAYZqzXdGaqayb+ntl6f2ai3FgshrGxMYyPjx/0yxoA6HQ6lJeXw+l0zjoIzudzaq5lppqYmMDU1JQS+ii/CIIAi8UCm80Gm80Gi8Vy0MepVZ9Uar3X86ncmY7BAJjBY01Xnw4y4QAAIABJREFUbrEEwOHhYYyOjqbtq6o0YsXyEqxaYcfKFXZUVBjwzD88ePzJEby7eyrtsXq9HnV1dbDb7QyAMxgdHZ22dbWkRIeVy+1YtdKOvr4QnvmnG5OT6Y9JBO7S0tKM1GWhATAUCqG3tzct/J1+mhPfu6Edq1amt+5FozLe3T2F//jObvzzeY+yXxRFVFZWwul0HrJe09FqAIxEIhgbG8PExAQkSTrg9xaLDtGohEgk/fmJFmWn0wmdTjenY2crAMZiMfT392NqamqaZwBmU+G0XBabmCQfcA4lVFVVobKy8oD9DIDqlTvTMRgAM3is6cothgA4Pj6O/v5+Zfvs91bg1psPR2Oj+aBl73p3Co8/OYI77xnAns4AgPjFpbW1FSaTac513b++xRoAR0ZG4Ha7le2zzqzA+y+pxupVDixeZIEoJusTCkt44V9jePzJEfzt726MjCa7Uqurq9MC03wtJACGQiH09PQoQfbYYxz47g3tOOWk8hmf+9TTo7j+27vRtTeg7HM4HKivrz9ovaajxQAYDAbR09Oj/M5gELDsaDvWrIp/eVi10oEjD7fC74/h6WfcePypETz7Tw8CgeSXCVEU0djYCKvVOutjZyMABoNBuFyutC8QTU1mnHZyOU47pRynnFyOmmrjgo9L8+f1RvHI4yO48+4BvPb6RNrvEu/Z1HODAVC9cmc6BgNgBo81XbmFHgCnpqbQ3d2tPPYzn2zEjf+zGDrd7F4/tyeC913QgXd2TgKIt1C1tbXN2LowU32LMQBOTExgYGBA2f7yF5rx/e+0p4W+g4nFZHzmiztw/4NDAOL1bmpqOuQFfDbmGwBTw58gAL/59VG4/LKaOR07HJbwy9t78T83dildxHa7XQmBs6G1ABiNRtHd3a0EptoaI+79yzKsXnXoMaKBoITnnvfg+m/vxt7ueOjW6/VobW2FXj/9UPFsB8CJiQkMDQ0p+654fw3+4xttaGudvmuRcm/3nincde8g7rpnAIND8S+kZrMZTU1NynnEAKheuTMdgwEwg8eartxCDoCRSASdnZ2IxWLQ6QTc+D+L8ZlPNs75OINDYZyzfpPSmmOz2dDc3Lyg+hZbAAwEAkqrjV4v4Kc/PhxXf3j2QQeId6F++ONv4cm/xbvqdTod2traDnoBn435BEBZlrFnzx4lhFz94Xr84qdHzLsOjz0xgo99+m2lq8lut6Ourm5W54CWAqAsy+jp6UEwGAQArFhWgnvvXI6G+tm3uPf0BnHO+k1w9YcAABaLBc3NzdPWJZsB0OPxYGRkRNn/6U804OYfHQ7OMSgMA4MhnHHORuU80uv1aGlpUXV1CAbAmY/BwRM0LUmS0rrv7vnzsnmFPyDeCvHEQyvR2BC/EE1OTmJ4eDhjdS10kUgELpcLsizD4dDjr/eumHP4AwC9XsAff7sUp58a72KNxWJKuWqamppSwl9drQn/871FCyrvgvVV+Mvvj4bRGP+48vl86O/vz9uZn7kyODiohL/zz6vC359YPafwBwDNTWY89teVqKqMd6sGAgHV36uSJKUNg7jumhbcciPDXyGpqzXhvruWw2aL9/REo9EDxpBT7jEA0rT8fj9Cofi3t+OPK8W5Z1csqLymfReWxIBtt9vNmXz7DA8PK4HpG19tUQLcfJiMIu758zIce0y8yy8QCKj+wTsxkRwH9LObD4fDsfDVptadW4m7/ng0TPtCoN/vz0m4zVdjY2Pwer0AgHPPrsCdfzgaVuvch1kAwGGLrXjkwRUoK9MrZaf+TbNtbGxMmbjyX99ehO/c0K7asSlzlh9dgj/csVQZwuL1evmZn2cYAGlak5OTys8fuLI2I2UuXmTF8WuTs1MPNqtPaxKvtV4v4MrLF/5aW606XP/1NmXb7/cvuMzZSqz3BwAXX1CNdeceOAtwvs45qwL3/GWZ8iVicnKSIXCfRPgDgE9+rGHBrWXLlpbgFz89Utn2eDyHeHTmSJKEsbExAMDSJTZc++W5DxWh/HHu2RX40X8vBhDvglXrPKLZYQCkaSVCidkk4pILqzNW7skpM0ADgcAhHqkNwWBQae04+70VqK7KzIzG9xxfqnSZhkKhQ64Bl0l+v1/591xycebOm4T3nuHEfXcug8XMEJggSZLS9VtTbcSZpy989jcAnHlauTLZKxQKTbucTKalLn+0fl1V1o9H2ffZTzUqvQAzrUdJ6mIApANEo1Hl7gzr11VmpAsv4dSTypSfGQDTW0E/dFVdxsq1WnU4dk1y5mciIGRbakvUkYfbsnKM009z4v67l8NiiXdxTk5Ooq+vT5WAko9S30eXX1Yz6xn6M7Hb9WlrNWb7/SrLstL6BwDrz2MALAaCAKxcHj+PJEliK2AeYQCkA6R2/1568dyW7pjJ6lUOZWBwauuXViVe68oKA845a2HjLPd36inJ1la1utsTQdNoFLGoPXvLdZx6cjn+eu9yZZzb1NQUXC6XJs+n1L/tB67M3JcIAGlrNmY7AEYiEaV1qKnJjBXLSrJ6PFLP6lXJLxIc+pM/GADpAKlv0LnOIpyJXi/g+OOS4wC13Aooy7Ly719ypA0GQ2anOZ52ivrd7YkAVl1thF6f3WmbJ72nDA/fvwIlJckQqMWWwMT7tbHRjKVLMtvqqmaLfeoEgfXvy9zYUcq9/e/6Q/mBAZAOkDpGQ5eFi3h5WbJLeTb3uS1WsVhMCSuJ8XqZlLpgbmJGd7YlxuLNYu3qjDhhbSkeuX8F7Pb4ORUIBDQVAmVZVlpdyzI4VCPhmJRhBGoGwCOyNHyAcmNNykLkaq0DSDNjACTKA9n4TEwts5gnSRx3bCkee3AFSkuTIbC3t1czITBBzMKnuTHlPrvZfj219vfSktSPNwbA/MEASEQFb81qBx7/60pl7bpgMIje3l7OOCTKA08+nVyLlAEwfzAAElFRWLnCjscfWgWn0wCAIZAoXzz+5MjMDyLVMQASUdFYsawETzy8EhUpITD1loZEpC6PJ4JXXkveScZm4/jOfMEASERF5eijSvDkI6uU+9mGQiGGQKIc+dszbsRi8THIBoMBZWVlMzyD1MIASERF56glNjz16CrUVKeHQC3POidS2+BQGD/7RbeyXVFRwTGAeYQBkIiK0hGHW/HUo6tQVxtfy5IhkEg9Pb1BnLN+E3a9G1+nUq/Xs/UvzzAAElHROmxxPAQmFjQPh8MMgURZtuvdKZyzfhO69ibXjmTrX/5hACSiorao3YK/PbYKjY1mAPEQ2N3dnbbwMBFlxqsbJnDu+Zvg6k8uPm+1Wtn6l4cyv3Q8EVGeaW2x4OnHVmHdhR3o6Q0iEomgp6cHTU1NMBgMua4eUcHq7Q3ihZfG8MKLY3jxpTEMDYfTfu90OlFTk9l7ylNmMAASkSY0N5njIfCizdjbHUAkEkFvby9DoEbcc98gfvLz7pkfSLM2NRVDn2v620yKooi6ujo4HI5pf0+5xwBIRJrR2GjG3x5bhfMu6kBnV0BpCWxubmYILHLjE1FlQgJll9FoRGNjI0wmU66rQofAMYBEpCkN9Sb87bFVWLzICgCIRqPo6elBOBye4ZlENB29Xg+73Y6amhq0traivb2d4a8AsAWQiDSnrjYeAtdf3IGdu6YQjUaV7mCj0Zjr6lGW2Ww2lJaWZv04c5n1KsuyKsdZSJn711EQBJjNZraeFygGQCLSpJpqI556dBXWX7wZO96ZZAjUEIPBoIxNk2U5a8uTFHsApMLGLmAi0qyqSiOefGQVjj6qBECyOzgUmn5gOxFRsWAAJCJNq6ww4ImHV2L50fEQGIvF0NvbyxBIREWNAZCINM/pNOCJh1dh5Qo7gGQIDAaDOa4ZEVF2MAASEQEoK9Pj8b+uxJrV8bFhsVgMfX19DIFEVJQYAImI9ikt1eOxB1fg2GOSIZAtgURUjBgAiYhS2O16PPrAShx/XHyZEEmS0Nvbi0AgMMMziYgKBwMgEdF+Skp0ePj+FTjxhPgN7CVJQl9fH0MgERUNBkAiomnYbDo8dN9ynHJSOYBkCJya4u3EiKjwMQASUcHo6VV3LJ7FosMD9yzH6acmQ6DL5WIIJKKCxwBIRAVj/cWb8e5udcOXxSzivruW48zTnQCSLYGTk5Oq1oOIKJMYAImoYAwOhrDuwvj9e9VkNom4985lOPu9FQDit8RyuVwMgURUsBgAiaigDA2Hse7CDux4R93wZTKKuPtPR2PduZUAkiHQ7/erWg8iokxgACSigjMyGsZ5F3Xgre3qhi+jUcRffn80zj+vCgBDIBEVLgZAyqnJyUlIkpTralABGnVHsP7izdiyTd3wZTAI+NNvl+Ki89NDoM/nU7UeREQLwQBIqjOakqfdxMQEOjs74fV6c1gjKlQeTwTnX9KBzVvUDV96vYDf37EUl11cDSAeAvv7+xkCiahgMACS6m695Qhc/402mPcFwUgkApfLhZ6eHoRCoRzXjgqFKMbPn/HxKM6/dDM2blL3S4ReL+A3tx+Fyy+rAZBsCeSXGSIqBAyApDqzScT1X2/Fm6+uxfp1lcr+yclJdHV1YWhoiN3CNKP6+nolBE5MRHHBZVvw+hsTqtZBpxNwx6+W4ANX1ir7+vv7MTGhbj2IiOaKAZByprnJjLv/tAwP378CixdZAcRbUTweD/bs2cOLKB2SxWJBU1MTdDodAMDni+Kiy7fg1Q3qnjeiKODXPz8SH/lgnbJvYGCA5y8R5TUGQMq5M0934rUXj8V/fXsRrNb4xTwajaK/vx/d3d0IBtW9+wMVDrPZnBYC/f4YLr58C15+ZVzVeoiigNt+diQ+9pF6Zd/AwADGx9WtBxHRbDEAUl4wGkVc++VmbHptrTKwHgCmpqbQ1dWFwcFBxGKxHNaQ8pXJZEoLgVNTMVx65Vb866UxVeshCPHxrZ/6eIOyb3BwkCGQiPISAyAdktpD8errTPj9HUvx5COrcNQSm7J/bGwMnZ2dvJjStBIhUK/XAwACgRgu/8BWPP+CR9V6CALwk5sOx2c/1ajsGxwcxNiYumGUiGgmDIB0gMRFFAB27MjNArcnn1iGl587Fjf94DA4HPH6xGIxDA4OYu/evQgEAjmpF+WvA0JgUMIVH9qGfzynbggEgB//8DB88XNNyvbw8DBDIBHlFQZAOoDValV+fvHl3LW46fUCPvfpRnRsWIsPXlkLQYjvDwaD2Lt3LwYGBtgtTGmMRmNaCAyGJFz14W14+hm36nX54fcX45ovNSvbw8PD8HjUD6NERNNhAKQDWCwW5ed/vZz7VouqSiNuv20J/vHUGqxYblf2j4+PY8+ePWxZoTT7h8BQWMKHPvYWnvzbqOp1+f53FuG6a1qU7ZGREYZAIsoLDIB0AIPBAIPBAADo7Q2iuyc/ZuEee4wD/3p2DW695Qg4nfH6JbqFu7q62C1MCqPRiObmZuU8DoclfOQTb+GxJ0ZUr8t3bmjHt65rVbZHRkbgdqvfIklElIoBkKaV1gqo8mzKQxFFAR//aD06XluLT1zdAFGM9wsnuoX7+/sRjUZzXEvKBwaDIS0ERiIyrv7U23jo0WHV6/If32zDDd9qU7ZHR0cxOqp+iyQRUQIDIE0rdRzgD27sQtfe/GpdKy834Gc3H45/PbsGxx1bquyfmJjAnj174PF4IMtyDmtI+WD/EBiNyvjEZ7bjwYeGVK/LN7/Wiu/e0K5su91uhkAiyhkGQJpWWVkZTCYTAGBgMITzL9kMV3/+3ad3xXI7nn1yNf73tiWorjICACRJwtDQELq6ujA5OZnjGlKuGQwGtLS0wGiMnx+xmIxPfX4H7r1/UPW6fO2aFvz3dxcp2263GyMj6ndLExExANK0RFFEU1OTcq/Vnt4gzr9kM0ZGwzmu2YEEAfjAlbXo2LAWn/9ME/T6eLdwKBRCT08PXC4XIpFIjmtJuaTX69Hc3JwWAj/7pXdw5z0DqtflK19sxo/+e7Gy7fF4GAKJSHUMgHRQRqMRDQ3Juxrs3jOFCy/dglF3foYpu12PG/9nMf79/LE45aRyZb/X60VnZyfcbje7hTVs/xAoSTK+8JV38Me/9Ktely98tgk3/+gwZdvj8WB4WP2xiUSkXQyAdEh2ux2VlZXK9lvb/Vhzwgb87o8uSFJ+hqklR9rwxMMr8YffLEVDfbwbW5IkjIyMsFtY4xIhMDG8QZaBr3xtJ373R5fqdfnMJxvx0x8frqxvOTY2xhBIRKphAKQZVVdXw+FwKNtjYxFc+/VdOPWsjXj9jYkc1uzQLr2oGhtfXYuvfqUFRmP8VA+Hw+jt7UVfXx+7hTVquhB47dd34Y7f9qlel09+rAE//8kRaSFwaEj9CSpEpD0MgDQrjY2NqK+vh06nU/Zt2erDWedtwue+tAPDI/k3NhAArFYdvvef7djw0nF47xlOZb/f70dnZydGR0fZLaxBOp0uLQQCwHXXv4tf/1+v6nW5+sP1+NXPj1SWNBofH8fgoPoTVIhIWxgAadZKS0uxaNEilJWVKftkGbjr3kGsPn4Dbr+jD9FofoapRe0WPHTfCtzz52VoaTYDAGRZxujoKDo7O+Hz+XJcQ1KbTqdDU1MTzGazsu9b/7kbv/hVj+p1+dBVdbj9F8kQODExgYEB9SeoEJF2MADSnOh0OtTV1aGtrS1tsWivN4pv3vAuTjrjDbz8Su7uHzyT895XiTdeWYvrv9EGizl++kciEbhcLvT29iIczs+WTMqO6ULgf35vD35ya7fqdbnqilr85tdLoNMxBBJR9jEA0ryYzWa0trairq5OuecqAGzfMYl1F3bg459+G/0D+bduIACYTSKu/3or3nhlLdavS05wmZycRFdXF0ZGRiBJUg5rSGpKLHmUGgL/6weduOkne1Wvy/svrcFv//coZSmjiYkJ9PerP0uZiIofAyAtSFlZGRYtWgSn05m2/8GHh7HmhA249bYehMP5Gaaam8y4+0/L8PD9K7B4UfzOJ7Isw+12o7OzE16vN8c1JLUkQmBqq/YPbuzCD2/qUr0ul15UjT/8ZikMhngI9Hq9cLlcHKtKRBnFAEgLJooiampq0N7ennYLucnJGL7z/T044dQ38NzznhzW8NDOPN2J1148Fv/17UWw2eKTXKLRKPr7+9Hd3Y1QKD9bMimzRFFEY2NjWgi88Za9+P4POlWvy4Xrq/Cn3x6thECfz4f+/n6GQCLKGAZAyhiTyYSWlhY0NDSkdQu/u3sKF12+BR+8+i309gZzWMODMxpFXPvlZmx8dS0uu7ha2T81NYWuri4MDQ2xW1gDEiEw9YvMLbd24zvf36N6Xdavq8SdfzhaWcLI5/OxJZCIMoYBkDLO4XBg0aJFqKiogJBY4AzA40+O4Jj3bMCNt+xFMJSfYaq+zoTf37EUTz26CkctsQGIdwt7PB7s2bMHExP5u+4hZYYoimhoaEgLgbfe1oPrv71b9bq875xK3PPno2HaFwL9fj9DIBFlBAMgZYUoiqiurkZ7eztsNpuyPxCU8MObunDcia/jqadHc1jDQzvpPWV4+bljcdMPDoPDEW/NjEajGBgYQHd3N4LB/GzJpMxItASmnru/+t9efP36d1Wvy1lnVuC+O5fBbEqGwL6+PoZAIloQBkDKqkS3cGNjIwwGg7J/b3cAV354Gy69cis6uwI5rOHB6fUCPvfpRnRsWIsPXlmr3K0hEAhg7969GBwcRCwWy20lKWsEQUBDQ0NaCPy/3/bhq9/YBbWz1xmnO3H/3cthscTHqE5OTqKvr4/DEoho3hgASRWJbuHKysq0buFn/+nGcSe9ju//oBOBQH6GqapKI26/bQn+8dQarFxhV/aPj4+js7MT4+P5u+4hLUwiBJaUlCj7fvsHF665bqfqIfC0U8rx4D3LYbUmQ6DLpf49jImoODAAkmpSu4VTL6jhsIRbbu3GmhM24OHHhnNYw0M79hgHXnhmDW695Qg4nfHWzFgshsHBQezduxeBQH62ZNLCCIKA+vr6tHP2D3/uxxeveQeSpG4KPPnEMjx033JltjrPOSKaLwZAUp3RaERTUxOamppgNBqV/X2uED76ibdx/iWb8c7OyRzW8OBEUcDHP1qPjtfW4hNXNyi37goGg+ju7sbAwAC7hYtQIgTa7ckW4L/cPYDPfVn9EPie48vwyAMrUFKim/nBREQHwQBIOVNSUoL29nZUVVVBFJOn4r9eGsN7TnsD//Gd3fD78zNMlZcb8LObD8e//nEM1h5bquyfmJjAnj17MDY2lsPaUTYIgoC6urq0EHjPfYP41Od3IBZTNwSuPbYUjz24UpmgREQ0VwyAlFOCIKCyshLt7e1pF9ZoVMYvb+/FqrWv4b4HhnJYw0NbsawEzzy5Gv/3yyWoroq3ZkqShKGhIXR1dWFqairHNaRMSrQEOhwOZd8Dfx3CJz+7HdGouiHwmDUOPPbgCpSWMgQS0dwxAFJeMBgMaGxsRHNzc1q38NBwGJ/6/Hacs34Ttr3tz2END04QgKuuqEXHhrX4wmeblPu4hkIh9PT0oL+/H9FoNMe1pEyqq6tLC4F/fWQYH/vU24hE1A2Bq1c58MRDK1Febpj5wUREKRgAKa/YbDa0t7ejuro6rVv41Q0TOOXMN3Hdt3ZhfDw/w5TdrseP/nsx/v38sTjlpHJlv9frRWdnJ9xuN9duKyJ1dXUoLU12/z/6xAg++sm3VA+BK5bb8cTDK1FRwRBIRLPHAEh5RxAEVFRUYNGiRWmtLLGYjDt+58Kqta/hT3cOqL4Mx2wtOdKGJx5eiT/+dika6k0A4t3CIyMj6OrqwuRkfk5wobmrra1FWVmZsv3EU6P44NXbEA6ruz7fsqUl+N3/LVX1mERU2BgAKW/p9Xo0NDSgpaUFJpNJ2e/2RPCla9/B6edsxMZN3hzW8NAuubAaG19di69d06LczzUcDqO3txculwuRSCTHNaRM2D8EPv2MG1d95C3Vb3dYyRZAIpoDBkDKe1arFe3t7aipqYFOl1z6YlOHF2ecuxFfvOYdjLrzM0xZrTp894Z2bHjpOJx1ZoWy3+fzoaenJ4c1o0yqra1FeXmy2//Zf7px5Ye3IRDknTqIKD8xAFLBKC8vR3t7e9q4K1kG/nzXAFYf/xru+J1L9eU4ZmtRuwV/vXc57v3LMrQ0mwGA4wGLTE1NTVoIfO55D6744Na8vcMNEWkbAyAVFJ1Oh7q6OrS2tsJsNiv7x8ejuO5bu3DKmW/i1Q0TOazhoa07txJvvLIW//HNNljMfPsVm5qaGjidTmX7hRfHcNlVWzE1xRBIRPmFVyAqSBaLBW1tbairq0vrFt72th/nrN+ET31+OwaHwjms4cGZTSK+dV0r3nhlLc4/ryrX1aEMq66uRkVFsrv/pX+P45IrtmJykiGQiPIHAyAVtLKyMixatCit6w0A7ntgCKuPfw23/bpX9WU5Zqu5yYy7/ng0Hrl/BQ5bbM11dSiDqqqq0kLgK6+N46L3b4HPl59LGBGR9jAAUsHT6XSora1FW1sbLBaLst/vj+GG7+7Ge057HS+8mL+3ZjvjdCdu+sFhua4GZVhVVRUqKyuV7Q1vTODC92+B18sQSES5xwBIRcNsNqO1tRX19fXQ65O3x9q5awoXXLoZH/3E2+hzhXJYQ9KaysrKtBD45kYvzr90c94uZk5E2sEASEWntLQUixYtgtPphCAIyv6HHxvGmhM24JZbuxFSeaFe0q79Q2DHZh/Ov6QDY2P5uXQREWkDAyAVJVEUUVNTg7a2NlityfF1gUAM3/9BJ9ae9Dqe+Yc7hzUkLamoqEBVVXLCz5Ztfqy/eDPcHoZAIsoNBkAqaiaTCS0tLWhoaEjrFu7sCuCyq7biig9tw97uQA5rSFrhdDpRXV2tbG9724/zLurAyGh+zlYnouKmn/khRIXP4XDAZrPB7XbD4/EoizD/7e+jeP4FD77yxWZ89ZoWrs1HWZWYrT48PAwA2L5jEuddtBmPP7QSNdXGXFZNU8bHxzE+Pp7rahQVURRhtVphtVphs9nS1mml/MSrHWmGKIqoqqpCW1sbbDabsj8YknDTT/bimPdswONPjuSwhqQF5eXlqKmpUbbf2TmJdRd2YGCQE5SocEmSBL/fj+HhYXR1dWHnzp3o7e3F1NRUrqtGB8EWQNIco9GIpqYm+Hw+DA8PIxKJj8Pq7Q3ig1e/hTNOd+LmHx7Gtfkoa8rKygAAQ0NDAIB3d09h3YUdeOLhVWioN+WyakVr8SILLru4euYH0pzJMhAKS9iyzY/e3iCAZCCcnJw84DaJlB8YAEmz7HY7SkpK4Ha74Xa7lW7h55734PhTXscXPtuEb36tFTabboaSiOaurKwMgiBgcHAQALCnM4D3XdCBpx5ZicZGdp9l2llnVuCsMytmfiDNmywDL78yjrvvHcAjj49gcjIGWZYxODiIUCiEmpqatJUZKHdEUWQXMGmbIAiorKxEe3s7SkpKlP2RiIxbb+vBmhM24MGHhnJYQypmpaWlqK2tVbb3dgdw7gUd6NnXikJUSAQBOPnEMtx+2xLs2X4i7vjVEmVs69jYGHp7exGL8ZaIuSTLMkRRhCAIDIBEAGAwGNDY2IjGxkYYjcnB+P0DIXz8M9ux7sIOvL1jMoc1pGJVWlqKuro6ZbunN4hzL+jg7HQqaFarDldeXov77lymTK6bnJxEd3d3jmumXbIsQ6fTKa2wDIBEKUpKStDW1oaqqiqIYvLt8fIr4zj5jDfwzRve5a28KOMcDkdaCOzrC+J9F3Sgs4shkArb6lUO/Ob2o5Do+Q0Gg/D5fLmtlEbp9fq0LngGQKL9CIKAiooKtLW1weFwKPujURm339GHVWs34K57B7FvyCBRRjgcDtTX1ysf0K7+EN53QQfe3c1ZlFTYLlhfhf/69iJle3R0NIe10R5BEKDTHTiWnQGQ6CAMBgMaGhrQ3NwMkyk5M3NkNIzPfWkH3rtuI7Zs5TdZyhy73Y7vreEdAAAQDUlEQVS6ujolBA4MhrDuwg7s3MUQSIXtmi814+IL4rOwA4EAl4dRiSiK04Y/gAGQaEY2mw1tbW2oqalJ6xZ+400vTj1rI665bic8vKUXZYjdbk9rCRwaDmPdhR3YzjGoVODOPy95T2y3m7fizCZBEOIzfcWDxzwGQKJZEAQBTqcT7e3tad3CkiTj93/qx6rjN+B3f3RBktgvTAtXUlKChoYGJQSOjIax/uIOvLXdn+OaEc3fyhV25Wefz4dwmLdBzIbZhD+AAZBoTvR6Perr69HS0pLWLTw2FsG1X9+FU8/aiNffmMhhDalYlJSUoLGxUQmBo+4I1l+8GVu2MQRSYVrUbkVJSbI7MhrlhLpMS4S/2ay3yABINA8Wi0XpFk4dX7Flqw9nnbcJn/vSDgyP8NstLYzNZksLgR5PBOdf0oHNWzj2lAqPIAArltlnfiDNS2Kyx2wX22YAJFqA8vJytLe3K7f2AuKr4d917yBWH78Bt9/Rh2g0N93CqbOU1Vp9P9HlMDYezUp3eEyDXeyJEJh4bcfHozj/0s3YuMmb9WNLMfVe79TuKjfH1BatZUcnF9znXUEy52AzfQ+FAZBogXQ6HWpra9Ha2gqzOXkLL683im/e8C5OOuMNvPzKuOr1GhtLXkT1enXu+pj49/t8UWzNcFel2xNBJBIPJHq9fsbxLcVk/xA4MRHFBZdtyfpwg+6e5B1Jsn2xTn3vvPzvsawei3JnF5c1yrhDzfQ95POyUBciTTKbzWhtbUVtbW3am3H7jkmsu7ADH//02+gfCKlWnyf/llxrK/XuJtlksViUn//1UmYv4vc9MKj8bLVaM1p2IbBarWkh0OeL4qLLt+DVDdkLgbveTV6ss/0lwmQyKSHztQ0TCIelrB6P1DcxEcVLLyc/F9gCuDCJVr/5fhlmACTKsLKyMrS3t6O8vDxt/4MPD2PNCRtw6209qlzcHki5h3HqhJVsymYAvPNubQdAIP7vbmpqUj7w/f4YLr58S9ZamFNba1LvlZ0NgiAorYCBoIQ3VejiJnX9/Vm30ooPMAAuxFzH+02HAZAoC3Q6HWpqatDa2poWiiYnY/jO9/fghFPfwHPPe7J2/O07JtPWjctFAHx1w0TGxj9u2epLWwJFqwEQiL/GqSFwaiqGS6/cmvHAHQpLuPf+ZOguLS3NaPnTST1/XnxZ/WETlF1PPDWi/Gy1WlX7XCo28xnvNx0GQKIsMpvNaGlpQV1dXVoX2ru7p3DR5VvwwavfQm9v8BAlzE9q658oimkX1mwSRVFpxZmcjOG+B4dmeMbs/OXuAeVng8EAg8GQkXILlcViQXNzsxICA4EYLv/AVjz/Qua+VNx4816lC9hoNKaN0cuW1PP0oUeG4ffHsn5MUkcgEMOzzyXPz6qqqhzWpnDNd7zftGVlpBQiOqTS0lK0t7fD6XSmNdk//uQIjnnPBtx4y14EQ5npFh51R9JabqqqqlSbBAIgbaHsL137Dp56emH3/XzjTS/uuT8ZJLPdFVkozGYzmpqalItBICjhig9twz+eW3gI3LLVh5//skfZVqP1D4i3CiXO1Xd2TuLyD25FIMixgIVuaiqGKz60DZOT8UBvtVphs9lyXKvCstDxftNhACRSiSiKqK6uRmtra1oXZiAo4Yc3deG4E19fcFjq2hvAe9+3Ea7++GQTs9l8wFjEbHM6nbDb42t9RaMyPvqJt/H8v+bXPfnQo8M476IO+HzxBWPNZjNbDlLsHwKDIQlXfXgbnn5m/rfZikRkfP7L76R136eG+mwSRTHtDigvvzKOD350GyeEFDCvN4oLL9uCF15MfgZUVlYe4hm0v0yM95uOrqam5nvzfXI2BnCqNSg0W8fha5L9MrNZrhrH0ev1KC0thclkQiAQgCTFL27jE1E8+PAw3tzkw7FrHCgvn1s356YOL9ZfvFkJfwDQ2NiY8da/2bwmJSUl8Pl8iMViiMVkPPr4CE4+sQyNDbPvRrzl1m589Ru7lCCi1+vR3Nw8p+4PNc6TXJ/jer0eNpsNPp8PsizHX+8nRhCNyjhmjQMGw+y/53u9UXzrP3fjmX8kA6TT6ZxVAMzU66DX66HT6TA5GR/D2tkVwDu7pnDh+VUQRU4aKCRuTwQXXLIZGzuSC5dbLBbU1NRk/djFcj3LZJfv/hgAC6DcQn5NCul1Vvs4JpNJWUA6GEyOA+zsCuD3f+pHICjhsMVWOOwzB7hn/uHGZR/Yhglv8tZKZWVlaQtUZ8psXhNBEGC1WuH1eiHLMqJRGQ89Oozu7gD0egGNjWbodQeWI0ky3t0dwA3f241f3t6bVl5zc/Ocl7PRQgAEkiHQ7/dDkiTEYjJefmUc994/iJpqI45acuhu80BQwq9u78VHPvk2XktZVsbpdKK6ujrj9Z2J2WxGNBpFKBT/MrNz1xSee8GDUFBCba1pVu8Jyp3BoTCeenoUn//yDrydMhnNYrGgsbExa4EmVTFczzLd5XvAcZcvXz7tND1Znnn23qFejNk8f7rHzeYFnm3Zh3rsbP+QcznWdOXO9fnTPSfTr8l8jzOfY0xX5nzK2f9583kjZqL+8637TMLhMIaGhpRWj1RLl9hw9lkVOPu9FVh7bCn0egG790zh9Te8eP3NCWx4fQI7dk6l3XmjpKQE9fX1C/7wmO7fO5fX3ufzweVyHbC/pESHs8+swPp1lRBEAR2bfejY7EXHFt+0A//r6+vTWqFm+3fIxjk912PM9zjzKTccDsPlciEcTr8N4fHHleK7/9mO+joT9DoBeoMAvV6AThTw8GPD+PFPujEwmL5G5Uzhbz6fUzNJLVOWZfT09KR9OYofB1i10oHz11XitFPKYTRxJFOuyXL8i+uLL43hxZfH0taPTCgrK0Ntba1qC7hn47NbrQA4l/v5Lui4DICZO9Z05TIAMgDOhd/vx9DQECKR6W+F5XDoYdALB71VVmL5mUyN2VpoAASAQCAAt9sNv3/udwaxWCyoqKg4YOIHA+DBy5VlGR6PB263e97/vtm0/GU7AAKAJEkYHx+Hx+NBLMYZwYVIEATU1tYqvRG56r0plACYqSVeZnVcBsDMHWu6chkAGQDnKnEB93q9ShfYbDgcDtTU1GT0wyMTATAhGAxidHR0VkGwpKQETqfzoOv9MQDOXG44HMbw8PC0rcoHYzab/7+9e1mO4gYDMKq+mAIvhuL9H9IuXCzMDDSbaCLL3fZc+q5zNoCTjG2STL78aknh+/fvF20cmiMAoxiCT09P4XQ69f45rEtVVefn/dIjhATg8OvWdT3rFZcCcMTP1fe6AlAA3uP379/h5eUlvLy8vFsKa9v2fCbe4XCY5HiUMQMwen19DU9PT+fNL+nnaJom/Pjx49MDYgXg5a/78+fP8Pz8HE6nUzidTu/+mi9fvoTD4RAOh8NVz1jOGYDpH39+fg6/fv0Kx+MxHI/H2f+dpF/TNOHx8TF8+/YtPD4+hq9fv/b+MyEA34vhN/fNKAJwxM/V97oCUACO5Xg8htfX13P0zfF/ilMEYN/rXvuaAvD21z2dTuHPnz/heDyGh4eHmw94XiIA+z7PvX8Pu65bxUaee76PuTY8fPQ1Xvo1CMC3ptzl+xlbqWAj3IDBGOLkeI6bPeYwxn+U1xCAa/s8c0+jShOf9Vvy91kAAgDMZMmpX0oAAgBMLB7vMudGj48IQACACa1hyTcnAAEAJrKWJd+cAAQAGNkap34pAQgAMKK1Tv1SAhAAYCRN06xmo8dHBCAAwJ3WvuSbE4AAAHfYytQvJQABAG6wtalfSgACAFxpi1O/lAAEALjQlqd+KQEIAHCBLRzvcikBCADwgRh+W5/6pQQgAECPuNy75Wf9hghAAIBM0zS7We7tIwABAP6z56lfSgACAIT9T/1SAhAAKFpVVaFt211t8viMAAQAitW27e6Xe/sIQACgOHs82uUaAhAAKEo+9YsR2HXdUl/S7AQgAFCEfJNHqdO/EAQgALBz+XJvyeEXCUAAYLfSM/2E3/8EIACwK13XvVnuFX7vCUAAYDfS5V7hN0wAAgCbF69wK+Umj3sJQABgs9K7e038LicAAYBNiuFX4k0e9xKAAMCmxLt7hd/tBCAAsAnCbzwCEABYvaZpQtvKlrH4nQQAVivu7LXBY1wCEABYnbquQ9u2wm8iAhAAWA3hNw8BCAAszgaPeQlAAGAxVVWFh4cHE7+ZCUAAYHYmfssSgADAbITfOghAAGBydV2fj3RheQIQAJhMvKvXWX7rIgABgNEJv3UTgADAaKqqcnvHBghAAOBucdpX17Xw2wABCADcTPhtkwAEAK5SVdX5OJf4c7ZFAAIAF0nDzzl+2yYAAYAPpeEXz/Hrum7hr4p7CEAAoFcMPwc4748ABADesNS7fwIQADhv5KjrWvgVQAACQMFi+MVlXuFXBgEIAAXKn+9zlEtZBCAAFCJd5nV4c9kEIADsXJz22dFLJAABYKdi9KUTPwhBAALArqRXs8XoE37kBCAA7EC6zBsnfp7vY4gABIANS8PP+X1cSgACwMbk0762bU37uIoABICN6Jv2CT9uIQABYMX6NnU4xoV7CUAAWKG+s/tM+xiLAASAlcif7TPtYyoCEAAWlC7xptFn2seUBCAALCCd9qVn98EcBCAAzCQNvhCCaR+LEYAAMKG+6HM9G0sTgAAwsrikm0720nP7uq5b8KsDAQgAo4nhl/48nt0HayIAAeAOQ8/1uaWDNROAAHClPPriYc2e7WMrBCAAXGAo+uziZYsEIAAMMOljrwQgACTy6AshnKPPQc3shQAEoHh90VfXdWjb9t3HYQ8EIABFSq9gGzqvD/ZKAAJQjDz4uq47P89nMwclEYAA7FrflC+NPiiRAARgdz6LvvTjrmWjRAIQgM1LN3Hk0edWDnhPAAKwSekmjvRj6ZRP9EE/AQjAZsRJXh52zumD6whAAFYrX9qN4Ref53MjB9xGAAKwKukGjnTSV1WVM/pgJAIQgEXld+3mP3dGH4xPAAIwu74pX3oo89CzfsA4BCAAkxta1g0hvIs+YHoCEIDRDd2zG8LwBg4HMsN8BCAAd0t36fbtyo3HtJjywToIQABuEmNu6CiWpmls3oCVEoAAXOSj5/jix9zAAdsgAAHolQZfPuFLl3xN+WB7BCAAIYT3E778EOY0BgUfbJsABChUumnjs926wL4IQICCpLdqpGGX37qRcjwL7I8ABNixvp26+a8t6UJ5BCDATgw9p5du1kgjECiXAATYqDz2YtSlv47RZxkXSLXpG0PXdaGqKm8UACuTX60W37vzjRyWc4FLtCGEN8sE8ceu696EIQDzyXfnpu/PYg+41+AScN9J7qIQYBpp1KVhF3fkOmwZGNNVzwDmUSgGAa7XN8WLP6ZXqcX3W++xwNju2gSSLx2HIAoBUvlze0PRBzCn0XcB58sW0d+/f0MIwhDYv/xQ5aE7deNjNafTacGvtl9J79WlfK+lfJ8hlPW93uofobzfbYnRxloAAAAASUVORK5CYII="}; diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/playback.js b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/playback.js new file mode 100644 index 0000000000000000000000000000000000000000..7756529d8cec792055b590f217e4c681150d3c3f --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/playback.js @@ -0,0 +1,102 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2012 Joel Martin + * Licensed under MPL 2.0 (see LICENSE.txt) + */ + +"use strict"; +/*jslint browser: true, white: false */ +/*global Util, VNC_frame_data, finish */ + +var rfb, mode, test_state, frame_idx, frame_length, + iteration, iterations, istart_time, + + // Pre-declarations for jslint + send_array, next_iteration, queue_next_packet, do_packet; + +// Override send_array +send_array = function (arr) { + // Stub out send_array +}; + +next_iteration = function () { + if (iteration === 0) { + frame_length = VNC_frame_data.length; + test_state = 'running'; + } else { + rfb.disconnect(); + } + + if (test_state !== 'running') { return; } + + iteration += 1; + if (iteration > iterations) { + finish(); + return; + } + + frame_idx = 0; + istart_time = (new Date()).getTime(); + rfb.connect('test', 0, "bogus"); + + queue_next_packet(); + +}; + +queue_next_packet = function () { + var frame, foffset, toffset, delay; + if (test_state !== 'running') { return; } + + frame = VNC_frame_data[frame_idx]; + while ((frame_idx < frame_length) && (frame.charAt(0) === "}")) { + //Util.Debug("Send frame " + frame_idx); + frame_idx += 1; + frame = VNC_frame_data[frame_idx]; + } + + if (frame === 'EOF') { + Util.Debug("Finished, found EOF"); + next_iteration(); + return; + } + if (frame_idx >= frame_length) { + Util.Debug("Finished, no more frames"); + next_iteration(); + return; + } + + if (mode === 'realtime') { + foffset = frame.slice(1, frame.indexOf('{', 1)); + toffset = (new Date()).getTime() - istart_time; + delay = foffset - toffset; + if (delay < 1) { + delay = 1; + } + + setTimeout(do_packet, delay); + } else { + setTimeout(do_packet, 1); + } +}; + +var bytes_processed = 0; + +do_packet = function () { + //Util.Debug("Processing frame: " + frame_idx); + var frame = VNC_frame_data[frame_idx], + start = frame.indexOf('{', 1) + 1; + bytes_processed += frame.length - start; + if (VNC_frame_encoding === 'binary') { + var u8 = new Uint8Array(frame.length - start); + for (var i = 0; i < frame.length - start; i++) { + u8[i] = frame.charCodeAt(start + i); + } + rfb.recv_message({'data' : u8}); + } else { + rfb.recv_message({'data' : frame.slice(start)}); + } + frame_idx += 1; + + queue_next_packet(); +}; + diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/rfb.js b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/rfb.js new file mode 100644 index 0000000000000000000000000000000000000000..e49feeb9ce34972befbdb1d172fff61b60568a9b --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/rfb.js @@ -0,0 +1,1961 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2012 Joel Martin + * Copyright (C) 2013 Samuel Mannehed for Cendio AB + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + * + * TIGHT decoder portion: + * (c) 2012 Michael Tinglof, Joe Balaz, Les Piech (Mercuri.ca) + */ + +/*jslint white: false, browser: true, bitwise: false, plusplus: false */ +/*global window, Util, Display, Keyboard, Mouse, Websock, Websock_native, Base64, DES */ + + +function RFB(defaults) { +"use strict"; +var that = {}, // Public API methods + conf = {}, // Configuration attributes + + // Pre-declare private functions used before definitions (jslint) + init_vars, updateState, fail, handle_message, + init_msg, normal_msg, framebufferUpdate, print_stats, + + pixelFormat, clientEncodings, fbUpdateRequest, fbUpdateRequests, + keyEvent, pointerEvent, clientCutText, + + getTightCLength, extract_data_uri, + keyPress, mouseButton, mouseMove, + + checkEvents, // Overridable for testing + + + // + // Private RFB namespace variables + // + rfb_host = '', + rfb_port = 5900, + rfb_password = '', + rfb_path = '', + + rfb_state = 'disconnected', + rfb_version = 0, + rfb_max_version= 3.8, + rfb_auth_scheme= '', + + + // In preference order + encodings = [ + ['COPYRECT', 0x01 ], + ['TIGHT', 0x07 ], + ['TIGHT_PNG', -260 ], + ['HEXTILE', 0x05 ], + ['RRE', 0x02 ], + ['RAW', 0x00 ], + ['DesktopSize', -223 ], + ['Cursor', -239 ], + + // Psuedo-encoding settings + //['JPEG_quality_lo', -32 ], + ['JPEG_quality_med', -26 ], + //['JPEG_quality_hi', -23 ], + //['compress_lo', -255 ], + ['compress_hi', -247 ], + ['last_rect', -224 ] + ], + + encHandlers = {}, + encNames = {}, + encStats = {}, // [rectCnt, rectCntTot] + + ws = null, // Websock object + display = null, // Display object + keyboard = null, // Keyboard input handler object + mouse = null, // Mouse input handler object + sendTimer = null, // Send Queue check timer + connTimer = null, // connection timer + disconnTimer = null, // disconnection timer + msgTimer = null, // queued handle_message timer + + // Frame buffer update state + FBU = { + rects : 0, + subrects : 0, // RRE + lines : 0, // RAW + tiles : 0, // HEXTILE + bytes : 0, + x : 0, + y : 0, + width : 0, + height : 0, + encoding : 0, + subencoding : -1, + background : null, + zlibs : [] // TIGHT zlib streams + }, + + fb_Bpp = 4, + fb_depth = 3, + fb_width = 0, + fb_height = 0, + fb_name = "", + + last_req_time = 0, + rre_chunk_sz = 100, + + timing = { + last_fbu : 0, + fbu_total : 0, + fbu_total_cnt : 0, + full_fbu_total : 0, + full_fbu_cnt : 0, + + fbu_rt_start : 0, + fbu_rt_total : 0, + fbu_rt_cnt : 0, + pixels : 0 + }, + + test_mode = false, + + def_con_timeout = Websock_native ? 2 : 5, + + /* Mouse state */ + mouse_buttonMask = 0, + mouse_arr = [], + viewportDragging = false, + viewportDragPos = {}; + +// Configuration attributes +Util.conf_defaults(conf, that, defaults, [ + ['target', 'wo', 'dom', null, 'VNC display rendering Canvas object'], + ['focusContainer', 'wo', 'dom', document, 'DOM element that captures keyboard input'], + + ['encrypt', 'rw', 'bool', false, 'Use TLS/SSL/wss encryption'], + ['true_color', 'rw', 'bool', true, 'Request true color pixel data'], + ['local_cursor', 'rw', 'bool', false, 'Request locally rendered cursor'], + ['shared', 'rw', 'bool', true, 'Request shared mode'], + ['view_only', 'rw', 'bool', false, 'Disable client mouse/keyboard'], + + ['connectTimeout', 'rw', 'int', def_con_timeout, 'Time (s) to wait for connection'], + ['disconnectTimeout', 'rw', 'int', 3, 'Time (s) to wait for disconnection'], + + // UltraVNC repeater ID to connect to + ['repeaterID', 'rw', 'str', '', 'RepeaterID to connect to'], + + ['viewportDrag', 'rw', 'bool', false, 'Move the viewport on mouse drags'], + + ['check_rate', 'rw', 'int', 217, 'Timing (ms) of send/receive check'], + ['fbu_req_rate', 'rw', 'int', 1413, 'Timing (ms) of frameBufferUpdate requests'], + + // Callback functions + ['onUpdateState', 'rw', 'func', function() { }, + 'onUpdateState(rfb, state, oldstate, statusMsg): RFB state update/change '], + ['onPasswordRequired', 'rw', 'func', function() { }, + 'onPasswordRequired(rfb): VNC password is required '], + ['onClipboard', 'rw', 'func', function() { }, + 'onClipboard(rfb, text): RFB clipboard contents received'], + ['onBell', 'rw', 'func', function() { }, + 'onBell(rfb): RFB Bell message received '], + ['onFBUReceive', 'rw', 'func', function() { }, + 'onFBUReceive(rfb, fbu): RFB FBU received but not yet processed '], + ['onFBUComplete', 'rw', 'func', function() { }, + 'onFBUComplete(rfb, fbu): RFB FBU received and processed '], + ['onFBResize', 'rw', 'func', function() { }, + 'onFBResize(rfb, width, height): frame buffer resized'], + ['onDesktopName', 'rw', 'func', function() { }, + 'onDesktopName(rfb, name): desktop name received'], + + // These callback names are deprecated + ['updateState', 'rw', 'func', function() { }, + 'obsolete, use onUpdateState'], + ['clipboardReceive', 'rw', 'func', function() { }, + 'obsolete, use onClipboard'] + ]); + + +// Override/add some specific configuration getters/setters +that.set_local_cursor = function(cursor) { + if ((!cursor) || (cursor in {'0':1, 'no':1, 'false':1})) { + conf.local_cursor = false; + } else { + if (display.get_cursor_uri()) { + conf.local_cursor = true; + } else { + Util.Warn("Browser does not support local cursor"); + } + } +}; + +// These are fake configuration getters +that.get_display = function() { return display; }; + +that.get_keyboard = function() { return keyboard; }; + +that.get_mouse = function() { return mouse; }; + + + +// +// Setup routines +// + +// Create the public API interface and initialize values that stay +// constant across connect/disconnect +function constructor() { + var i, rmode; + Util.Debug(">> RFB.constructor"); + // Create lookup tables based encoding number + for (i=0; i < encodings.length; i+=1) { + encHandlers[encodings[i][1]] = encHandlers[encodings[i][0]]; + encNames[encodings[i][1]] = encodings[i][0]; + encStats[encodings[i][1]] = [0, 0]; + } + // Initialize display, mouse, keyboard, and websock + try { + display = new Display({'target': conf.target}); + } catch (exc) { + Util.Error("Display exception: " + exc); + updateState('fatal', "No working Display"); + } + keyboard = new Keyboard({'target': conf.focusContainer, + 'onKeyPress': keyPress}); + mouse = new Mouse({'target': conf.target, + 'onMouseButton': mouseButton, + 'onMouseMove': mouseMove}); + + rmode = display.get_render_mode(); + ws = new Websock(); + ws.on('message', handle_message); + ws.on('open', function() { + if (rfb_state === "connect") { + updateState('ProtocolVersion', "Starting VNC handshake"); + } else { + fail("Got unexpected WebSockets connection"); + } + }); + ws.on('close', function(e) { + Util.Warn("WebSocket on-close event"); + var msg = ""; + if (e.code) { + msg = " (code: " + e.code; + if (e.reason) { + msg += ", reason: " + e.reason; + } + msg += ")"; + } + if (rfb_state === 'disconnect') { + updateState('disconnected', 'VNC disconnected' + msg); + } else if (rfb_state === 'ProtocolVersion') { + fail('Failed to connect to server' + msg); + } else if (rfb_state in {'failed':1, 'disconnected':1}) { + Util.Error("Received onclose while disconnected" + msg); + } else { + fail('Server disconnected' + msg); + } + }); + ws.on('error', function(e) { + Util.Warn("WebSocket on-error event"); + //fail("WebSock reported an error"); + }); + + + init_vars(); + + /* Check web-socket-js if no builtin WebSocket support */ + if (Websock_native) { + Util.Info("Using native WebSockets"); + updateState('loaded', 'noVNC ready: native WebSockets, ' + rmode); + } else { + Util.Warn("Using web-socket-js bridge. Flash version: " + + Util.Flash.version); + if ((! Util.Flash) || + (Util.Flash.version < 9)) { + updateState('fatal', "WebSockets or <a href='http://get.adobe.com/flashplayer'>Adobe Flash<\/a> is required"); + } else if (document.location.href.substr(0, 7) === "file://") { + updateState('fatal', + "'file://' URL is incompatible with Adobe Flash"); + } else { + updateState('loaded', 'noVNC ready: WebSockets emulation, ' + rmode); + } + } + + Util.Debug("<< RFB.constructor"); + return that; // Return the public API interface +} + +function connect() { + Util.Debug(">> RFB.connect"); + var uri; + if (typeof UsingSocketIO !== "undefined") { + uri = "http://" + rfb_host + ":" + rfb_port + "/" + rfb_path; + } else { + if (conf.encrypt) { + uri = "wss://"; + } else { + uri = "ws://"; + } + uri += rfb_host + ":" + rfb_port + "/" + rfb_path; + } + Util.Info("connecting to " + uri); + // TODO: make protocols a configurable + ws.open(uri, ['binary', 'base64']); + + Util.Debug("<< RFB.connect"); +} + +// Initialize variables that are reset before each connection +init_vars = function() { + var i; + /* Reset state */ + ws.init(); + + FBU.rects = 0; + FBU.subrects = 0; // RRE and HEXTILE + FBU.lines = 0; // RAW + FBU.tiles = 0; // HEXTILE + FBU.zlibs = []; // TIGHT zlib encoders + mouse_buttonMask = 0; + mouse_arr = []; + + // Clear the per connection encoding stats + for (i=0; i < encodings.length; i+=1) { + encStats[encodings[i][1]][0] = 0; + } + + for (i=0; i < 4; i++) { + //FBU.zlibs[i] = new InflateStream(); + FBU.zlibs[i] = new TINF(); + FBU.zlibs[i].init(); + } +}; + +// Print statistics +print_stats = function() { + var i, s; + Util.Info("Encoding stats for this connection:"); + for (i=0; i < encodings.length; i+=1) { + s = encStats[encodings[i][1]]; + if ((s[0] + s[1]) > 0) { + Util.Info(" " + encodings[i][0] + ": " + + s[0] + " rects"); + } + } + Util.Info("Encoding stats since page load:"); + for (i=0; i < encodings.length; i+=1) { + s = encStats[encodings[i][1]]; + if ((s[0] + s[1]) > 0) { + Util.Info(" " + encodings[i][0] + ": " + + s[1] + " rects"); + } + } +}; + +// +// Utility routines +// + + +/* + * Page states: + * loaded - page load, equivalent to disconnected + * disconnected - idle state + * connect - starting to connect (to ProtocolVersion) + * normal - connected + * disconnect - starting to disconnect + * failed - abnormal disconnect + * fatal - failed to load page, or fatal error + * + * RFB protocol initialization states: + * ProtocolVersion + * Security + * Authentication + * password - waiting for password, not part of RFB + * SecurityResult + * ClientInitialization - not triggered by server message + * ServerInitialization (to normal) + */ +updateState = function(state, statusMsg) { + var func, cmsg, oldstate = rfb_state; + + if (state === oldstate) { + /* Already here, ignore */ + Util.Debug("Already in state '" + state + "', ignoring."); + return; + } + + /* + * These are disconnected states. A previous connect may + * asynchronously cause a connection so make sure we are closed. + */ + if (state in {'disconnected':1, 'loaded':1, 'connect':1, + 'disconnect':1, 'failed':1, 'fatal':1}) { + if (sendTimer) { + clearInterval(sendTimer); + sendTimer = null; + } + + if (msgTimer) { + clearInterval(msgTimer); + msgTimer = null; + } + + if (display && display.get_context()) { + keyboard.ungrab(); + mouse.ungrab(); + display.defaultCursor(); + if ((Util.get_logging() !== 'debug') || + (state === 'loaded')) { + // Show noVNC logo on load and when disconnected if + // debug is off + display.clear(); + } + } + + ws.close(); + } + + if (oldstate === 'fatal') { + Util.Error("Fatal error, cannot continue"); + } + + if ((state === 'failed') || (state === 'fatal')) { + func = Util.Error; + } else { + func = Util.Warn; + } + + cmsg = typeof(statusMsg) !== 'undefined' ? (" Msg: " + statusMsg) : ""; + func("New state '" + state + "', was '" + oldstate + "'." + cmsg); + + if ((oldstate === 'failed') && (state === 'disconnected')) { + // Do disconnect action, but stay in failed state + rfb_state = 'failed'; + } else { + rfb_state = state; + } + + if (connTimer && (rfb_state !== 'connect')) { + Util.Debug("Clearing connect timer"); + clearInterval(connTimer); + connTimer = null; + } + + if (disconnTimer && (rfb_state !== 'disconnect')) { + Util.Debug("Clearing disconnect timer"); + clearInterval(disconnTimer); + disconnTimer = null; + } + + switch (state) { + case 'normal': + if ((oldstate === 'disconnected') || (oldstate === 'failed')) { + Util.Error("Invalid transition from 'disconnected' or 'failed' to 'normal'"); + } + + break; + + + case 'connect': + + connTimer = setTimeout(function () { + fail("Connect timeout"); + }, conf.connectTimeout * 1000); + + init_vars(); + connect(); + + // WebSocket.onopen transitions to 'ProtocolVersion' + break; + + + case 'disconnect': + + if (! test_mode) { + disconnTimer = setTimeout(function () { + fail("Disconnect timeout"); + }, conf.disconnectTimeout * 1000); + } + + print_stats(); + + // WebSocket.onclose transitions to 'disconnected' + break; + + + case 'failed': + if (oldstate === 'disconnected') { + Util.Error("Invalid transition from 'disconnected' to 'failed'"); + } + if (oldstate === 'normal') { + Util.Error("Error while connected."); + } + if (oldstate === 'init') { + Util.Error("Error while initializing."); + } + + // Make sure we transition to disconnected + setTimeout(function() { updateState('disconnected'); }, 50); + + break; + + + default: + // No state change action to take + + } + + if ((oldstate === 'failed') && (state === 'disconnected')) { + // Leave the failed message + conf.updateState(that, state, oldstate); // Obsolete + conf.onUpdateState(that, state, oldstate); + } else { + conf.updateState(that, state, oldstate, statusMsg); // Obsolete + conf.onUpdateState(that, state, oldstate, statusMsg); + } +}; + +fail = function(msg) { + updateState('failed', msg); + return false; +}; + +handle_message = function() { + //Util.Debug(">> handle_message ws.rQlen(): " + ws.rQlen()); + //Util.Debug("ws.rQslice(0,20): " + ws.rQslice(0,20) + " (" + ws.rQlen() + ")"); + if (ws.rQlen() === 0) { + Util.Warn("handle_message called on empty receive queue"); + return; + } + switch (rfb_state) { + case 'disconnected': + case 'failed': + Util.Error("Got data while disconnected"); + break; + case 'normal': + if (normal_msg() && ws.rQlen() > 0) { + // true means we can continue processing + // Give other events a chance to run + if (msgTimer === null) { + Util.Debug("More data to process, creating timer"); + msgTimer = setTimeout(function () { + msgTimer = null; + handle_message(); + }, 10); + } else { + Util.Debug("More data to process, existing timer"); + } + } + break; + default: + init_msg(); + break; + } +}; + + +function genDES(password, challenge) { + var i, passwd = []; + for (i=0; i < password.length; i += 1) { + passwd.push(password.charCodeAt(i)); + } + return (new DES(passwd)).encrypt(challenge); +} + +function flushClient() { + if (mouse_arr.length > 0) { + //send(mouse_arr.concat(fbUpdateRequests())); + ws.send(mouse_arr); + setTimeout(function() { + ws.send(fbUpdateRequests()); + }, 50); + + mouse_arr = []; + return true; + } else { + return false; + } +} + +// overridable for testing +checkEvents = function() { + var now; + if (rfb_state === 'normal' && !viewportDragging) { + if (! flushClient()) { + now = new Date().getTime(); + if (now > last_req_time + conf.fbu_req_rate) { + last_req_time = now; + ws.send(fbUpdateRequests()); + } + } + } + setTimeout(checkEvents, conf.check_rate); +}; + +keyPress = function(keysym, down) { + var arr; + + if (conf.view_only) { return; } // View only, skip keyboard events + + arr = keyEvent(keysym, down); + arr = arr.concat(fbUpdateRequests()); + ws.send(arr); +}; + +mouseButton = function(x, y, down, bmask) { + if (down) { + mouse_buttonMask |= bmask; + } else { + mouse_buttonMask ^= bmask; + } + + if (conf.viewportDrag) { + if (down && !viewportDragging) { + viewportDragging = true; + viewportDragPos = {'x': x, 'y': y}; + + // Skip sending mouse events + return; + } else { + viewportDragging = false; + ws.send(fbUpdateRequests()); // Force immediate redraw + } + } + + if (conf.view_only) { return; } // View only, skip mouse events + + mouse_arr = mouse_arr.concat( + pointerEvent(display.absX(x), display.absY(y)) ); + flushClient(); +}; + +mouseMove = function(x, y) { + //Util.Debug('>> mouseMove ' + x + "," + y); + var deltaX, deltaY; + + if (viewportDragging) { + //deltaX = x - viewportDragPos.x; // drag viewport + deltaX = viewportDragPos.x - x; // drag frame buffer + //deltaY = y - viewportDragPos.y; // drag viewport + deltaY = viewportDragPos.y - y; // drag frame buffer + viewportDragPos = {'x': x, 'y': y}; + + display.viewportChange(deltaX, deltaY); + + // Skip sending mouse events + return; + } + + if (conf.view_only) { return; } // View only, skip mouse events + + mouse_arr = mouse_arr.concat( + pointerEvent(display.absX(x), display.absY(y)) ); +}; + + +// +// Server message handlers +// + +// RFB/VNC initialisation message handler +init_msg = function() { + //Util.Debug(">> init_msg [rfb_state '" + rfb_state + "']"); + + var strlen, reason, length, sversion, cversion, repeaterID, + i, types, num_types, challenge, response, bpp, depth, + big_endian, red_max, green_max, blue_max, red_shift, + green_shift, blue_shift, true_color, name_length, is_repeater; + + //Util.Debug("ws.rQ (" + ws.rQlen() + ") " + ws.rQslice(0)); + switch (rfb_state) { + + case 'ProtocolVersion' : + if (ws.rQlen() < 12) { + return fail("Incomplete protocol version"); + } + sversion = ws.rQshiftStr(12).substr(4,7); + Util.Info("Server ProtocolVersion: " + sversion); + is_repeater = 0; + switch (sversion) { + case "000.000": is_repeater = 1; break; // UltraVNC repeater + case "003.003": rfb_version = 3.3; break; + case "003.006": rfb_version = 3.3; break; // UltraVNC + case "003.889": rfb_version = 3.3; break; // Apple Remote Desktop + case "003.007": rfb_version = 3.7; break; + case "003.008": rfb_version = 3.8; break; + case "004.000": rfb_version = 3.8; break; // Intel AMT KVM + case "004.001": rfb_version = 3.8; break; // RealVNC 4.6 + default: + return fail("Invalid server version " + sversion); + } + if (is_repeater) { + repeaterID = conf.repeaterID; + while (repeaterID.length < 250) { + repeaterID += "\0"; + } + ws.send_string(repeaterID); + break; + } + if (rfb_version > rfb_max_version) { + rfb_version = rfb_max_version; + } + + if (! test_mode) { + sendTimer = setInterval(function() { + // Send updates either at a rate of one update + // every 50ms, or whatever slower rate the network + // can handle. + ws.flush(); + }, 50); + } + + cversion = "00" + parseInt(rfb_version,10) + + ".00" + ((rfb_version * 10) % 10); + ws.send_string("RFB " + cversion + "\n"); + updateState('Security', "Sent ProtocolVersion: " + cversion); + break; + + case 'Security' : + if (rfb_version >= 3.7) { + // Server sends supported list, client decides + num_types = ws.rQshift8(); + if (ws.rQwait("security type", num_types, 1)) { return false; } + if (num_types === 0) { + strlen = ws.rQshift32(); + reason = ws.rQshiftStr(strlen); + return fail("Security failure: " + reason); + } + rfb_auth_scheme = 0; + types = ws.rQshiftBytes(num_types); + Util.Debug("Server security types: " + types); + for (i=0; i < types.length; i+=1) { + if ((types[i] > rfb_auth_scheme) && (types[i] < 3)) { + rfb_auth_scheme = types[i]; + } + } + if (rfb_auth_scheme === 0) { + return fail("Unsupported security types: " + types); + } + + ws.send([rfb_auth_scheme]); + } else { + // Server decides + if (ws.rQwait("security scheme", 4)) { return false; } + rfb_auth_scheme = ws.rQshift32(); + } + updateState('Authentication', + "Authenticating using scheme: " + rfb_auth_scheme); + init_msg(); // Recursive fallthrough (workaround JSLint complaint) + break; + + // Triggered by fallthough, not by server message + case 'Authentication' : + //Util.Debug("Security auth scheme: " + rfb_auth_scheme); + switch (rfb_auth_scheme) { + case 0: // connection failed + if (ws.rQwait("auth reason", 4)) { return false; } + strlen = ws.rQshift32(); + reason = ws.rQshiftStr(strlen); + return fail("Auth failure: " + reason); + case 1: // no authentication + if (rfb_version >= 3.8) { + updateState('SecurityResult'); + return; + } + // Fall through to ClientInitialisation + break; + case 2: // VNC authentication + if (rfb_password.length === 0) { + // Notify via both callbacks since it is kind of + // a RFB state change and a UI interface issue. + updateState('password', "Password Required"); + conf.onPasswordRequired(that); + return; + } + if (ws.rQwait("auth challenge", 16)) { return false; } + challenge = ws.rQshiftBytes(16); + //Util.Debug("Password: " + rfb_password); + //Util.Debug("Challenge: " + challenge + + // " (" + challenge.length + ")"); + response = genDES(rfb_password, challenge); + //Util.Debug("Response: " + response + + // " (" + response.length + ")"); + + //Util.Debug("Sending DES encrypted auth response"); + ws.send(response); + updateState('SecurityResult'); + return; + default: + fail("Unsupported auth scheme: " + rfb_auth_scheme); + return; + } + updateState('ClientInitialisation', "No auth required"); + init_msg(); // Recursive fallthrough (workaround JSLint complaint) + break; + + case 'SecurityResult' : + if (ws.rQwait("VNC auth response ", 4)) { return false; } + switch (ws.rQshift32()) { + case 0: // OK + // Fall through to ClientInitialisation + break; + case 1: // failed + if (rfb_version >= 3.8) { + length = ws.rQshift32(); + if (ws.rQwait("SecurityResult reason", length, 8)) { + return false; + } + reason = ws.rQshiftStr(length); + fail(reason); + } else { + fail("Authentication failed"); + } + return; + case 2: // too-many + return fail("Too many auth attempts"); + } + updateState('ClientInitialisation', "Authentication OK"); + init_msg(); // Recursive fallthrough (workaround JSLint complaint) + break; + + // Triggered by fallthough, not by server message + case 'ClientInitialisation' : + ws.send([conf.shared ? 1 : 0]); // ClientInitialisation + updateState('ServerInitialisation', "Authentication OK"); + break; + + case 'ServerInitialisation' : + if (ws.rQwait("server initialization", 24)) { return false; } + + /* Screen size */ + fb_width = ws.rQshift16(); + fb_height = ws.rQshift16(); + + /* PIXEL_FORMAT */ + bpp = ws.rQshift8(); + depth = ws.rQshift8(); + big_endian = ws.rQshift8(); + true_color = ws.rQshift8(); + + red_max = ws.rQshift16(); + green_max = ws.rQshift16(); + blue_max = ws.rQshift16(); + red_shift = ws.rQshift8(); + green_shift = ws.rQshift8(); + blue_shift = ws.rQshift8(); + ws.rQshiftStr(3); // padding + + Util.Info("Screen: " + fb_width + "x" + fb_height + + ", bpp: " + bpp + ", depth: " + depth + + ", big_endian: " + big_endian + + ", true_color: " + true_color + + ", red_max: " + red_max + + ", green_max: " + green_max + + ", blue_max: " + blue_max + + ", red_shift: " + red_shift + + ", green_shift: " + green_shift + + ", blue_shift: " + blue_shift); + + if (big_endian !== 0) { + Util.Warn("Server native endian is not little endian"); + } + if (red_shift !== 16) { + Util.Warn("Server native red-shift is not 16"); + } + if (blue_shift !== 0) { + Util.Warn("Server native blue-shift is not 0"); + } + + /* Connection name/title */ + name_length = ws.rQshift32(); + fb_name = ws.rQshiftStr(name_length); + conf.onDesktopName(that, fb_name); + + if (conf.true_color && fb_name === "Intel(r) AMT KVM") + { + Util.Warn("Intel AMT KVM only support 8/16 bit depths. Disabling true color"); + conf.true_color = false; + } + + display.set_true_color(conf.true_color); + conf.onFBResize(that, fb_width, fb_height); + display.resize(fb_width, fb_height); + keyboard.grab(); + mouse.grab(); + + if (conf.true_color) { + fb_Bpp = 4; + fb_depth = 3; + } else { + fb_Bpp = 1; + fb_depth = 1; + } + + response = pixelFormat(); + response = response.concat(clientEncodings()); + response = response.concat(fbUpdateRequests()); + timing.fbu_rt_start = (new Date()).getTime(); + timing.pixels = 0; + ws.send(response); + + /* Start pushing/polling */ + setTimeout(checkEvents, conf.check_rate); + + if (conf.encrypt) { + updateState('normal', "Connected (encrypted) to: " + fb_name); + } else { + updateState('normal', "Connected (unencrypted) to: " + fb_name); + } + break; + } + //Util.Debug("<< init_msg"); +}; + + +/* Normal RFB/VNC server message handler */ +normal_msg = function() { + //Util.Debug(">> normal_msg"); + + var ret = true, msg_type, length, text, + c, first_colour, num_colours, red, green, blue; + + if (FBU.rects > 0) { + msg_type = 0; + } else { + msg_type = ws.rQshift8(); + } + switch (msg_type) { + case 0: // FramebufferUpdate + ret = framebufferUpdate(); // false means need more data + break; + case 1: // SetColourMapEntries + Util.Debug("SetColourMapEntries"); + ws.rQshift8(); // Padding + first_colour = ws.rQshift16(); // First colour + num_colours = ws.rQshift16(); + if (ws.rQwait("SetColourMapEntries", num_colours*6, 6)) { return false; } + + for (c=0; c < num_colours; c+=1) { + red = ws.rQshift16(); + //Util.Debug("red before: " + red); + red = parseInt(red / 256, 10); + //Util.Debug("red after: " + red); + green = parseInt(ws.rQshift16() / 256, 10); + blue = parseInt(ws.rQshift16() / 256, 10); + display.set_colourMap([blue, green, red], first_colour + c); + } + Util.Debug("colourMap: " + display.get_colourMap()); + Util.Info("Registered " + num_colours + " colourMap entries"); + //Util.Debug("colourMap: " + display.get_colourMap()); + break; + case 2: // Bell + Util.Debug("Bell"); + conf.onBell(that); + break; + case 3: // ServerCutText + Util.Debug("ServerCutText"); + if (ws.rQwait("ServerCutText header", 7, 1)) { return false; } + ws.rQshiftBytes(3); // Padding + length = ws.rQshift32(); + if (ws.rQwait("ServerCutText", length, 8)) { return false; } + + text = ws.rQshiftStr(length); + conf.clipboardReceive(that, text); // Obsolete + conf.onClipboard(that, text); + break; + default: + fail("Disconnected: illegal server message type " + msg_type); + Util.Debug("ws.rQslice(0,30):" + ws.rQslice(0,30)); + break; + } + //Util.Debug("<< normal_msg"); + return ret; +}; + +framebufferUpdate = function() { + var now, hdr, fbu_rt_diff, ret = true; + + if (FBU.rects === 0) { + //Util.Debug("New FBU: ws.rQslice(0,20): " + ws.rQslice(0,20)); + if (ws.rQwait("FBU header", 3)) { + ws.rQunshift8(0); // FBU msg_type + return false; + } + ws.rQshift8(); // padding + FBU.rects = ws.rQshift16(); + //Util.Debug("FramebufferUpdate, rects:" + FBU.rects); + FBU.bytes = 0; + timing.cur_fbu = 0; + if (timing.fbu_rt_start > 0) { + now = (new Date()).getTime(); + Util.Info("First FBU latency: " + (now - timing.fbu_rt_start)); + } + } + + while (FBU.rects > 0) { + if (rfb_state !== "normal") { + return false; + } + if (ws.rQwait("FBU", FBU.bytes)) { return false; } + if (FBU.bytes === 0) { + if (ws.rQwait("rect header", 12)) { return false; } + /* New FramebufferUpdate */ + + hdr = ws.rQshiftBytes(12); + FBU.x = (hdr[0] << 8) + hdr[1]; + FBU.y = (hdr[2] << 8) + hdr[3]; + FBU.width = (hdr[4] << 8) + hdr[5]; + FBU.height = (hdr[6] << 8) + hdr[7]; + FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) + + (hdr[10] << 8) + hdr[11], 10); + + conf.onFBUReceive(that, + {'x': FBU.x, 'y': FBU.y, + 'width': FBU.width, 'height': FBU.height, + 'encoding': FBU.encoding, + 'encodingName': encNames[FBU.encoding]}); + + if (encNames[FBU.encoding]) { + // Debug: + /* + var msg = "FramebufferUpdate rects:" + FBU.rects; + msg += " x: " + FBU.x + " y: " + FBU.y; + msg += " width: " + FBU.width + " height: " + FBU.height; + msg += " encoding:" + FBU.encoding; + msg += "(" + encNames[FBU.encoding] + ")"; + msg += ", ws.rQlen(): " + ws.rQlen(); + Util.Debug(msg); + */ + } else { + fail("Disconnected: unsupported encoding " + + FBU.encoding); + return false; + } + } + + timing.last_fbu = (new Date()).getTime(); + + ret = encHandlers[FBU.encoding](); + + now = (new Date()).getTime(); + timing.cur_fbu += (now - timing.last_fbu); + + if (ret) { + encStats[FBU.encoding][0] += 1; + encStats[FBU.encoding][1] += 1; + timing.pixels += FBU.width * FBU.height; + } + + if (timing.pixels >= (fb_width * fb_height)) { + if (((FBU.width === fb_width) && + (FBU.height === fb_height)) || + (timing.fbu_rt_start > 0)) { + timing.full_fbu_total += timing.cur_fbu; + timing.full_fbu_cnt += 1; + Util.Info("Timing of full FBU, cur: " + + timing.cur_fbu + ", total: " + + timing.full_fbu_total + ", cnt: " + + timing.full_fbu_cnt + ", avg: " + + (timing.full_fbu_total / + timing.full_fbu_cnt)); + } + if (timing.fbu_rt_start > 0) { + fbu_rt_diff = now - timing.fbu_rt_start; + timing.fbu_rt_total += fbu_rt_diff; + timing.fbu_rt_cnt += 1; + Util.Info("full FBU round-trip, cur: " + + fbu_rt_diff + ", total: " + + timing.fbu_rt_total + ", cnt: " + + timing.fbu_rt_cnt + ", avg: " + + (timing.fbu_rt_total / + timing.fbu_rt_cnt)); + timing.fbu_rt_start = 0; + } + } + if (! ret) { + return ret; // false ret means need more data + } + } + + conf.onFBUComplete(that, + {'x': FBU.x, 'y': FBU.y, + 'width': FBU.width, 'height': FBU.height, + 'encoding': FBU.encoding, + 'encodingName': encNames[FBU.encoding]}); + + return true; // We finished this FBU +}; + +// +// FramebufferUpdate encodings +// + +encHandlers.RAW = function display_raw() { + //Util.Debug(">> display_raw (" + ws.rQlen() + " bytes)"); + + var cur_y, cur_height; + + if (FBU.lines === 0) { + FBU.lines = FBU.height; + } + FBU.bytes = FBU.width * fb_Bpp; // At least a line + if (ws.rQwait("RAW", FBU.bytes)) { return false; } + cur_y = FBU.y + (FBU.height - FBU.lines); + cur_height = Math.min(FBU.lines, + Math.floor(ws.rQlen()/(FBU.width * fb_Bpp))); + display.blitImage(FBU.x, cur_y, FBU.width, cur_height, + ws.get_rQ(), ws.get_rQi()); + ws.rQshiftBytes(FBU.width * cur_height * fb_Bpp); + FBU.lines -= cur_height; + + if (FBU.lines > 0) { + FBU.bytes = FBU.width * fb_Bpp; // At least another line + } else { + FBU.rects -= 1; + FBU.bytes = 0; + } + //Util.Debug("<< display_raw (" + ws.rQlen() + " bytes)"); + return true; +}; + +encHandlers.COPYRECT = function display_copy_rect() { + //Util.Debug(">> display_copy_rect"); + + var old_x, old_y; + + FBU.bytes = 4; + if (ws.rQwait("COPYRECT", 4)) { return false; } + display.renderQ_push({ + 'type': 'copy', + 'old_x': ws.rQshift16(), + 'old_y': ws.rQshift16(), + 'x': FBU.x, + 'y': FBU.y, + 'width': FBU.width, + 'height': FBU.height}); + FBU.rects -= 1; + FBU.bytes = 0; + return true; +}; + +encHandlers.RRE = function display_rre() { + //Util.Debug(">> display_rre (" + ws.rQlen() + " bytes)"); + var color, x, y, width, height, chunk; + + if (FBU.subrects === 0) { + FBU.bytes = 4+fb_Bpp; + if (ws.rQwait("RRE", 4+fb_Bpp)) { return false; } + FBU.subrects = ws.rQshift32(); + color = ws.rQshiftBytes(fb_Bpp); // Background + display.fillRect(FBU.x, FBU.y, FBU.width, FBU.height, color); + } + while ((FBU.subrects > 0) && (ws.rQlen() >= (fb_Bpp + 8))) { + color = ws.rQshiftBytes(fb_Bpp); + x = ws.rQshift16(); + y = ws.rQshift16(); + width = ws.rQshift16(); + height = ws.rQshift16(); + display.fillRect(FBU.x + x, FBU.y + y, width, height, color); + FBU.subrects -= 1; + } + //Util.Debug(" display_rre: rects: " + FBU.rects + + // ", FBU.subrects: " + FBU.subrects); + + if (FBU.subrects > 0) { + chunk = Math.min(rre_chunk_sz, FBU.subrects); + FBU.bytes = (fb_Bpp + 8) * chunk; + } else { + FBU.rects -= 1; + FBU.bytes = 0; + } + //Util.Debug("<< display_rre, FBU.bytes: " + FBU.bytes); + return true; +}; + +encHandlers.HEXTILE = function display_hextile() { + //Util.Debug(">> display_hextile"); + var subencoding, subrects, color, cur_tile, + tile_x, x, w, tile_y, y, h, xy, s, sx, sy, wh, sw, sh, + rQ = ws.get_rQ(), rQi = ws.get_rQi(); + + if (FBU.tiles === 0) { + FBU.tiles_x = Math.ceil(FBU.width/16); + FBU.tiles_y = Math.ceil(FBU.height/16); + FBU.total_tiles = FBU.tiles_x * FBU.tiles_y; + FBU.tiles = FBU.total_tiles; + } + + /* FBU.bytes comes in as 1, ws.rQlen() at least 1 */ + while (FBU.tiles > 0) { + FBU.bytes = 1; + if (ws.rQwait("HEXTILE subencoding", FBU.bytes)) { return false; } + subencoding = rQ[rQi]; // Peek + if (subencoding > 30) { // Raw + fail("Disconnected: illegal hextile subencoding " + subencoding); + //Util.Debug("ws.rQslice(0,30):" + ws.rQslice(0,30)); + return false; + } + subrects = 0; + cur_tile = FBU.total_tiles - FBU.tiles; + tile_x = cur_tile % FBU.tiles_x; + tile_y = Math.floor(cur_tile / FBU.tiles_x); + x = FBU.x + tile_x * 16; + y = FBU.y + tile_y * 16; + w = Math.min(16, (FBU.x + FBU.width) - x); + h = Math.min(16, (FBU.y + FBU.height) - y); + + /* Figure out how much we are expecting */ + if (subencoding & 0x01) { // Raw + //Util.Debug(" Raw subencoding"); + FBU.bytes += w * h * fb_Bpp; + } else { + if (subencoding & 0x02) { // Background + FBU.bytes += fb_Bpp; + } + if (subencoding & 0x04) { // Foreground + FBU.bytes += fb_Bpp; + } + if (subencoding & 0x08) { // AnySubrects + FBU.bytes += 1; // Since we aren't shifting it off + if (ws.rQwait("hextile subrects header", FBU.bytes)) { return false; } + subrects = rQ[rQi + FBU.bytes-1]; // Peek + if (subencoding & 0x10) { // SubrectsColoured + FBU.bytes += subrects * (fb_Bpp + 2); + } else { + FBU.bytes += subrects * 2; + } + } + } + + /* + Util.Debug(" tile:" + cur_tile + "/" + (FBU.total_tiles - 1) + + " (" + tile_x + "," + tile_y + ")" + + " [" + x + "," + y + "]@" + w + "x" + h + + ", subenc:" + subencoding + + "(last: " + FBU.lastsubencoding + "), subrects:" + + subrects + + ", ws.rQlen():" + ws.rQlen() + ", FBU.bytes:" + FBU.bytes + + " last:" + ws.rQslice(FBU.bytes-10, FBU.bytes) + + " next:" + ws.rQslice(FBU.bytes-1, FBU.bytes+10)); + */ + if (ws.rQwait("hextile", FBU.bytes)) { return false; } + + /* We know the encoding and have a whole tile */ + FBU.subencoding = rQ[rQi]; + rQi += 1; + if (FBU.subencoding === 0) { + if (FBU.lastsubencoding & 0x01) { + /* Weird: ignore blanks after RAW */ + Util.Debug(" Ignoring blank after RAW"); + } else { + display.fillRect(x, y, w, h, FBU.background); + } + } else if (FBU.subencoding & 0x01) { // Raw + display.blitImage(x, y, w, h, rQ, rQi); + rQi += FBU.bytes - 1; + } else { + if (FBU.subencoding & 0x02) { // Background + FBU.background = rQ.slice(rQi, rQi + fb_Bpp); + rQi += fb_Bpp; + } + if (FBU.subencoding & 0x04) { // Foreground + FBU.foreground = rQ.slice(rQi, rQi + fb_Bpp); + rQi += fb_Bpp; + } + + display.startTile(x, y, w, h, FBU.background); + if (FBU.subencoding & 0x08) { // AnySubrects + subrects = rQ[rQi]; + rQi += 1; + for (s = 0; s < subrects; s += 1) { + if (FBU.subencoding & 0x10) { // SubrectsColoured + color = rQ.slice(rQi, rQi + fb_Bpp); + rQi += fb_Bpp; + } else { + color = FBU.foreground; + } + xy = rQ[rQi]; + rQi += 1; + sx = (xy >> 4); + sy = (xy & 0x0f); + + wh = rQ[rQi]; + rQi += 1; + sw = (wh >> 4) + 1; + sh = (wh & 0x0f) + 1; + + display.subTile(sx, sy, sw, sh, color); + } + } + display.finishTile(); + } + ws.set_rQi(rQi); + FBU.lastsubencoding = FBU.subencoding; + FBU.bytes = 0; + FBU.tiles -= 1; + } + + if (FBU.tiles === 0) { + FBU.rects -= 1; + } + + //Util.Debug("<< display_hextile"); + return true; +}; + + +// Get 'compact length' header and data size +getTightCLength = function (arr) { + var header = 1, data = 0; + data += arr[0] & 0x7f; + if (arr[0] & 0x80) { + header += 1; + data += (arr[1] & 0x7f) << 7; + if (arr[1] & 0x80) { + header += 1; + data += arr[2] << 14; + } + } + return [header, data]; +}; + +function display_tight(isTightPNG) { + //Util.Debug(">> display_tight"); + + if (fb_depth === 1) { + fail("Tight protocol handler only implements true color mode"); + } + + var ctl, cmode, clength, color, img, data; + var filterId = -1, resetStreams = 0, streamId = -1; + var rQ = ws.get_rQ(), rQi = ws.get_rQi(); + + FBU.bytes = 1; // compression-control byte + if (ws.rQwait("TIGHT compression-control", FBU.bytes)) { return false; } + + var checksum = function(data) { + var sum=0, i; + for (i=0; i<data.length;i++) { + sum += data[i]; + if (sum > 65536) sum -= 65536; + } + return sum; + } + + var decompress = function(data) { + for (var i=0; i<4; i++) { + if ((resetStreams >> i) & 1) { + FBU.zlibs[i].reset(); + Util.Info("Reset zlib stream " + i); + } + } + var uncompressed = FBU.zlibs[streamId].uncompress(data, 0); + if (uncompressed.status !== 0) { + Util.Error("Invalid data in zlib stream"); + } + //Util.Warn("Decompressed " + data.length + " to " + + // uncompressed.data.length + " checksums " + + // checksum(data) + ":" + checksum(uncompressed.data)); + + return uncompressed.data; + } + + var indexedToRGB = function (data, numColors, palette, width, height) { + // Convert indexed (palette based) image data to RGB + // TODO: reduce number of calculations inside loop + var dest = []; + var x, y, b, w, w1, dp, sp; + if (numColors === 2) { + w = Math.floor((width + 7) / 8); + w1 = Math.floor(width / 8); + for (y = 0; y < height; y++) { + for (x = 0; x < w1; x++) { + for (b = 7; b >= 0; b--) { + dp = (y*width + x*8 + 7-b) * 3; + sp = (data[y*w + x] >> b & 1) * 3; + dest[dp ] = palette[sp ]; + dest[dp+1] = palette[sp+1]; + dest[dp+2] = palette[sp+2]; + } + } + for (b = 7; b >= 8 - width % 8; b--) { + dp = (y*width + x*8 + 7-b) * 3; + sp = (data[y*w + x] >> b & 1) * 3; + dest[dp ] = palette[sp ]; + dest[dp+1] = palette[sp+1]; + dest[dp+2] = palette[sp+2]; + } + } + } else { + for (y = 0; y < height; y++) { + for (x = 0; x < width; x++) { + dp = (y*width + x) * 3; + sp = data[y*width + x] * 3; + dest[dp ] = palette[sp ]; + dest[dp+1] = palette[sp+1]; + dest[dp+2] = palette[sp+2]; + } + } + } + return dest; + }; + var handlePalette = function() { + var numColors = rQ[rQi + 2] + 1; + var paletteSize = numColors * fb_depth; + FBU.bytes += paletteSize; + if (ws.rQwait("TIGHT palette " + cmode, FBU.bytes)) { return false; } + + var bpp = (numColors <= 2) ? 1 : 8; + var rowSize = Math.floor((FBU.width * bpp + 7) / 8); + var raw = false; + if (rowSize * FBU.height < 12) { + raw = true; + clength = [0, rowSize * FBU.height]; + } else { + clength = getTightCLength(ws.rQslice(3 + paletteSize, + 3 + paletteSize + 3)); + } + FBU.bytes += clength[0] + clength[1]; + if (ws.rQwait("TIGHT " + cmode, FBU.bytes)) { return false; } + + // Shift ctl, filter id, num colors, palette entries, and clength off + ws.rQshiftBytes(3); + var palette = ws.rQshiftBytes(paletteSize); + ws.rQshiftBytes(clength[0]); + + if (raw) { + data = ws.rQshiftBytes(clength[1]); + } else { + data = decompress(ws.rQshiftBytes(clength[1])); + } + + // Convert indexed (palette based) image data to RGB + var rgb = indexedToRGB(data, numColors, palette, FBU.width, FBU.height); + + // Add it to the render queue + display.renderQ_push({ + 'type': 'blitRgb', + 'data': rgb, + 'x': FBU.x, + 'y': FBU.y, + 'width': FBU.width, + 'height': FBU.height}); + return true; + } + + var handleCopy = function() { + var raw = false; + var uncompressedSize = FBU.width * FBU.height * fb_depth; + if (uncompressedSize < 12) { + raw = true; + clength = [0, uncompressedSize]; + } else { + clength = getTightCLength(ws.rQslice(1, 4)); + } + FBU.bytes = 1 + clength[0] + clength[1]; + if (ws.rQwait("TIGHT " + cmode, FBU.bytes)) { return false; } + + // Shift ctl, clength off + ws.rQshiftBytes(1 + clength[0]); + + if (raw) { + data = ws.rQshiftBytes(clength[1]); + } else { + data = decompress(ws.rQshiftBytes(clength[1])); + } + + display.renderQ_push({ + 'type': 'blitRgb', + 'data': data, + 'x': FBU.x, + 'y': FBU.y, + 'width': FBU.width, + 'height': FBU.height}); + return true; + } + + ctl = ws.rQpeek8(); + + // Keep tight reset bits + resetStreams = ctl & 0xF; + + // Figure out filter + ctl = ctl >> 4; + streamId = ctl & 0x3; + + if (ctl === 0x08) cmode = "fill"; + else if (ctl === 0x09) cmode = "jpeg"; + else if (ctl === 0x0A) cmode = "png"; + else if (ctl & 0x04) cmode = "filter"; + else if (ctl < 0x04) cmode = "copy"; + else return fail("Illegal tight compression received, ctl: " + ctl); + + if (isTightPNG && (cmode === "filter" || cmode === "copy")) { + return fail("filter/copy received in tightPNG mode"); + } + + switch (cmode) { + // fill uses fb_depth because TPIXELs drop the padding byte + case "fill": FBU.bytes += fb_depth; break; // TPIXEL + case "jpeg": FBU.bytes += 3; break; // max clength + case "png": FBU.bytes += 3; break; // max clength + case "filter": FBU.bytes += 2; break; // filter id + num colors if palette + case "copy": break; + } + + if (ws.rQwait("TIGHT " + cmode, FBU.bytes)) { return false; } + + //Util.Debug(" ws.rQslice(0,20): " + ws.rQslice(0,20) + " (" + ws.rQlen() + ")"); + //Util.Debug(" cmode: " + cmode); + + // Determine FBU.bytes + switch (cmode) { + case "fill": + ws.rQshift8(); // shift off ctl + color = ws.rQshiftBytes(fb_depth); + display.renderQ_push({ + 'type': 'fill', + 'x': FBU.x, + 'y': FBU.y, + 'width': FBU.width, + 'height': FBU.height, + 'color': [color[2], color[1], color[0]] }); + break; + case "png": + case "jpeg": + clength = getTightCLength(ws.rQslice(1, 4)); + FBU.bytes = 1 + clength[0] + clength[1]; // ctl + clength size + jpeg-data + if (ws.rQwait("TIGHT " + cmode, FBU.bytes)) { return false; } + + // We have everything, render it + //Util.Debug(" jpeg, ws.rQlen(): " + ws.rQlen() + ", clength[0]: " + + // clength[0] + ", clength[1]: " + clength[1]); + ws.rQshiftBytes(1 + clength[0]); // shift off ctl + compact length + img = new Image(); + img.src = "data:image/" + cmode + + extract_data_uri(ws.rQshiftBytes(clength[1])); + display.renderQ_push({ + 'type': 'img', + 'img': img, + 'x': FBU.x, + 'y': FBU.y}); + img = null; + break; + case "filter": + filterId = rQ[rQi + 1]; + if (filterId === 1) { + if (!handlePalette()) { return false; } + } else { + // Filter 0, Copy could be valid here, but servers don't send it as an explicit filter + // Filter 2, Gradient is valid but not used if jpeg is enabled + throw("Unsupported tight subencoding received, filter: " + filterId); + } + break; + case "copy": + if (!handleCopy()) { return false; } + break; + } + + FBU.bytes = 0; + FBU.rects -= 1; + //Util.Debug(" ending ws.rQslice(0,20): " + ws.rQslice(0,20) + " (" + ws.rQlen() + ")"); + //Util.Debug("<< display_tight_png"); + return true; +} + +extract_data_uri = function(arr) { + //var i, stra = []; + //for (i=0; i< arr.length; i += 1) { + // stra.push(String.fromCharCode(arr[i])); + //} + //return "," + escape(stra.join('')); + return ";base64," + Base64.encode(arr); +}; + +encHandlers.TIGHT = function () { return display_tight(false); }; +encHandlers.TIGHT_PNG = function () { return display_tight(true); }; + +encHandlers.last_rect = function last_rect() { + //Util.Debug(">> last_rect"); + FBU.rects = 0; + //Util.Debug("<< last_rect"); + return true; +}; + +encHandlers.DesktopSize = function set_desktopsize() { + Util.Debug(">> set_desktopsize"); + fb_width = FBU.width; + fb_height = FBU.height; + conf.onFBResize(that, fb_width, fb_height); + display.resize(fb_width, fb_height); + timing.fbu_rt_start = (new Date()).getTime(); + // Send a new non-incremental request + ws.send(fbUpdateRequests()); + + FBU.bytes = 0; + FBU.rects -= 1; + + Util.Debug("<< set_desktopsize"); + return true; +}; + +encHandlers.Cursor = function set_cursor() { + var x, y, w, h, pixelslength, masklength; + Util.Debug(">> set_cursor"); + x = FBU.x; // hotspot-x + y = FBU.y; // hotspot-y + w = FBU.width; + h = FBU.height; + + pixelslength = w * h * fb_Bpp; + masklength = Math.floor((w + 7) / 8) * h; + + FBU.bytes = pixelslength + masklength; + if (ws.rQwait("cursor encoding", FBU.bytes)) { return false; } + + //Util.Debug(" set_cursor, x: " + x + ", y: " + y + ", w: " + w + ", h: " + h); + + display.changeCursor(ws.rQshiftBytes(pixelslength), + ws.rQshiftBytes(masklength), + x, y, w, h); + + FBU.bytes = 0; + FBU.rects -= 1; + + Util.Debug("<< set_cursor"); + return true; +}; + +encHandlers.JPEG_quality_lo = function set_jpeg_quality() { + Util.Error("Server sent jpeg_quality pseudo-encoding"); +}; + +encHandlers.compress_lo = function set_compress_level() { + Util.Error("Server sent compress level pseudo-encoding"); +}; + +/* + * Client message routines + */ + +pixelFormat = function() { + //Util.Debug(">> pixelFormat"); + var arr; + arr = [0]; // msg-type + arr.push8(0); // padding + arr.push8(0); // padding + arr.push8(0); // padding + + arr.push8(fb_Bpp * 8); // bits-per-pixel + arr.push8(fb_depth * 8); // depth + arr.push8(0); // little-endian + arr.push8(conf.true_color ? 1 : 0); // true-color + + arr.push16(255); // red-max + arr.push16(255); // green-max + arr.push16(255); // blue-max + arr.push8(16); // red-shift + arr.push8(8); // green-shift + arr.push8(0); // blue-shift + + arr.push8(0); // padding + arr.push8(0); // padding + arr.push8(0); // padding + //Util.Debug("<< pixelFormat"); + return arr; +}; + +clientEncodings = function() { + //Util.Debug(">> clientEncodings"); + var arr, i, encList = []; + + for (i=0; i<encodings.length; i += 1) { + if ((encodings[i][0] === "Cursor") && + (! conf.local_cursor)) { + Util.Debug("Skipping Cursor pseudo-encoding"); + + // TODO: remove this when we have tight+non-true-color + } else if ((encodings[i][0] === "TIGHT") && + (! conf.true_color)) { + Util.Warn("Skipping tight, only support with true color"); + } else { + //Util.Debug("Adding encoding: " + encodings[i][0]); + encList.push(encodings[i][1]); + } + } + + arr = [2]; // msg-type + arr.push8(0); // padding + + arr.push16(encList.length); // encoding count + for (i=0; i < encList.length; i += 1) { + arr.push32(encList[i]); + } + //Util.Debug("<< clientEncodings: " + arr); + return arr; +}; + +fbUpdateRequest = function(incremental, x, y, xw, yw) { + //Util.Debug(">> fbUpdateRequest"); + if (typeof(x) === "undefined") { x = 0; } + if (typeof(y) === "undefined") { y = 0; } + if (typeof(xw) === "undefined") { xw = fb_width; } + if (typeof(yw) === "undefined") { yw = fb_height; } + var arr; + arr = [3]; // msg-type + arr.push8(incremental); + arr.push16(x); + arr.push16(y); + arr.push16(xw); + arr.push16(yw); + //Util.Debug("<< fbUpdateRequest"); + return arr; +}; + +// Based on clean/dirty areas, generate requests to send +fbUpdateRequests = function() { + var cleanDirty = display.getCleanDirtyReset(), + arr = [], i, cb, db; + + cb = cleanDirty.cleanBox; + if (cb.w > 0 && cb.h > 0) { + // Request incremental for clean box + arr = arr.concat(fbUpdateRequest(1, cb.x, cb.y, cb.w, cb.h)); + } + for (i = 0; i < cleanDirty.dirtyBoxes.length; i++) { + db = cleanDirty.dirtyBoxes[i]; + // Force all (non-incremental for dirty box + arr = arr.concat(fbUpdateRequest(0, db.x, db.y, db.w, db.h)); + } + return arr; +}; + + + +keyEvent = function(keysym, down) { + //Util.Debug(">> keyEvent, keysym: " + keysym + ", down: " + down); + var arr; + //alert(keysym); + arr = [4]; // msg-type + arr.push8(down); + arr.push16(0); + arr.push32(keysym); + //Util.Debug("<< keyEvent"); + return arr; +}; + +pointerEvent = function(x, y) { + //Util.Debug(">> pointerEvent, x,y: " + x + "," + y + + // " , mask: " + mouse_buttonMask); + var arr; + arr = [5]; // msg-type + arr.push8(mouse_buttonMask); + arr.push16(x); + arr.push16(y); + //Util.Debug("<< pointerEvent"); + return arr; +}; + +clientCutText = function(text) { + //Util.Debug(">> clientCutText"); + var arr, i, n; + arr = [6]; // msg-type + arr.push8(0); // padding + arr.push8(0); // padding + arr.push8(0); // padding + arr.push32(text.length); + n = text.length; + for (i=0; i < n; i+=1) { + arr.push(text.charCodeAt(i)); + } + //Util.Debug("<< clientCutText:" + arr); + return arr; +}; + + + +// +// Public API interface functions +// + +that.connect = function(host, port, password, path) { + //Util.Debug(">> connect"); + + rfb_host = host; + rfb_port = port; + rfb_password = (password !== undefined) ? password : ""; + rfb_path = (path !== undefined) ? path : ""; + + if ((!rfb_host) || (!rfb_port)) { + return fail("Must set host and port"); + } + updateState('connect'); + //Util.Debug("<< connect"); + +}; + +that.disconnect = function() { + //Util.Debug(">> disconnect"); + updateState('disconnect', 'Disconnecting'); + //Util.Debug("<< disconnect"); +}; + +that.sendPassword = function(passwd) { + rfb_password = passwd; + rfb_state = "Authentication"; + setTimeout(init_msg, 1); +}; + +that.sendCtrl = function() { + if (rfb_state !== "normal" || conf.view_only) { return false; } + Util.Info("Sending Ctrl"); + var arr = []; + if (document.getElementById('noVNC_ctrl_box').checked) { + + arr = arr.concat(keyEvent(0xFFE3, 1)); // Control + } + else{ + //arr = arr.concat(keyEvent(0xFFE9, 1)); // Alt + //arr = arr.concat(keyEvent(0xFFE9, 0)); // Alt + arr = arr.concat(keyEvent(0xFFE3, 0)); // Control + } + arr = arr.concat(fbUpdateRequests()); + ws.send(arr); +}; + + + +that.toggleCtrl = function(){ + alert('skata'); +} + +that.sendAlt = function() { + if (rfb_state !== "normal" || conf.view_only) { return false; } + Util.Info("Sending Alt"); + var arr = []; + if (document.getElementById('noVNC_alt_box').checked) { + + arr = arr.concat(keyEvent(0xFFE9, 1)); // Alt + } + else{ + arr = arr.concat(keyEvent(0xFFE9, 0)); // Alt + //arr = arr.concat(keyEvent(0xFFE3, 0)); // Control + } + arr = arr.concat(fbUpdateRequests()); + ws.send(arr); +}; + +that.sendEnter = function() { + if (rfb_state !== "normal" || conf.view_only) { return false; } + Util.Info("Sending Enter"); + var arr = []; + + arr = arr.concat(keyEvent(0xFF0D, 1)); // Alt + arr = arr.concat(keyEvent(0xFF0D, 0)); // Alt + //arr = arr.concat(keyEvent(0xFFE3, 0)); // Control + arr = arr.concat(fbUpdateRequests()); + ws.send(arr); +}; + + +that.sendShift = function() { + if (rfb_state !== "normal" || conf.view_only) { return false; } + Util.Info("Sending Shift"); + var arr = []; + if (document.getElementById('noVNC_shift_box').checked) { + Util.Info("Sending Shift down (checked)"); + arr = arr.concat(keyEvent(0xFFE1, 1)); // Shift + } + else{ + Util.Info("Sending Shift up (unchecked)"); + arr = arr.concat(keyEvent(0xFFE1, 0)); // Shift + //arr = arr.concat(keyEvent(0xFFE3, 0)); // Control + } + arr = arr.concat(fbUpdateRequests()); + ws.send(arr); +}; + + +that.sendCtrlAltDel = function() { + if (rfb_state !== "normal" || conf.view_only) { return false; } + Util.Info("Sending Ctrl-Alt-Del"); + var arr = []; + arr = arr.concat(keyEvent(0xFFE3, 1)); // Control + arr = arr.concat(keyEvent(0xFFE9, 1)); // Alt + arr = arr.concat(keyEvent(0xFFFF, 1)); // Delete + arr = arr.concat(keyEvent(0xFFFF, 0)); // Delete + arr = arr.concat(keyEvent(0xFFE9, 0)); // Alt + arr = arr.concat(keyEvent(0xFFE3, 0)); // Control + arr = arr.concat(fbUpdateRequests()); + ws.send(arr); +}; + +// Send a key press. If 'down' is not specified then send a down key +// followed by an up key. +that.sendKey = function(code, down) { + if (rfb_state !== "normal" || conf.view_only) { return false; } + var arr = []; + if (typeof down !== 'undefined') { + Util.Info("Sending key code (" + (down ? "down" : "up") + "): " + code); + arr = arr.concat(keyEvent(code, down ? 1 : 0)); + } else { + Util.Info("Sending key code (down + up): " + code); + arr = arr.concat(keyEvent(code, 1)); + arr = arr.concat(keyEvent(code, 0)); + } + arr = arr.concat(fbUpdateRequests()); + ws.send(arr); +}; + +that.clipboardPasteFrom = function(text) { + if (rfb_state !== "normal") { return; } + //Util.Debug(">> clipboardPasteFrom: " + text.substr(0,40) + "..."); + var arr = []; + var n,i; + n=text.length; + for (i=0; i < n; i+=1) { + keyPress(text.charCodeAt(i),1); + keyPress(text.charCodeAt(i),0); + //alert(" i = "+i+" val = "+text.charCodeAt(i)); + //alert('0x00'+text[i].charCodeAt(0).toString(16).toUpperCase()); + //alert(text.charCodeAt(i)); + //arr.concat(keyEvent(text.charCodeAt(i),1)); + //arr.concat(keyEvent(text.charCodeAt(i),0)); + //arr.concat(keyEvent('0x00'+text.charCodeAt(i).toString(16).toUpperCase(),1)); + //arr.concat(keyEvent('0x00'+text.charCodeAt(i).toString(16).toUpperCase(),0)); + //arr = arr.concat(fbUpdateRequests()); + //ws.send(arr); + //arr = []; + + } + //arr = arr.concat(keyEvent(0x0061, 1)); // Alt + //arr = arr.concat(keyEvent(0x0061, 0)); // Alt + //arr = arr.concat(keyEvent(0xFFE3, 0)); // Control + //arr = arr.concat(fbUpdateRequests()); + //ws.send(arr); + + //ws.send(clientCutText(text)); + //Util.Debug("<< clipboardPasteFrom"); +}; + +// Override internal functions for testing +that.testMode = function(override_send, data_mode) { + test_mode = true; + that.recv_message = ws.testMode(override_send, data_mode); + + checkEvents = function () { /* Stub Out */ }; + that.connect = function(host, port, password) { + rfb_host = host; + rfb_port = port; + rfb_password = password; + init_vars(); + updateState('ProtocolVersion', "Starting VNC handshake"); + }; +}; + + +return constructor(); // Return the public API interface + +} // End of RFB() diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/self.pem b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/self.pem new file mode 100755 index 0000000000000000000000000000000000000000..910740f974291736d5f26dcf85563c4b3558dbcf --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/self.pem @@ -0,0 +1,33 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICWwIBAAKBgQCoJCeswz+HoY3rKFgxlPMlmIYlxkvxQmoP30uMiFWX4gOdK7om +cPgfyk1hanME6ldmDTtQ3KqbpGcXgDsuKIgjfZbzgxNtSXL+QEPFu16700aNqMMW +ai5g+pZR/8AA2Y9lgq1GeJXFSaPxKyZQc+yY4Rqm7AgC2uebwx3+LT50BQIDAQAB +AoGAXnz64sZSREkQdM8WSL64qS7+a+n0sV6uNb85OH9BAnpbp450LLgdZ9gLBiyI ++IEsnkffRoDLS23nFTjViQnz/nwpPt710bmMScgp3mtbA/zu31E8fxbnvigK/hea +Pfiory8aMzZEeWTFUWuEKj55elURo2AfmUprVEqHoytNgf0CQQDc0nwTtSSuav2R +3CN48keoHGci5TKDPhV9nqYirUMbeXD0TgkJPCz93sYP08y4P7F4BkEc7JCXK1nB +FJOihEc3AkEAwu0/gzAYmygCin+HB9njfWydV+MLr+T99vNG//G7z5Htomb7M8EA +QQv2pDK+NvmW1k04KcUVbSNw73iWGPnEowJBAMImWislp/OmY/2bdKDBPBllp5R5 +ubjEnDaPh3iTp53/Xz2dYrp46wHmnXOK/8K7VXi23wbkQ5h15/sn8UoBTW8CQHcr +2u+WgQSiwmLwMpqvMHCm7c8khSmlY0sOUrL5lCwD+HeYZC2w6jnaWZDrYPV1RC2C +ijqnPkE6MLqHS6S7VucCP3XHDYjsf0dizLwWLhPXIAarKu9jTbFF398KtEbNoBcG +FFP6duVEhn8klQwkrrZqfw5YpAO5RWi21mHO69QQUw== +-----END RSA PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIC5DCCAk2gAwIBAgIJAOj/9G4BevjJMA0GCSqGSIb3DQEBBQUAMFYxCzAJBgNV +BAYTAkdSMRMwEQYDVQQIEwpTb21lLVN0YXRlMQ8wDQYDVQQHEwZBdGhlbnMxITAf +BgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0xMzA4MjMwOTEyMjla +Fw0xNDA4MjMwOTEyMjlaMFYxCzAJBgNVBAYTAkdSMRMwEQYDVQQIEwpTb21lLVN0 +YXRlMQ8wDQYDVQQHEwZBdGhlbnMxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMg +UHR5IEx0ZDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAqCQnrMM/h6GN6yhY +MZTzJZiGJcZL8UJqD99LjIhVl+IDnSu6JnD4H8pNYWpzBOpXZg07UNyqm6RnF4A7 +LiiII32W84MTbUly/kBDxbteu9NGjajDFmouYPqWUf/AANmPZYKtRniVxUmj8Ssm +UHPsmOEapuwIAtrnm8Md/i0+dAUCAwEAAaOBuTCBtjAdBgNVHQ4EFgQUC+ezzx9Z +5hz75/RJQJXMoPuuF30wgYYGA1UdIwR/MH2AFAvns88fWeYc++f0SUCVzKD7rhd9 +oVqkWDBWMQswCQYDVQQGEwJHUjETMBEGA1UECBMKU29tZS1TdGF0ZTEPMA0GA1UE +BxMGQXRoZW5zMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQDo +//RuAXr4yTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4GBADYcvbjjxe3s +Lv56Ha+u1Vx5P/EoPMeMGMeJYPLRvopXV140AxYqYzRvVmGhEw8RatoBtlIt2DiU +Qd3XmaFBHgejU9A1nuiXtTGbUOC24hnoD2to/2NnyViHoItiKVD7Fizr8KFNH0Fr +PU3ptDwoMw1IgKC1IGpXCb/6XNx64Pzd +-----END CERTIFICATE----- diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/ui.js b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/ui.js new file mode 100644 index 0000000000000000000000000000000000000000..02704215af4fed3c8e357960f9e5eabb2253ad56 --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/ui.js @@ -0,0 +1,839 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2012 Joel Martin + * Copyright (C) 2013 Samuel Mannehed for Cendio AB + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +"use strict"; +/*jslint white: false, browser: true */ +/*global window, $D, Util, WebUtil, RFB, Display */ + +// Load supporting scripts +//window.onscriptsload = function () { UI.load(); }; + +//Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js", +// "input.js", "display.js", "jsunzip.js", "rfb.js"]); + +var UI = { + +rfb_state : 'loaded', +settingsOpen : false, +connSettingsOpen : false, +popupStatusOpen : false, +clipboardOpen: true, +keyboardVisible: true, + +// Setup rfb object, load settings from browser storage, then call +// UI.init to setup the UI/menus +load: function (callback) { + WebUtil.initSettings(UI.start, callback); +}, + +// Render default UI and initialize settings menu +start: function(callback) { + var html = '', i, sheet, sheets, llevels, port; + + // Stylesheet selection dropdown + sheet = WebUtil.selectStylesheet(); + sheets = WebUtil.getStylesheets(); + for (i = 0; i < sheets.length; i += 1) { + UI.addOption($D('noVNC_stylesheet'),sheets[i].title, sheets[i].title); + } + + // Logging selection dropdown + llevels = ['error', 'warn', 'info', 'debug']; + for (i = 0; i < llevels.length; i += 1) { + UI.addOption($D('noVNC_logging'),llevels[i], llevels[i]); + } + + // Settings with immediate effects + UI.initSetting('logging', 'warn'); + WebUtil.init_logging(UI.getSetting('logging')); + + UI.initSetting('stylesheet', 'default'); + WebUtil.selectStylesheet(null); + // call twice to get around webkit bug + WebUtil.selectStylesheet(UI.getSetting('stylesheet')); + + // if port == 80 (or 443) then it won't be present and should be + // set manually + port = window.location.port; + if (!port) { + if (window.location.protocol.substring(0,5) == 'https') { + port = 443; + } + else if (window.location.protocol.substring(0,4) == 'http') { + port = 80; + } + } + + /* Populate the controls if defaults are provided in the URL */ + UI.initSetting('host', window.location.hostname); + UI.initSetting('port', port); + UI.initSetting('password', ''); + UI.initSetting('encrypt', (window.location.protocol === "https:")); + UI.initSetting('true_color', true); + UI.initSetting('cursor', false); + UI.initSetting('shared', true); + UI.initSetting('view_only', false); + UI.initSetting('connectTimeout', 2); + UI.initSetting('path', 'websockify'); + UI.initSetting('repeaterID', ''); + + UI.rfb = RFB({'target': $D('noVNC_canvas'), + 'onUpdateState': UI.updateState, + 'onClipboard': UI.clipReceive, + 'onDesktopName': UI.updateDocumentTitle}); + UI.updateVisualState(); + + // Unfocus clipboard when over the VNC area + //$D('VNC_screen').onmousemove = function () { + // var keyboard = UI.rfb.get_keyboard(); + // if ((! keyboard) || (! keyboard.get_focused())) { + // $D('VNC_clipboard_text').blur(); + // } + // }; + + // Show mouse selector buttons on touch screen devices + if ('ontouchstart' in document.documentElement) { + // Show mobile buttons + $D('noVNC_mobile_buttons').style.display = "inline"; + UI.setMouseButton(); + // Remove the address bar + setTimeout(function() { window.scrollTo(0, 1); }, 100); + UI.forceSetting('clip', true); + $D('noVNC_clip').disabled = true; + } else { + UI.initSetting('clip', false); + } + + //iOS Safari does not support CSS position:fixed. + //This detects iOS devices and enables javascript workaround. + if ((navigator.userAgent.match(/iPhone/i)) || + (navigator.userAgent.match(/iPod/i)) || + (navigator.userAgent.match(/iPad/i))) { + //UI.setOnscroll(); + //UI.setResize(); + } + UI.setBarPosition(); + + $D('noVNC_host').focus(); + + UI.setViewClip(); + Util.addEvent(window, 'resize', UI.setViewClip); + + Util.addEvent(window, 'beforeunload', function () { + if (UI.rfb_state === 'normal') { + return "You are currently connected."; + } + } ); + + // Show description by default when hosted at for kanaka.github.com + if (location.host === "kanaka.github.com") { + // Open the description dialog + $D('noVNC_description').style.display = "block"; + } else { + // Open the connect panel on first load + UI.toggleConnectPanel(); + } + + // Add mouse event click/focus/blur event handlers to the UI + UI.addMouseHandlers(); + + if (typeof callback === "function") { + callback(UI.rfb); + } +}, + +addMouseHandlers: function() { + // Setup interface handlers that can't be inline + $D("noVNC_view_drag_button").onclick = UI.setViewDrag; + $D("noVNC_mouse_button0").onclick = function () { UI.setMouseButton(1); }; + $D("noVNC_mouse_button1").onclick = function () { UI.setMouseButton(2); }; + $D("noVNC_mouse_button2").onclick = function () { UI.setMouseButton(4); }; + $D("noVNC_mouse_button4").onclick = function () { UI.setMouseButton(0); }; + $D("showKeyboard").onclick = UI.showKeyboard; + //$D("keyboardinput").onkeydown = function (event) { onKeyDown(event); }; + $D("keyboardinput").onblur = UI.keyInputBlur; + + $D("keyboardText").onfocus = UI.virtKeyText; + $D("sendCtrlAltDelButton").onclick = UI.sendCtrlAltDel; + $D("sendEnterButton").onclick = UI.sendEnter; + $D("noVNC_ctrl_box").onclick = UI.sendCtrl; + $D("noVNC_alt_box").onclick = UI.sendAlt; + //$D("noVNC_shift_box").onclick = UI.sendShift; + + $D("toggleCtrlButton").onclick = UI.toggleCtrl; + $D("toggleAltButton").onclick = UI.toggleAlt; + + $D("noVNC_status").onclick = UI.togglePopupStatusPanel; + $D("noVNC_popup_status_panel").onclick = UI.togglePopupStatusPanel; + $D("clipboardButton").onclick = UI.toggleClipboardPanel; + $D("settingsButton").onclick = UI.toggleSettingsPanel; + $D("connectButton").onclick = UI.toggleConnectPanel; + $D("disconnectButton").onclick = UI.disconnect; + $D("descriptionButton").onclick = UI.toggleConnectPanel; + + $D("noVNC_clipboard_text").onfocus = UI.displayBlur; + $D("noVNC_clipboard_text").onblur = UI.displayFocus; + $D("noVNC_clipboard_text").onchange = UI.clipSend; + $D("noVNC_clipboard_clear_button").onclick = UI.clipClear; + + $D("noVNC_settings_menu").onmouseover = UI.displayBlur; + $D("noVNC_settings_menu").onmouseover = UI.displayFocus; + $D("noVNC_apply").onclick = UI.settingsApply; + + $D("noVNC_connect_button").onclick = UI.connect; +}, + +// Read form control compatible setting from cookie +getSetting: function(name) { + var val, ctrl = $D('noVNC_' + name); + val = WebUtil.readSetting(name); + if (val !== null && ctrl.type === 'checkbox') { + if (val.toString().toLowerCase() in {'0':1, 'no':1, 'false':1}) { + val = false; + } else { + val = true; + } + } + return val; +}, + +// Update cookie and form control setting. If value is not set, then +// updates from control to current cookie setting. +updateSetting: function(name, value) { + + var i, ctrl = $D('noVNC_' + name); + // Save the cookie for this session + if (typeof value !== 'undefined') { + WebUtil.writeSetting(name, value); + } + + // Update the settings control + value = UI.getSetting(name); + + if (ctrl.type === 'checkbox') { + ctrl.checked = value; + + } else if (typeof ctrl.options !== 'undefined') { + for (i = 0; i < ctrl.options.length; i += 1) { + if (ctrl.options[i].value === value) { + ctrl.selectedIndex = i; + break; + } + } + } else { + /*Weird IE9 error leads to 'null' appearring + in textboxes instead of ''.*/ + if (value === null) { + value = ""; + } + ctrl.value = value; + } +}, + +// Save control setting to cookie +saveSetting: function(name) { + var val, ctrl = $D('noVNC_' + name); + if (ctrl.type === 'checkbox') { + val = ctrl.checked; + } else if (typeof ctrl.options !== 'undefined') { + val = ctrl.options[ctrl.selectedIndex].value; + } else { + val = ctrl.value; + } + WebUtil.writeSetting(name, val); + //Util.Debug("Setting saved '" + name + "=" + val + "'"); + return val; +}, + +// Initial page load read/initialization of settings +initSetting: function(name, defVal) { + var val; + + // Check Query string followed by cookie + val = WebUtil.getQueryVar(name); + if (val === null) { + val = WebUtil.readSetting(name, defVal); + } + UI.updateSetting(name, val); + //Util.Debug("Setting '" + name + "' initialized to '" + val + "'"); + return val; +}, + +// Force a setting to be a certain value +forceSetting: function(name, val) { + UI.updateSetting(name, val); + return val; +}, + + +// Show the popup status panel +togglePopupStatusPanel: function() { + var psp = $D('noVNC_popup_status_panel'); + if (UI.popupStatusOpen === true) { + psp.style.display = "none"; + UI.popupStatusOpen = false; + } else { + psp.innerHTML = $D('noVNC_status').innerHTML; + psp.style.display = "block"; + psp.style.left = window.innerWidth/2 - + parseInt(window.getComputedStyle(psp, false).width)/2 -30 + "px"; + UI.popupStatusOpen = true; + } +}, + +// Show the clipboard panel +toggleClipboardPanel: function() { + // Close the description panel + $D('noVNC_description').style.display = "none"; + // Close settings if open + if (UI.settingsOpen === true) { + UI.settingsApply(); + UI.closeSettingsMenu(); + } + // Close connection settings if open + if (UI.connSettingsOpen === true) { + UI.toggleConnectPanel(); + } + // Close popup status panel if open + if (UI.popupStatusOpen === true) { + UI.togglePopupStatusPanel(); + } + // Toggle Clipboard Panel + if (UI.clipboardOpen === true) { + $D('noVNC_clipboard').style.display = "none"; + $D('clipboardButton').className = "noVNC_status_button"; + UI.clipboardOpen = false; + } else { + $D('noVNC_clipboard').style.display = "block"; + $D('clipboardButton').className = "noVNC_status_button_selected"; + UI.clipboardOpen = true; + } +}, + +// Show the connection settings panel/menu +toggleConnectPanel: function() { + // Close the description panel + $D('noVNC_description').style.display = "none"; + // Close connection settings if open + if (UI.settingsOpen === true) { + UI.settingsApply(); + UI.closeSettingsMenu(); + $D('connectButton').className = "noVNC_status_button"; + } + // Close clipboard panel if open + if (UI.clipboardOpen === true) { + UI.toggleClipboardPanel(); + } + // Close popup status panel if open + if (UI.popupStatusOpen === true) { + UI.togglePopupStatusPanel(); + } + + // Toggle Connection Panel + if (UI.connSettingsOpen === true) { + $D('noVNC_controls').style.display = "none"; + $D('connectButton').className = "noVNC_status_button"; + UI.connSettingsOpen = false; + UI.saveSetting('host'); + UI.saveSetting('port'); + //UI.saveSetting('password'); + } else { + $D('noVNC_controls').style.display = "block"; + $D('connectButton').className = "noVNC_status_button_selected"; + UI.connSettingsOpen = true; + $D('noVNC_host').focus(); + } +}, + +// Toggle the settings menu: +// On open, settings are refreshed from saved cookies. +// On close, settings are applied +toggleSettingsPanel: function() { + // Close the description panel + $D('noVNC_description').style.display = "none"; + if (UI.settingsOpen) { + UI.settingsApply(); + UI.closeSettingsMenu(); + } else { + UI.updateSetting('encrypt'); + UI.updateSetting('true_color'); + if (UI.rfb.get_display().get_cursor_uri()) { + UI.updateSetting('cursor'); + } else { + UI.updateSetting('cursor', false); + $D('noVNC_cursor').disabled = true; + } + UI.updateSetting('clip'); + UI.updateSetting('shared'); + UI.updateSetting('view_only'); + UI.updateSetting('connectTimeout'); + UI.updateSetting('path'); + UI.updateSetting('repeaterID'); + UI.updateSetting('stylesheet'); + UI.updateSetting('logging'); + + UI.openSettingsMenu(); + } +}, + +// Open menu +openSettingsMenu: function() { + // Close the description panel + $D('noVNC_description').style.display = "none"; + // Close clipboard panel if open + if (UI.clipboardOpen === true) { + UI.toggleClipboardPanel(); + } + // Close connection settings if open + if (UI.connSettingsOpen === true) { + UI.toggleConnectPanel(); + } + // Close popup status panel if open + if (UI.popupStatusOpen === true) { + UI.togglePopupStatusPanel(); + } + $D('noVNC_settings').style.display = "block"; + $D('settingsButton').className = "noVNC_status_button_selected"; + UI.settingsOpen = true; +}, + +// Close menu (without applying settings) +closeSettingsMenu: function() { + $D('noVNC_settings').style.display = "none"; + $D('settingsButton').className = "noVNC_status_button"; + UI.settingsOpen = false; +}, + +// Save/apply settings when 'Apply' button is pressed +settingsApply: function() { + //Util.Debug(">> settingsApply"); + UI.saveSetting('encrypt'); + UI.saveSetting('true_color'); + if (UI.rfb.get_display().get_cursor_uri()) { + UI.saveSetting('cursor'); + } + UI.saveSetting('clip'); + UI.saveSetting('shared'); + UI.saveSetting('view_only'); + UI.saveSetting('connectTimeout'); + UI.saveSetting('path'); + UI.saveSetting('repeaterID'); + UI.saveSetting('stylesheet'); + UI.saveSetting('logging'); + + // Settings with immediate (non-connected related) effect + WebUtil.selectStylesheet(UI.getSetting('stylesheet')); + WebUtil.init_logging(UI.getSetting('logging')); + UI.setViewClip(); + UI.setViewDrag(UI.rfb.get_viewportDrag()); + //Util.Debug("<< settingsApply"); +}, + + + +setPassword: function() { + UI.rfb.sendPassword($D('noVNC_password').value); + //Reset connect button. + $D('noVNC_connect_button').value = "Connect"; + $D('noVNC_connect_button').onclick = UI.Connect; + //Hide connection panel. + UI.toggleConnectPanel(); + return false; +}, + +virtKeyText: function(){ + var text = $D('keyboardText').value; + Util.Debug("$$$$$$ the text i get is : " + text); + Util.Debug(">> UI.keyboardText: " + text.substr(0,40) + "..."); + UI.rfb.clipboardPasteFrom(text); + Util.Debug("<< UI.keyboardText"); + //UI.rfb.virtKeyText(); + $D('keyboardText').value = ""; + //UI.rfb.clipboardPasteFrom(""); + +}, + +sendCtrlAltDel: function() { + + UI.rfb.sendCtrlAltDel(); +}, + +sendEnter: function() { + + UI.rfb.sendEnter(); +}, + +sendCtrl: function() { + UI.rfb.sendCtrl(); +}, + +toggleCtrl: function(){ + if (document.getElementById('noVNC_ctrl_box').checked) { + document.getElementById('noVNC_ctrl_box').checked = false; + $D('toggleCtrlButton').className = "noVNC_status_button"; + } else{ + document.getElementById('noVNC_ctrl_box').checked = true; + $D('toggleCtrlButton').className = "noVNC_status_button_selected"; + } + UI.rfb.sendCtrl(); +}, + +toggleAlt: function(){ + if (document.getElementById('noVNC_alt_box').checked) { + document.getElementById('noVNC_alt_box').checked = false; + $D('toggleAltButton').className = "noVNC_status_button"; + } else{ + document.getElementById('noVNC_alt_box').checked = true; + $D('toggleAltButton').className = "noVNC_status_button_selected"; + } + UI.rfb.sendAlt(); +}, + +sendAlt: function() { + UI.rfb.sendAlt(); +}, + +sendShift: function() { + UI.rfb.sendShift(); +}, + +setMouseButton: function(num) { + var b, blist = [0, 1,2,4], button; + + if (typeof num === 'undefined') { + // Disable mouse buttons + num = -1; + } + if (UI.rfb) { + UI.rfb.get_mouse().set_touchButton(num); + } + + for (b = 0; b < blist.length; b++) { + button = $D('noVNC_mouse_button' + blist[b]); + if (blist[b] === num) { + button.style.display = ""; + } else { + button.style.display = "none"; + /* + button.style.backgroundColor = "black"; + button.style.color = "lightgray"; + button.style.backgroundColor = ""; + button.style.color = ""; + */ + } + } +}, + +updateState: function(rfb, state, oldstate, msg) { + var s, sb, c, d, cad, vd, klass; + UI.rfb_state = state; + switch (state) { + case 'failed': + case 'fatal': + klass = "noVNC_status_error"; + break; + case 'normal': + klass = "noVNC_status_normal"; + break; + case 'disconnected': + $D('noVNC_logo').style.display = "block"; + // Fall through + case 'loaded': + klass = "noVNC_status_normal"; + break; + case 'password': + UI.toggleConnectPanel(); + + $D('noVNC_connect_button').value = "Send Password"; + $D('noVNC_connect_button').onclick = UI.setPassword; + $D('noVNC_password').focus(); + + klass = "noVNC_status_warn"; + break; + default: + klass = "noVNC_status_warn"; + break; + } + + if (typeof(msg) !== 'undefined') { + $D('noVNC-control-bar').setAttribute("class", klass); + $D('noVNC_status').innerHTML = msg; + } + + UI.updateVisualState(); +}, + +// Disable/enable controls depending on connection state +updateVisualState: function() { + var connected = UI.rfb_state === 'normal' ? true : false; + + //Util.Debug(">> updateVisualState"); + $D('noVNC_encrypt').disabled = connected; + $D('noVNC_true_color').disabled = connected; + if (UI.rfb && UI.rfb.get_display() && + UI.rfb.get_display().get_cursor_uri()) { + $D('noVNC_cursor').disabled = connected; + } else { + UI.updateSetting('cursor', false); + $D('noVNC_cursor').disabled = true; + } + $D('noVNC_shared').disabled = connected; + $D('noVNC_view_only').disabled = connected; + $D('noVNC_connectTimeout').disabled = connected; + $D('noVNC_path').disabled = connected; + $D('noVNC_repeaterID').disabled = connected; + + if (connected) { + UI.setViewClip(); + UI.setMouseButton(1); + $D('clipboardButton').style.display = "inline"; + $D('showKeyboard').style.display = "inline"; + $D('sendCtrlAltDelButton').style.display = "inline"; + $D('sendEnterButton').style.display = "inline"; + $D('noVNC_ctrl_box').style.display = "inline"; + $D('noVNC_alt_box').style.display = "inline"; + //$D('noVNC_shift_box').style.display = "inline"; + $D('ctrl_label').style.display = "inline"; + $D('alt_label').style.display = "inline"; + $D('keyboardText').style.display = "inline"; + //$D('shift_label').style.display = "inline"; + } else { + UI.setMouseButton(); + $D('clipboardButton').style.display = "none"; + $D('showKeyboard').style.display = "none"; + $D('sendCtrlAltDelButton').style.display = "none"; + $D('sendEnterButton').style.display = "none"; + $D('noVNC_ctrl_box').style.display = "none"; + $D('noVNC_alt_box').style.display = "none"; + //$D('noVNC_shift_box').style.display = "none"; + $D('ctrl_label').style.display = "none"; + $D('alt_label').style.display = "none"; + $D('keyboardText').style.display = "none"; + //$D('shift_label').style.display = "none"; + } + // State change disables viewport dragging. + // It is enabled (toggled) by direct click on the button + UI.setViewDrag(false); + + switch (UI.rfb_state) { + case 'fatal': + case 'failed': + case 'loaded': + case 'disconnected': + $D('connectButton').style.display = ""; + $D('disconnectButton').style.display = "none"; + break; + default: + $D('connectButton').style.display = "none"; + $D('disconnectButton').style.display = ""; + break; + } + + //Util.Debug("<< updateVisualState"); +}, + + +// Display the desktop name in the document title +updateDocumentTitle: function(rfb, name) { + document.title = name + " - noVNC"; +}, + + +clipReceive: function(rfb, text) { + Util.Debug(">> UI.clipReceive: " + text.substr(0,40) + "..."); + $D('noVNC_clipboard_text').value = text; + Util.Debug("<< UI.clipReceive"); +}, + + +connect: function() { + var host, port, password, path; + + UI.closeSettingsMenu(); + UI.toggleConnectPanel(); + + host = $D('noVNC_host').value; + port = $D('noVNC_port').value; + password = $D('noVNC_password').value; + path = $D('noVNC_path').value; + if ((!host) || (!port)) { + throw("Must set host and port"); + } + + UI.rfb.set_encrypt(UI.getSetting('encrypt')); + UI.rfb.set_true_color(UI.getSetting('true_color')); + UI.rfb.set_local_cursor(UI.getSetting('cursor')); + UI.rfb.set_shared(UI.getSetting('shared')); + UI.rfb.set_view_only(UI.getSetting('view_only')); + UI.rfb.set_connectTimeout(UI.getSetting('connectTimeout')); + UI.rfb.set_repeaterID(UI.getSetting('repeaterID')); + + UI.rfb.connect(host, port, password, path); + + //Close dialog. + setTimeout(UI.setBarPosition, 100); + $D('noVNC_logo').style.display = "none"; +}, + +disconnect: function() { + UI.closeSettingsMenu(); + UI.rfb.disconnect(); + + $D('noVNC_logo').style.display = "block"; + UI.connSettingsOpen = false; + UI.toggleConnectPanel(); +}, + +displayBlur: function() { + UI.rfb.get_keyboard().set_focused(false); + UI.rfb.get_mouse().set_focused(false); +}, + +displayFocus: function() { + UI.rfb.get_keyboard().set_focused(true); + UI.rfb.get_mouse().set_focused(true); +}, + +clipClear: function() { + $D('noVNC_clipboard_text').value = ""; + UI.rfb.clipboardPasteFrom(""); +}, + +clipSend: function() { + var text = $D('noVNC_clipboard_text').value; + Util.Debug(">> UI.clipSend: " + text.substr(0,40) + "..."); + UI.rfb.clipboardPasteFrom(text); + Util.Debug("<< UI.clipSend"); +}, + + +// Enable/disable and configure viewport clipping +setViewClip: function(clip) { + var display, cur_clip, pos, new_w, new_h; + + if (UI.rfb) { + display = UI.rfb.get_display(); + } else { + return; + } + + cur_clip = display.get_viewport(); + + if (typeof(clip) !== 'boolean') { + // Use current setting + clip = UI.getSetting('clip'); + } + + if (clip && !cur_clip) { + // Turn clipping on + UI.updateSetting('clip', true); + } else if (!clip && cur_clip) { + // Turn clipping off + UI.updateSetting('clip', false); + display.set_viewport(false); + $D('noVNC_canvas').style.position = 'static'; + display.viewportChange(); + } + if (UI.getSetting('clip')) { + // If clipping, update clipping settings + $D('noVNC_canvas').style.position = 'absolute'; + pos = Util.getPosition($D('noVNC_canvas')); + new_w = window.innerWidth - pos.x; + new_h = window.innerHeight - pos.y; + display.set_viewport(true); + display.viewportChange(0, 0, new_w, new_h); + } +}, + +// Toggle/set/unset the viewport drag/move button +setViewDrag: function(drag) { + var vmb = $D('noVNC_view_drag_button'); + if (!UI.rfb) { return; } + + if (UI.rfb_state === 'normal' && + UI.rfb.get_display().get_viewport()) { + vmb.style.display = "inline"; + } else { + vmb.style.display = "none"; + } + + if (typeof(drag) === "undefined" || + typeof(drag) === "object") { + // If not specified, then toggle + drag = !UI.rfb.get_viewportDrag(); + } + if (drag) { + vmb.className = "noVNC_status_button_selected"; + UI.rfb.set_viewportDrag(true); + } else { + vmb.className = "noVNC_status_button"; + UI.rfb.set_viewportDrag(false); + } +}, + +// On touch devices, show the OS keyboard +showKeyboard: function() { + if(UI.keyboardVisible === false) { + $D('keyboardinput').focus(); + UI.keyboardVisible = true; + $D('showKeyboard').className = "noVNC_status_button_selected"; + } else if(UI.keyboardVisible === true) { + $D('keyboardinput').blur(); + $D('showKeyboard').className = "noVNC_status_button"; + UI.keyboardVisible = false; + } +}, + +keyInputBlur: function() { + $D('showKeyboard').className = "noVNC_status_button"; + //Weird bug in iOS if you change keyboardVisible + //here it does not actually occur so next time + //you click keyboard icon it doesnt work. + setTimeout(function() { UI.setKeyboard(); },100); +}, + +setKeyboard: function() { + UI.keyboardVisible = false; +}, + +// iOS < Version 5 does not support position fixed. Javascript workaround: +setOnscroll: function() { + window.onscroll = function() { + UI.setBarPosition(); + }; +}, + +setResize: function () { + window.onResize = function() { + UI.setBarPosition(); + }; +}, + +//Helper to add options to dropdown. +addOption: function(selectbox,text,value ) +{ + var optn = document.createElement("OPTION"); + optn.text = text; + optn.value = value; + selectbox.options.add(optn); +}, + +setBarPosition: function() { + $D('noVNC-control-bar').style.top = (window.pageYOffset) + 'px'; + $D('noVNC_mobile_buttons').style.left = (window.pageXOffset) + 'px'; + + var vncwidth = $D('noVNC_screen').style.offsetWidth; + $D('noVNC-control-bar').style.width = vncwidth + 'px'; +} + +}; + + + + diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/util.js b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/util.js new file mode 100644 index 0000000000000000000000000000000000000000..8893591c440b8daf9930eaf1c59eadabdee3bb92 --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/util.js @@ -0,0 +1,383 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2012 Joel Martin + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +"use strict"; +/*jslint bitwise: false, white: false */ +/*global window, console, document, navigator, ActiveXObject */ + +// Globals defined here +var Util = {}; + + +/* + * Make arrays quack + */ + +Array.prototype.push8 = function (num) { + this.push(num & 0xFF); +}; + +Array.prototype.push16 = function (num) { + this.push((num >> 8) & 0xFF, + (num ) & 0xFF ); +}; +Array.prototype.push32 = function (num) { + this.push((num >> 24) & 0xFF, + (num >> 16) & 0xFF, + (num >> 8) & 0xFF, + (num ) & 0xFF ); +}; + +// IE does not support map (even in IE9) +//This prototype is provided by the Mozilla foundation and +//is distributed under the MIT license. +//http://www.ibiblio.org/pub/Linux/LICENSES/mit.license +if (!Array.prototype.map) +{ + Array.prototype.map = function(fun /*, thisp*/) + { + var len = this.length; + if (typeof fun != "function") + throw new TypeError(); + + var res = new Array(len); + var thisp = arguments[1]; + for (var i = 0; i < len; i++) + { + if (i in this) + res[i] = fun.call(thisp, this[i], i, this); + } + + return res; + }; +} + +// +// requestAnimationFrame shim with setTimeout fallback +// + +window.requestAnimFrame = (function(){ + return window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function(callback){ + window.setTimeout(callback, 1000 / 60); + }; +})(); + +/* + * ------------------------------------------------------ + * Namespaced in Util + * ------------------------------------------------------ + */ + +/* + * Logging/debug routines + */ + +Util._log_level = 'warn'; +Util.init_logging = function (level) { + if (typeof level === 'undefined') { + level = Util._log_level; + } else { + Util._log_level = level; + } + if (typeof window.console === "undefined") { + if (typeof window.opera !== "undefined") { + window.console = { + 'log' : window.opera.postError, + 'warn' : window.opera.postError, + 'error': window.opera.postError }; + } else { + window.console = { + 'log' : function(m) {}, + 'warn' : function(m) {}, + 'error': function(m) {}}; + } + } + + Util.Debug = Util.Info = Util.Warn = Util.Error = function (msg) {}; + switch (level) { + case 'debug': Util.Debug = function (msg) { console.log(msg); }; + case 'info': Util.Info = function (msg) { console.log(msg); }; + case 'warn': Util.Warn = function (msg) { console.warn(msg); }; + case 'error': Util.Error = function (msg) { console.error(msg); }; + case 'none': + break; + default: + throw("invalid logging type '" + level + "'"); + } +}; +Util.get_logging = function () { + return Util._log_level; +}; +// Initialize logging level +Util.init_logging(); + + +// Set configuration default for Crockford style function namespaces +Util.conf_default = function(cfg, api, defaults, v, mode, type, defval, desc) { + var getter, setter; + + // Default getter function + getter = function (idx) { + if ((type in {'arr':1, 'array':1}) && + (typeof idx !== 'undefined')) { + return cfg[v][idx]; + } else { + return cfg[v]; + } + }; + + // Default setter function + setter = function (val, idx) { + if (type in {'boolean':1, 'bool':1}) { + if ((!val) || (val in {'0':1, 'no':1, 'false':1})) { + val = false; + } else { + val = true; + } + } else if (type in {'integer':1, 'int':1}) { + val = parseInt(val, 10); + } else if (type === 'str') { + val = String(val); + } else if (type === 'func') { + if (!val) { + val = function () {}; + } + } + if (typeof idx !== 'undefined') { + cfg[v][idx] = val; + } else { + cfg[v] = val; + } + }; + + // Set the description + api[v + '_description'] = desc; + + // Set the getter function + if (typeof api['get_' + v] === 'undefined') { + api['get_' + v] = getter; + } + + // Set the setter function with extra sanity checks + if (typeof api['set_' + v] === 'undefined') { + api['set_' + v] = function (val, idx) { + if (mode in {'RO':1, 'ro':1}) { + throw(v + " is read-only"); + } else if ((mode in {'WO':1, 'wo':1}) && + (typeof cfg[v] !== 'undefined')) { + throw(v + " can only be set once"); + } + setter(val, idx); + }; + } + + // Set the default value + if (typeof defaults[v] !== 'undefined') { + defval = defaults[v]; + } else if ((type in {'arr':1, 'array':1}) && + (! (defval instanceof Array))) { + defval = []; + } + // Coerce existing setting to the right type + //Util.Debug("v: " + v + ", defval: " + defval + ", defaults[v]: " + defaults[v]); + setter(defval); +}; + +// Set group of configuration defaults +Util.conf_defaults = function(cfg, api, defaults, arr) { + var i; + for (i = 0; i < arr.length; i++) { + Util.conf_default(cfg, api, defaults, arr[i][0], arr[i][1], + arr[i][2], arr[i][3], arr[i][4]); + } +}; + + +/* + * Cross-browser routines + */ + + +// Dynamically load scripts without using document.write() +// Reference: http://unixpapa.com/js/dyna.html +// +// Handles the case where load_scripts is invoked from a script that +// itself is loaded via load_scripts. Once all scripts are loaded the +// window.onscriptsloaded handler is called (if set). +Util.get_include_uri = function() { + return (typeof INCLUDE_URI !== "undefined") ? INCLUDE_URI : "include/"; +} +Util._loading_scripts = []; +Util._pending_scripts = []; +Util.load_scripts = function(files) { + var head = document.getElementsByTagName('head')[0], script, + ls = Util._loading_scripts, ps = Util._pending_scripts; + for (var f=0; f<files.length; f++) { + script = document.createElement('script'); + script.type = 'text/javascript'; + script.src = Util.get_include_uri() + files[f]; + //console.log("loading script: " + script.src); + script.onload = script.onreadystatechange = function (e) { + while (ls.length > 0 && (ls[0].readyState === 'loaded' || + ls[0].readyState === 'complete')) { + // For IE, append the script to trigger execution + var s = ls.shift(); + //console.log("loaded script: " + s.src); + head.appendChild(s); + } + if (!this.readyState || + (Util.Engine.presto && this.readyState === 'loaded') || + this.readyState === 'complete') { + if (ps.indexOf(this) >= 0) { + this.onload = this.onreadystatechange = null; + //console.log("completed script: " + this.src); + ps.splice(ps.indexOf(this), 1); + + // Call window.onscriptsload after last script loads + if (ps.length === 0 && window.onscriptsload) { + window.onscriptsload(); + } + } + } + }; + // In-order script execution tricks + if (Util.Engine.trident) { + // For IE wait until readyState is 'loaded' before + // appending it which will trigger execution + // http://wiki.whatwg.org/wiki/Dynamic_Script_Execution_Order + ls.push(script); + } else { + // For webkit and firefox set async=false and append now + // https://developer.mozilla.org/en-US/docs/HTML/Element/script + script.async = false; + head.appendChild(script); + } + ps.push(script); + } +} + +// Get DOM element position on page +Util.getPosition = function (obj) { + var x = 0, y = 0; + if (obj.offsetParent) { + do { + x += obj.offsetLeft; + y += obj.offsetTop; + obj = obj.offsetParent; + } while (obj); + } + return {'x': x, 'y': y}; +}; + +// Get mouse event position in DOM element +Util.getEventPosition = function (e, obj, scale) { + var evt, docX, docY, pos; + //if (!e) evt = window.event; + evt = (e ? e : window.event); + evt = (evt.changedTouches ? evt.changedTouches[0] : evt.touches ? evt.touches[0] : evt); + if (evt.pageX || evt.pageY) { + docX = evt.pageX; + docY = evt.pageY; + } else if (evt.clientX || evt.clientY) { + docX = evt.clientX + document.body.scrollLeft + + document.documentElement.scrollLeft; + docY = evt.clientY + document.body.scrollTop + + document.documentElement.scrollTop; + } + pos = Util.getPosition(obj); + if (typeof scale === "undefined") { + scale = 1; + } + var realx = docX - pos.x; + var realy = docY - pos.y; + var x = Math.max(Math.min(realx, obj.width-1), 0); + var y = Math.max(Math.min(realy, obj.height-1), 0); + return {'x': x / scale, 'y': y / scale, 'realx': realx / scale, 'realy': realy / scale}; +}; + + +// Event registration. Based on: http://www.scottandrew.com/weblog/articles/cbs-events +Util.addEvent = function (obj, evType, fn){ + if (obj.attachEvent){ + var r = obj.attachEvent("on"+evType, fn); + return r; + } else if (obj.addEventListener){ + obj.addEventListener(evType, fn, false); + return true; + } else { + throw("Handler could not be attached"); + } +}; + +Util.removeEvent = function(obj, evType, fn){ + if (obj.detachEvent){ + var r = obj.detachEvent("on"+evType, fn); + return r; + } else if (obj.removeEventListener){ + obj.removeEventListener(evType, fn, false); + return true; + } else { + throw("Handler could not be removed"); + } +}; + +Util.stopEvent = function(e) { + if (e.stopPropagation) { e.stopPropagation(); } + else { e.cancelBubble = true; } + + if (e.preventDefault) { e.preventDefault(); } + else { e.returnValue = false; } +}; + + +// Set browser engine versions. Based on mootools. +Util.Features = {xpath: !!(document.evaluate), air: !!(window.runtime), query: !!(document.querySelector)}; + +Util.Engine = { + // Version detection break in Opera 11.60 (errors on arguments.callee.caller reference) + //'presto': (function() { + // return (!window.opera) ? false : ((arguments.callee.caller) ? 960 : ((document.getElementsByClassName) ? 950 : 925)); }()), + 'presto': (function() { return (!window.opera) ? false : true; }()), + + 'trident': (function() { + return (!window.ActiveXObject) ? false : ((window.XMLHttpRequest) ? ((document.querySelectorAll) ? 6 : 5) : 4); }()), + 'webkit': (function() { + try { return (navigator.taintEnabled) ? false : ((Util.Features.xpath) ? ((Util.Features.query) ? 525 : 420) : 419); } catch (e) { return false; } }()), + //'webkit': (function() { + // return ((typeof navigator.taintEnabled !== "unknown") && navigator.taintEnabled) ? false : ((Util.Features.xpath) ? ((Util.Features.query) ? 525 : 420) : 419); }()), + 'gecko': (function() { + return (!document.getBoxObjectFor && window.mozInnerScreenX == null) ? false : ((document.getElementsByClassName) ? 19 : 18); }()) +}; +if (Util.Engine.webkit) { + // Extract actual webkit version if available + Util.Engine.webkit = (function(v) { + var re = new RegExp('WebKit/([0-9\.]*) '); + v = (navigator.userAgent.match(re) || ['', v])[1]; + return parseFloat(v, 10); + })(Util.Engine.webkit); +} + +Util.Flash = (function(){ + var v, version; + try { + v = navigator.plugins['Shockwave Flash'].description; + } catch(err1) { + try { + v = new ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version'); + } catch(err2) { + v = '0 r0'; + } + } + version = v.match(/\d+/g); + return {version: parseInt(version[0] || 0 + '.' + version[1], 10) || 0, build: parseInt(version[2], 10) || 0}; +}()); diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/web-socket-js/README.txt b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/web-socket-js/README.txt new file mode 100644 index 0000000000000000000000000000000000000000..2e32ea7f34f39aa65ab6b7bd54e51d38b5b21b6d --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/web-socket-js/README.txt @@ -0,0 +1,109 @@ +* How to try + +Assuming you have Web server (e.g. Apache) running at http://example.com/ . + +- Download web_socket.rb from: + http://github.com/gimite/web-socket-ruby/tree/master +- Run sample Web Socket server (echo server) in example.com with: (#1) + $ ruby web-socket-ruby/samples/echo_server.rb example.com 10081 +- If your server already provides socket policy file at port 843, modify the file to allow access to port 10081. Otherwise you can skip this step. See below for details. +- Publish the web-socket-js directory with your Web server (e.g. put it in ~/public_html). +- Change ws://localhost:10081 to ws://example.com:10081 in sample.html. +- Open sample.html in your browser. +- After "onopen" is shown, input something, click [Send] and confirm echo back. + +#1: First argument of echo_server.rb means that it accepts Web Socket connection from HTML pages in example.com. + + +* Troubleshooting + +If it doesn't work, try these: + +1. Try Chrome and Firefox 3.x. +- It doesn't work on Chrome: +-- It's likely an issue of your code or the server. Debug your code as usual e.g. using console.log. +- It works on Chrome but it doesn't work on Firefox: +-- It's likely an issue of web-socket-js specific configuration (e.g. 3 and 4 below). +- It works on both Chrome and Firefox, but it doesn't work on your browser: +-- Check "Supported environment" section below. Your browser may not be supported by web-socket-js. + +2. Add this line before your code: + WEB_SOCKET_DEBUG = true; +and use Developer Tools (Chrome/Safari) or Firebug (Firefox) to see if console.log outputs any errors. + +3. Make sure you do NOT open your HTML page as local file e.g. file:///.../sample.html. web-socket-js doesn't work on local file. Open it via Web server e.g. http:///.../sample.html. + +4. If you are NOT using web-socket-ruby as your WebSocket server, you need to place Flash socket policy file on your server. See "Flash socket policy file" section below for details. + +5. Check if sample.html bundled with web-socket-js works. + +6. Make sure the port used for WebSocket (10081 in example above) is not blocked by your server/client's firewall. + +7. Install debugger version of Flash Player available here to see Flash errors: +http://www.adobe.com/support/flashplayer/downloads.html + + +* Supported environments + +It should work on: +- Google Chrome 4 or later (just uses native implementation) +- Firefox 3.x, Internet Explorer 8 + Flash Player 9 or later + +It may or may not work on other browsers such as Safari, Opera or IE 6. Patch for these browsers are appreciated, but I will not work on fixing issues specific to these browsers by myself. + + +* Flash socket policy file + +This implementation uses Flash's socket, which means that your server must provide Flash socket policy file to declare the server accepts connections from Flash. + +If you use web-socket-ruby available at +http://github.com/gimite/web-socket-ruby/tree/master +, you don't need anything special, because web-socket-ruby handles Flash socket policy file request. But if you already provide socket policy file at port 843, you need to modify the file to allow access to Web Socket port, because it precedes what web-socket-ruby provides. + +If you use other Web Socket server implementation, you need to provide socket policy file yourself. See +http://www.lightsphere.com/dev/articles/flash_socket_policy.html +for details and sample script to run socket policy file server. node.js implementation is available here: +http://github.com/LearnBoost/Socket.IO-node/blob/master/lib/socket.io/transports/flashsocket.js + +Actually, it's still better to provide socket policy file at port 843 even if you use web-socket-ruby. Flash always try to connect to port 843 first, so providing the file at port 843 makes startup faster. + + +* Cookie considerations + +Cookie is sent if Web Socket host is the same as the origin of JavaScript. Otherwise it is not sent, because I don't know way to send right Cookie (which is Cookie of the host of Web Socket, I heard). + +Note that it's technically possible that client sends arbitrary string as Cookie and any other headers (by modifying this library for example) once you place Flash socket policy file in your server. So don't trust Cookie and other headers if you allow connection from untrusted origin. + + +* Proxy considerations + +The WebSocket spec (http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol) specifies instructions for User Agents to support proxied connections by implementing the HTTP CONNECT method. + +The AS3 Socket class doesn't implement this mechanism, which renders it useless for the scenarios where the user trying to open a socket is behind a proxy. + +The class RFC2817Socket (by Christian Cantrell) effectively lets us implement this, as long as the proxy settings are known and provided by the interface that instantiates the WebSocket. As such, if you want to support proxied conncetions, you'll have to supply this information to the WebSocket constructor when Flash is being used. One way to go about it would be to ask the user for proxy settings information if the initial connection fails. + + +* How to host HTML file and SWF file in different domains + +By default, HTML file and SWF file must be in the same domain. You can follow steps below to allow hosting them in different domain. + +WARNING: If you use the method below, HTML files in ANY domains can send arbitrary TCP data to your WebSocket server, regardless of configuration in Flash socket policy file. Arbitrary TCP data means that they can even fake request headers including Origin and Cookie. + +- Unzip WebSocketMainInsecure.zip to extract WebSocketMainInsecure.swf. +- Put WebSocketMainInsecure.swf on your server, instead of WebSocketMain.swf. +- In JavaScript, set WEB_SOCKET_SWF_LOCATION to URL of your WebSocketMainInsecure.swf. + + +* How to build WebSocketMain.swf + +Install Flex 4 SDK: +http://opensource.adobe.com/wiki/display/flexsdk/Download+Flex+4 + +$ cd flash-src +$ ./build.sh + + +* License + +New BSD License. diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/web-socket-js/WebSocketMain.swf b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/web-socket-js/WebSocketMain.swf new file mode 100644 index 0000000000000000000000000000000000000000..8174466912475a494681e9436844f4bf90d909f9 Binary files /dev/null and b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/web-socket-js/WebSocketMain.swf differ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/web-socket-js/swfobject.js b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/web-socket-js/swfobject.js new file mode 100644 index 0000000000000000000000000000000000000000..8eafe9dd83f91b52c2decd532db81527bb98e488 --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/web-socket-js/swfobject.js @@ -0,0 +1,4 @@ +/* SWFObject v2.2 <http://code.google.com/p/swfobject/> + is released under the MIT License <http://www.opensource.org/licenses/mit-license.php> +*/ +var swfobject=function(){var D="undefined",r="object",S="Shockwave Flash",W="ShockwaveFlash.ShockwaveFlash",q="application/x-shockwave-flash",R="SWFObjectExprInst",x="onreadystatechange",O=window,j=document,t=navigator,T=false,U=[h],o=[],N=[],I=[],l,Q,E,B,J=false,a=false,n,G,m=true,M=function(){var aa=typeof j.getElementById!=D&&typeof j.getElementsByTagName!=D&&typeof j.createElement!=D,ah=t.userAgent.toLowerCase(),Y=t.platform.toLowerCase(),ae=Y?/win/.test(Y):/win/.test(ah),ac=Y?/mac/.test(Y):/mac/.test(ah),af=/webkit/.test(ah)?parseFloat(ah.replace(/^.*webkit\/(\d+(\.\d+)?).*$/,"$1")):false,X=!+"\v1",ag=[0,0,0],ab=null;if(typeof t.plugins!=D&&typeof t.plugins[S]==r){ab=t.plugins[S].description;if(ab&&!(typeof t.mimeTypes!=D&&t.mimeTypes[q]&&!t.mimeTypes[q].enabledPlugin)){T=true;X=false;ab=ab.replace(/^.*\s+(\S+\s+\S+$)/,"$1");ag[0]=parseInt(ab.replace(/^(.*)\..*$/,"$1"),10);ag[1]=parseInt(ab.replace(/^.*\.(.*)\s.*$/,"$1"),10);ag[2]=/[a-zA-Z]/.test(ab)?parseInt(ab.replace(/^.*[a-zA-Z]+(.*)$/,"$1"),10):0}}else{if(typeof O.ActiveXObject!=D){try{var ad=new ActiveXObject(W);if(ad){ab=ad.GetVariable("$version");if(ab){X=true;ab=ab.split(" ")[1].split(",");ag=[parseInt(ab[0],10),parseInt(ab[1],10),parseInt(ab[2],10)]}}}catch(Z){}}}return{w3:aa,pv:ag,wk:af,ie:X,win:ae,mac:ac}}(),k=function(){if(!M.w3){return}if((typeof j.readyState!=D&&j.readyState=="complete")||(typeof j.readyState==D&&(j.getElementsByTagName("body")[0]||j.body))){f()}if(!J){if(typeof j.addEventListener!=D){j.addEventListener("DOMContentLoaded",f,false)}if(M.ie&&M.win){j.attachEvent(x,function(){if(j.readyState=="complete"){j.detachEvent(x,arguments.callee);f()}});if(O==top){(function(){if(J){return}try{j.documentElement.doScroll("left")}catch(X){setTimeout(arguments.callee,0);return}f()})()}}if(M.wk){(function(){if(J){return}if(!/loaded|complete/.test(j.readyState)){setTimeout(arguments.callee,0);return}f()})()}s(f)}}();function f(){if(J){return}try{var Z=j.getElementsByTagName("body")[0].appendChild(C("span"));Z.parentNode.removeChild(Z)}catch(aa){return}J=true;var X=U.length;for(var Y=0;Y<X;Y++){U[Y]()}}function K(X){if(J){X()}else{U[U.length]=X}}function s(Y){if(typeof O.addEventListener!=D){O.addEventListener("load",Y,false)}else{if(typeof j.addEventListener!=D){j.addEventListener("load",Y,false)}else{if(typeof O.attachEvent!=D){i(O,"onload",Y)}else{if(typeof O.onload=="function"){var X=O.onload;O.onload=function(){X();Y()}}else{O.onload=Y}}}}}function h(){if(T){V()}else{H()}}function V(){var X=j.getElementsByTagName("body")[0];var aa=C(r);aa.setAttribute("type",q);var Z=X.appendChild(aa);if(Z){var Y=0;(function(){if(typeof Z.GetVariable!=D){var ab=Z.GetVariable("$version");if(ab){ab=ab.split(" ")[1].split(",");M.pv=[parseInt(ab[0],10),parseInt(ab[1],10),parseInt(ab[2],10)]}}else{if(Y<10){Y++;setTimeout(arguments.callee,10);return}}X.removeChild(aa);Z=null;H()})()}else{H()}}function H(){var ag=o.length;if(ag>0){for(var af=0;af<ag;af++){var Y=o[af].id;var ab=o[af].callbackFn;var aa={success:false,id:Y};if(M.pv[0]>0){var ae=c(Y);if(ae){if(F(o[af].swfVersion)&&!(M.wk&&M.wk<312)){w(Y,true);if(ab){aa.success=true;aa.ref=z(Y);ab(aa)}}else{if(o[af].expressInstall&&A()){var ai={};ai.data=o[af].expressInstall;ai.width=ae.getAttribute("width")||"0";ai.height=ae.getAttribute("height")||"0";if(ae.getAttribute("class")){ai.styleclass=ae.getAttribute("class")}if(ae.getAttribute("align")){ai.align=ae.getAttribute("align")}var ah={};var X=ae.getElementsByTagName("param");var ac=X.length;for(var ad=0;ad<ac;ad++){if(X[ad].getAttribute("name").toLowerCase()!="movie"){ah[X[ad].getAttribute("name")]=X[ad].getAttribute("value")}}P(ai,ah,Y,ab)}else{p(ae);if(ab){ab(aa)}}}}}else{w(Y,true);if(ab){var Z=z(Y);if(Z&&typeof Z.SetVariable!=D){aa.success=true;aa.ref=Z}ab(aa)}}}}}function z(aa){var X=null;var Y=c(aa);if(Y&&Y.nodeName=="OBJECT"){if(typeof Y.SetVariable!=D){X=Y}else{var Z=Y.getElementsByTagName(r)[0];if(Z){X=Z}}}return X}function A(){return !a&&F("6.0.65")&&(M.win||M.mac)&&!(M.wk&&M.wk<312)}function P(aa,ab,X,Z){a=true;E=Z||null;B={success:false,id:X};var ae=c(X);if(ae){if(ae.nodeName=="OBJECT"){l=g(ae);Q=null}else{l=ae;Q=X}aa.id=R;if(typeof aa.width==D||(!/%$/.test(aa.width)&&parseInt(aa.width,10)<310)){aa.width="310"}if(typeof aa.height==D||(!/%$/.test(aa.height)&&parseInt(aa.height,10)<137)){aa.height="137"}j.title=j.title.slice(0,47)+" - Flash Player Installation";var ad=M.ie&&M.win?"ActiveX":"PlugIn",ac="MMredirectURL="+O.location.toString().replace(/&/g,"%26")+"&MMplayerType="+ad+"&MMdoctitle="+j.title;if(typeof ab.flashvars!=D){ab.flashvars+="&"+ac}else{ab.flashvars=ac}if(M.ie&&M.win&&ae.readyState!=4){var Y=C("div");X+="SWFObjectNew";Y.setAttribute("id",X);ae.parentNode.insertBefore(Y,ae);ae.style.display="none";(function(){if(ae.readyState==4){ae.parentNode.removeChild(ae)}else{setTimeout(arguments.callee,10)}})()}u(aa,ab,X)}}function p(Y){if(M.ie&&M.win&&Y.readyState!=4){var X=C("div");Y.parentNode.insertBefore(X,Y);X.parentNode.replaceChild(g(Y),X);Y.style.display="none";(function(){if(Y.readyState==4){Y.parentNode.removeChild(Y)}else{setTimeout(arguments.callee,10)}})()}else{Y.parentNode.replaceChild(g(Y),Y)}}function g(ab){var aa=C("div");if(M.win&&M.ie){aa.innerHTML=ab.innerHTML}else{var Y=ab.getElementsByTagName(r)[0];if(Y){var ad=Y.childNodes;if(ad){var X=ad.length;for(var Z=0;Z<X;Z++){if(!(ad[Z].nodeType==1&&ad[Z].nodeName=="PARAM")&&!(ad[Z].nodeType==8)){aa.appendChild(ad[Z].cloneNode(true))}}}}}return aa}function u(ai,ag,Y){var X,aa=c(Y);if(M.wk&&M.wk<312){return X}if(aa){if(typeof ai.id==D){ai.id=Y}if(M.ie&&M.win){var ah="";for(var ae in ai){if(ai[ae]!=Object.prototype[ae]){if(ae.toLowerCase()=="data"){ag.movie=ai[ae]}else{if(ae.toLowerCase()=="styleclass"){ah+=' class="'+ai[ae]+'"'}else{if(ae.toLowerCase()!="classid"){ah+=" "+ae+'="'+ai[ae]+'"'}}}}}var af="";for(var ad in ag){if(ag[ad]!=Object.prototype[ad]){af+='<param name="'+ad+'" value="'+ag[ad]+'" />'}}aa.outerHTML='<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"'+ah+">"+af+"</object>";N[N.length]=ai.id;X=c(ai.id)}else{var Z=C(r);Z.setAttribute("type",q);for(var ac in ai){if(ai[ac]!=Object.prototype[ac]){if(ac.toLowerCase()=="styleclass"){Z.setAttribute("class",ai[ac])}else{if(ac.toLowerCase()!="classid"){Z.setAttribute(ac,ai[ac])}}}}for(var ab in ag){if(ag[ab]!=Object.prototype[ab]&&ab.toLowerCase()!="movie"){e(Z,ab,ag[ab])}}aa.parentNode.replaceChild(Z,aa);X=Z}}return X}function e(Z,X,Y){var aa=C("param");aa.setAttribute("name",X);aa.setAttribute("value",Y);Z.appendChild(aa)}function y(Y){var X=c(Y);if(X&&X.nodeName=="OBJECT"){if(M.ie&&M.win){X.style.display="none";(function(){if(X.readyState==4){b(Y)}else{setTimeout(arguments.callee,10)}})()}else{X.parentNode.removeChild(X)}}}function b(Z){var Y=c(Z);if(Y){for(var X in Y){if(typeof Y[X]=="function"){Y[X]=null}}Y.parentNode.removeChild(Y)}}function c(Z){var X=null;try{X=j.getElementById(Z)}catch(Y){}return X}function C(X){return j.createElement(X)}function i(Z,X,Y){Z.attachEvent(X,Y);I[I.length]=[Z,X,Y]}function F(Z){var Y=M.pv,X=Z.split(".");X[0]=parseInt(X[0],10);X[1]=parseInt(X[1],10)||0;X[2]=parseInt(X[2],10)||0;return(Y[0]>X[0]||(Y[0]==X[0]&&Y[1]>X[1])||(Y[0]==X[0]&&Y[1]==X[1]&&Y[2]>=X[2]))?true:false}function v(ac,Y,ad,ab){if(M.ie&&M.mac){return}var aa=j.getElementsByTagName("head")[0];if(!aa){return}var X=(ad&&typeof ad=="string")?ad:"screen";if(ab){n=null;G=null}if(!n||G!=X){var Z=C("style");Z.setAttribute("type","text/css");Z.setAttribute("media",X);n=aa.appendChild(Z);if(M.ie&&M.win&&typeof j.styleSheets!=D&&j.styleSheets.length>0){n=j.styleSheets[j.styleSheets.length-1]}G=X}if(M.ie&&M.win){if(n&&typeof n.addRule==r){n.addRule(ac,Y)}}else{if(n&&typeof j.createTextNode!=D){n.appendChild(j.createTextNode(ac+" {"+Y+"}"))}}}function w(Z,X){if(!m){return}var Y=X?"visible":"hidden";if(J&&c(Z)){c(Z).style.visibility=Y}else{v("#"+Z,"visibility:"+Y)}}function L(Y){var Z=/[\\\"<>\.;]/;var X=Z.exec(Y)!=null;return X&&typeof encodeURIComponent!=D?encodeURIComponent(Y):Y}var d=function(){if(M.ie&&M.win){window.attachEvent("onunload",function(){var ac=I.length;for(var ab=0;ab<ac;ab++){I[ab][0].detachEvent(I[ab][1],I[ab][2])}var Z=N.length;for(var aa=0;aa<Z;aa++){y(N[aa])}for(var Y in M){M[Y]=null}M=null;for(var X in swfobject){swfobject[X]=null}swfobject=null})}}();return{registerObject:function(ab,X,aa,Z){if(M.w3&&ab&&X){var Y={};Y.id=ab;Y.swfVersion=X;Y.expressInstall=aa;Y.callbackFn=Z;o[o.length]=Y;w(ab,false)}else{if(Z){Z({success:false,id:ab})}}},getObjectById:function(X){if(M.w3){return z(X)}},embedSWF:function(ab,ah,ae,ag,Y,aa,Z,ad,af,ac){var X={success:false,id:ah};if(M.w3&&!(M.wk&&M.wk<312)&&ab&&ah&&ae&&ag&&Y){w(ah,false);K(function(){ae+="";ag+="";var aj={};if(af&&typeof af===r){for(var al in af){aj[al]=af[al]}}aj.data=ab;aj.width=ae;aj.height=ag;var am={};if(ad&&typeof ad===r){for(var ak in ad){am[ak]=ad[ak]}}if(Z&&typeof Z===r){for(var ai in Z){if(typeof am.flashvars!=D){am.flashvars+="&"+ai+"="+Z[ai]}else{am.flashvars=ai+"="+Z[ai]}}}if(F(Y)){var an=u(aj,am,ah);if(aj.id==ah){w(ah,true)}X.success=true;X.ref=an}else{if(aa&&A()){aj.data=aa;P(aj,am,ah,ac);return}else{w(ah,true)}}if(ac){ac(X)}})}else{if(ac){ac(X)}}},switchOffAutoHideShow:function(){m=false},ua:M,getFlashPlayerVersion:function(){return{major:M.pv[0],minor:M.pv[1],release:M.pv[2]}},hasFlashPlayerVersion:F,createSWF:function(Z,Y,X){if(M.w3){return u(Z,Y,X)}else{return undefined}},showExpressInstall:function(Z,aa,X,Y){if(M.w3&&A()){P(Z,aa,X,Y)}},removeSWF:function(X){if(M.w3){y(X)}},createCSS:function(aa,Z,Y,X){if(M.w3){v(aa,Z,Y,X)}},addDomLoadEvent:K,addLoadEvent:s,getQueryParamValue:function(aa){var Z=j.location.search||j.location.hash;if(Z){if(/\?/.test(Z)){Z=Z.split("?")[1]}if(aa==null){return L(Z)}var Y=Z.split("&");for(var X=0;X<Y.length;X++){if(Y[X].substring(0,Y[X].indexOf("="))==aa){return L(Y[X].substring((Y[X].indexOf("=")+1)))}}}return""},expressInstallCallback:function(){if(a){var X=c(R);if(X&&l){X.parentNode.replaceChild(l,X);if(Q){w(Q,true);if(M.ie&&M.win){l.style.display="block"}}if(E){E(B)}}a=false}}}}(); \ No newline at end of file diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/web-socket-js/web_socket.js b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/web-socket-js/web_socket.js new file mode 100644 index 0000000000000000000000000000000000000000..06cc5d02707f33b53de284b82dd11232827555b5 --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/web-socket-js/web_socket.js @@ -0,0 +1,391 @@ +// Copyright: Hiroshi Ichikawa <http://gimite.net/en/> +// License: New BSD License +// Reference: http://dev.w3.org/html5/websockets/ +// Reference: http://tools.ietf.org/html/rfc6455 + +(function() { + + if (window.WEB_SOCKET_FORCE_FLASH) { + // Keeps going. + } else if (window.WebSocket) { + return; + } else if (window.MozWebSocket) { + // Firefox. + window.WebSocket = MozWebSocket; + return; + } + + var logger; + if (window.WEB_SOCKET_LOGGER) { + logger = WEB_SOCKET_LOGGER; + } else if (window.console && window.console.log && window.console.error) { + // In some environment, console is defined but console.log or console.error is missing. + logger = window.console; + } else { + logger = {log: function(){ }, error: function(){ }}; + } + + // swfobject.hasFlashPlayerVersion("10.0.0") doesn't work with Gnash. + if (swfobject.getFlashPlayerVersion().major < 10) { + logger.error("Flash Player >= 10.0.0 is required."); + return; + } + if (location.protocol == "file:") { + logger.error( + "WARNING: web-socket-js doesn't work in file:///... URL " + + "unless you set Flash Security Settings properly. " + + "Open the page via Web server i.e. http://..."); + } + + /** + * Our own implementation of WebSocket class using Flash. + * @param {string} url + * @param {array or string} protocols + * @param {string} proxyHost + * @param {int} proxyPort + * @param {string} headers + */ + window.WebSocket = function(url, protocols, proxyHost, proxyPort, headers) { + var self = this; + self.__id = WebSocket.__nextId++; + WebSocket.__instances[self.__id] = self; + self.readyState = WebSocket.CONNECTING; + self.bufferedAmount = 0; + self.__events = {}; + if (!protocols) { + protocols = []; + } else if (typeof protocols == "string") { + protocols = [protocols]; + } + // Uses setTimeout() to make sure __createFlash() runs after the caller sets ws.onopen etc. + // Otherwise, when onopen fires immediately, onopen is called before it is set. + self.__createTask = setTimeout(function() { + WebSocket.__addTask(function() { + self.__createTask = null; + WebSocket.__flash.create( + self.__id, url, protocols, proxyHost || null, proxyPort || 0, headers || null); + }); + }, 0); + }; + + /** + * Send data to the web socket. + * @param {string} data The data to send to the socket. + * @return {boolean} True for success, false for failure. + */ + WebSocket.prototype.send = function(data) { + if (this.readyState == WebSocket.CONNECTING) { + throw "INVALID_STATE_ERR: Web Socket connection has not been established"; + } + // We use encodeURIComponent() here, because FABridge doesn't work if + // the argument includes some characters. We don't use escape() here + // because of this: + // https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Functions#escape_and_unescape_Functions + // But it looks decodeURIComponent(encodeURIComponent(s)) doesn't + // preserve all Unicode characters either e.g. "\uffff" in Firefox. + // Note by wtritch: Hopefully this will not be necessary using ExternalInterface. Will require + // additional testing. + var result = WebSocket.__flash.send(this.__id, encodeURIComponent(data)); + if (result < 0) { // success + return true; + } else { + this.bufferedAmount += result; + return false; + } + }; + + /** + * Close this web socket gracefully. + */ + WebSocket.prototype.close = function() { + if (this.__createTask) { + clearTimeout(this.__createTask); + this.__createTask = null; + this.readyState = WebSocket.CLOSED; + return; + } + if (this.readyState == WebSocket.CLOSED || this.readyState == WebSocket.CLOSING) { + return; + } + this.readyState = WebSocket.CLOSING; + WebSocket.__flash.close(this.__id); + }; + + /** + * Implementation of {@link <a href="http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-registration">DOM 2 EventTarget Interface</a>} + * + * @param {string} type + * @param {function} listener + * @param {boolean} useCapture + * @return void + */ + WebSocket.prototype.addEventListener = function(type, listener, useCapture) { + if (!(type in this.__events)) { + this.__events[type] = []; + } + this.__events[type].push(listener); + }; + + /** + * Implementation of {@link <a href="http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-registration">DOM 2 EventTarget Interface</a>} + * + * @param {string} type + * @param {function} listener + * @param {boolean} useCapture + * @return void + */ + WebSocket.prototype.removeEventListener = function(type, listener, useCapture) { + if (!(type in this.__events)) return; + var events = this.__events[type]; + for (var i = events.length - 1; i >= 0; --i) { + if (events[i] === listener) { + events.splice(i, 1); + break; + } + } + }; + + /** + * Implementation of {@link <a href="http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-registration">DOM 2 EventTarget Interface</a>} + * + * @param {Event} event + * @return void + */ + WebSocket.prototype.dispatchEvent = function(event) { + var events = this.__events[event.type] || []; + for (var i = 0; i < events.length; ++i) { + events[i](event); + } + var handler = this["on" + event.type]; + if (handler) handler.apply(this, [event]); + }; + + /** + * Handles an event from Flash. + * @param {Object} flashEvent + */ + WebSocket.prototype.__handleEvent = function(flashEvent) { + + if ("readyState" in flashEvent) { + this.readyState = flashEvent.readyState; + } + if ("protocol" in flashEvent) { + this.protocol = flashEvent.protocol; + } + + var jsEvent; + if (flashEvent.type == "open" || flashEvent.type == "error") { + jsEvent = this.__createSimpleEvent(flashEvent.type); + } else if (flashEvent.type == "close") { + jsEvent = this.__createSimpleEvent("close"); + jsEvent.wasClean = flashEvent.wasClean ? true : false; + jsEvent.code = flashEvent.code; + jsEvent.reason = flashEvent.reason; + } else if (flashEvent.type == "message") { + var data = decodeURIComponent(flashEvent.message); + jsEvent = this.__createMessageEvent("message", data); + } else { + throw "unknown event type: " + flashEvent.type; + } + + this.dispatchEvent(jsEvent); + + }; + + WebSocket.prototype.__createSimpleEvent = function(type) { + if (document.createEvent && window.Event) { + var event = document.createEvent("Event"); + event.initEvent(type, false, false); + return event; + } else { + return {type: type, bubbles: false, cancelable: false}; + } + }; + + WebSocket.prototype.__createMessageEvent = function(type, data) { + if (document.createEvent && window.MessageEvent && !window.opera) { + var event = document.createEvent("MessageEvent"); + event.initMessageEvent("message", false, false, data, null, null, window, null); + return event; + } else { + // IE and Opera, the latter one truncates the data parameter after any 0x00 bytes. + return {type: type, data: data, bubbles: false, cancelable: false}; + } + }; + + /** + * Define the WebSocket readyState enumeration. + */ + WebSocket.CONNECTING = 0; + WebSocket.OPEN = 1; + WebSocket.CLOSING = 2; + WebSocket.CLOSED = 3; + + // Field to check implementation of WebSocket. + WebSocket.__isFlashImplementation = true; + WebSocket.__initialized = false; + WebSocket.__flash = null; + WebSocket.__instances = {}; + WebSocket.__tasks = []; + WebSocket.__nextId = 0; + + /** + * Load a new flash security policy file. + * @param {string} url + */ + WebSocket.loadFlashPolicyFile = function(url){ + WebSocket.__addTask(function() { + WebSocket.__flash.loadManualPolicyFile(url); + }); + }; + + /** + * Loads WebSocketMain.swf and creates WebSocketMain object in Flash. + */ + WebSocket.__initialize = function() { + + if (WebSocket.__initialized) return; + WebSocket.__initialized = true; + + if (WebSocket.__swfLocation) { + // For backword compatibility. + window.WEB_SOCKET_SWF_LOCATION = WebSocket.__swfLocation; + } + if (!window.WEB_SOCKET_SWF_LOCATION) { + logger.error("[WebSocket] set WEB_SOCKET_SWF_LOCATION to location of WebSocketMain.swf"); + return; + } + if (!window.WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR && + !WEB_SOCKET_SWF_LOCATION.match(/(^|\/)WebSocketMainInsecure\.swf(\?.*)?$/) && + WEB_SOCKET_SWF_LOCATION.match(/^\w+:\/\/([^\/]+)/)) { + var swfHost = RegExp.$1; + if (location.host != swfHost) { + logger.error( + "[WebSocket] You must host HTML and WebSocketMain.swf in the same host " + + "('" + location.host + "' != '" + swfHost + "'). " + + "See also 'How to host HTML file and SWF file in different domains' section " + + "in README.md. If you use WebSocketMainInsecure.swf, you can suppress this message " + + "by WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR = true;"); + } + } + var container = document.createElement("div"); + container.id = "webSocketContainer"; + // Hides Flash box. We cannot use display: none or visibility: hidden because it prevents + // Flash from loading at least in IE. So we move it out of the screen at (-100, -100). + // But this even doesn't work with Flash Lite (e.g. in Droid Incredible). So with Flash + // Lite, we put it at (0, 0). This shows 1x1 box visible at left-top corner but this is + // the best we can do as far as we know now. + container.style.position = "absolute"; + if (WebSocket.__isFlashLite()) { + container.style.left = "0px"; + container.style.top = "0px"; + } else { + container.style.left = "-100px"; + container.style.top = "-100px"; + } + var holder = document.createElement("div"); + holder.id = "webSocketFlash"; + container.appendChild(holder); + document.body.appendChild(container); + // See this article for hasPriority: + // http://help.adobe.com/en_US/as3/mobile/WS4bebcd66a74275c36cfb8137124318eebc6-7ffd.html + swfobject.embedSWF( + WEB_SOCKET_SWF_LOCATION, + "webSocketFlash", + "1" /* width */, + "1" /* height */, + "10.0.0" /* SWF version */, + null, + null, + {hasPriority: true, swliveconnect : true, allowScriptAccess: "always"}, + null, + function(e) { + if (!e.success) { + logger.error("[WebSocket] swfobject.embedSWF failed"); + } + } + ); + + }; + + /** + * Called by Flash to notify JS that it's fully loaded and ready + * for communication. + */ + WebSocket.__onFlashInitialized = function() { + // We need to set a timeout here to avoid round-trip calls + // to flash during the initialization process. + setTimeout(function() { + WebSocket.__flash = document.getElementById("webSocketFlash"); + WebSocket.__flash.setCallerUrl(location.href); + WebSocket.__flash.setDebug(!!window.WEB_SOCKET_DEBUG); + for (var i = 0; i < WebSocket.__tasks.length; ++i) { + WebSocket.__tasks[i](); + } + WebSocket.__tasks = []; + }, 0); + }; + + /** + * Called by Flash to notify WebSockets events are fired. + */ + WebSocket.__onFlashEvent = function() { + setTimeout(function() { + try { + // Gets events using receiveEvents() instead of getting it from event object + // of Flash event. This is to make sure to keep message order. + // It seems sometimes Flash events don't arrive in the same order as they are sent. + var events = WebSocket.__flash.receiveEvents(); + for (var i = 0; i < events.length; ++i) { + WebSocket.__instances[events[i].webSocketId].__handleEvent(events[i]); + } + } catch (e) { + logger.error(e); + } + }, 0); + return true; + }; + + // Called by Flash. + WebSocket.__log = function(message) { + logger.log(decodeURIComponent(message)); + }; + + // Called by Flash. + WebSocket.__error = function(message) { + logger.error(decodeURIComponent(message)); + }; + + WebSocket.__addTask = function(task) { + if (WebSocket.__flash) { + task(); + } else { + WebSocket.__tasks.push(task); + } + }; + + /** + * Test if the browser is running flash lite. + * @return {boolean} True if flash lite is running, false otherwise. + */ + WebSocket.__isFlashLite = function() { + if (!window.navigator || !window.navigator.mimeTypes) { + return false; + } + var mimeType = window.navigator.mimeTypes["application/x-shockwave-flash"]; + if (!mimeType || !mimeType.enabledPlugin || !mimeType.enabledPlugin.filename) { + return false; + } + return mimeType.enabledPlugin.filename.match(/flashlite/i) ? true : false; + }; + + if (!window.WEB_SOCKET_DISABLE_AUTO_INITIALIZATION) { + // NOTE: + // This fires immediately if web_socket.js is dynamically loaded after + // the document is loaded. + swfobject.addDomLoadEvent(function() { + WebSocket.__initialize(); + }); + } + +})(); diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/websock.js b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/websock.js new file mode 100644 index 0000000000000000000000000000000000000000..a91d6e671d62b27d95d90809d14f0d1405d02c4f --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/websock.js @@ -0,0 +1,423 @@ +/* + * Websock: high-performance binary WebSockets + * Copyright (C) 2012 Joel Martin + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * Websock is similar to the standard WebSocket object but Websock + * enables communication with raw TCP sockets (i.e. the binary stream) + * via websockify. This is accomplished by base64 encoding the data + * stream between Websock and websockify. + * + * Websock has built-in receive queue buffering; the message event + * does not contain actual data but is simply a notification that + * there is new data available. Several rQ* methods are available to + * read binary data off of the receive queue. + */ + +/*jslint browser: true, bitwise: false, plusplus: false */ +/*global Util, Base64 */ + + +// Load Flash WebSocket emulator if needed + +// To force WebSocket emulator even when native WebSocket available +//window.WEB_SOCKET_FORCE_FLASH = true; +// To enable WebSocket emulator debug: +//window.WEB_SOCKET_DEBUG=1; + + +if (window.WebSocket && !window.WEB_SOCKET_FORCE_FLASH) { + Websock_native = true; +} else if (window.MozWebSocket && !window.WEB_SOCKET_FORCE_FLASH) { + Websock_native = true; + window.WebSocket = window.MozWebSocket; +} else { + /* no builtin WebSocket so load web_socket.js */ + + Websock_native = false; + (function () { + var base_path = MEDIA_URL + "extra/noVNC/include/"; + window.WEB_SOCKET_SWF_LOCATION = base_path + + "web-socket-js/WebSocketMain.swf"; + if (Util.Engine.trident) { + Util.Debug("Forcing uncached load of WebSocketMain.swf"); + window.WEB_SOCKET_SWF_LOCATION += "?" + Math.random(); + } + var INCLUDE_URI = ''; + Util.load_scripts([base_path + "web-socket-js/swfobject.js", + base_path + "web-socket-js/web_socket.js"]); + }()); +} + + +function Websock() { +"use strict"; + +var api = {}, // Public API + websocket = null, // WebSocket object + mode = 'base64', // Current WebSocket mode: 'binary', 'base64' + rQ = [], // Receive queue + rQi = 0, // Receive queue index + rQmax = 10000, // Max receive queue size before compacting + sQ = [], // Send queue + + eventHandlers = { + 'message' : function() {}, + 'open' : function() {}, + 'close' : function() {}, + 'error' : function() {} + }, + + test_mode = false; + + +// +// Queue public functions +// + +function get_sQ() { + return sQ; +} + +function get_rQ() { + return rQ; +} +function get_rQi() { + return rQi; +} +function set_rQi(val) { + rQi = val; +} + +function rQlen() { + return rQ.length - rQi; +} + +function rQpeek8() { + return (rQ[rQi] ); +} +function rQshift8() { + return (rQ[rQi++] ); +} +function rQunshift8(num) { + if (rQi === 0) { + rQ.unshift(num); + } else { + rQi -= 1; + rQ[rQi] = num; + } + +} +function rQshift16() { + return (rQ[rQi++] << 8) + + (rQ[rQi++] ); +} +function rQshift32() { + return (rQ[rQi++] << 24) + + (rQ[rQi++] << 16) + + (rQ[rQi++] << 8) + + (rQ[rQi++] ); +} +function rQshiftStr(len) { + if (typeof(len) === 'undefined') { len = rQlen(); } + var arr = rQ.slice(rQi, rQi + len); + rQi += len; + return String.fromCharCode.apply(null, arr); +} +function rQshiftBytes(len) { + if (typeof(len) === 'undefined') { len = rQlen(); } + rQi += len; + return rQ.slice(rQi-len, rQi); +} + +function rQslice(start, end) { + if (end) { + return rQ.slice(rQi + start, rQi + end); + } else { + return rQ.slice(rQi + start); + } +} + +// Check to see if we must wait for 'num' bytes (default to FBU.bytes) +// to be available in the receive queue. Return true if we need to +// wait (and possibly print a debug message), otherwise false. +function rQwait(msg, num, goback) { + var rQlen = rQ.length - rQi; // Skip rQlen() function call + if (rQlen < num) { + if (goback) { + if (rQi < goback) { + throw("rQwait cannot backup " + goback + " bytes"); + } + rQi -= goback; + } + //Util.Debug(" waiting for " + (num-rQlen) + + // " " + msg + " byte(s)"); + return true; // true means need more data + } + return false; +} + +// +// Private utility routines +// + +function encode_message() { + if (mode === 'binary') { + // Put in a binary arraybuffer + return (new Uint8Array(sQ)).buffer; + } else { + // base64 encode + return Base64.encode(sQ); + } +} + +function decode_message(data) { + //Util.Debug(">> decode_message: " + data); + if (mode === 'binary') { + // push arraybuffer values onto the end + var u8 = new Uint8Array(data); + for (var i = 0; i < u8.length; i++) { + rQ.push(u8[i]); + } + } else { + // base64 decode and concat to the end + rQ = rQ.concat(Base64.decode(data, 0)); + } + //Util.Debug(">> decode_message, rQ: " + rQ); +} + + +// +// Public Send functions +// + +function flush() { + if (websocket.bufferedAmount !== 0) { + Util.Debug("bufferedAmount: " + websocket.bufferedAmount); + } + if (websocket.bufferedAmount < api.maxBufferedAmount) { + //Util.Debug("arr: " + arr); + //Util.Debug("sQ: " + sQ); + if (sQ.length > 0) { + websocket.send(encode_message(sQ)); + sQ = []; + } + return true; + } else { + Util.Info("Delaying send, bufferedAmount: " + + websocket.bufferedAmount); + return false; + } +} + +// overridable for testing +function send(arr) { + //Util.Debug(">> send_array: " + arr); + sQ = sQ.concat(arr); + return flush(); +} + +function send_string(str) { + //Util.Debug(">> send_string: " + str); + api.send(str.split('').map( + function (chr) { return chr.charCodeAt(0); } ) ); +} + +// +// Other public functions + +function recv_message(e) { + //Util.Debug(">> recv_message: " + e.data.length); + + try { + decode_message(e.data); + if (rQlen() > 0) { + eventHandlers.message(); + // Compact the receive queue + if (rQ.length > rQmax) { + //Util.Debug("Compacting receive queue"); + rQ = rQ.slice(rQi); + rQi = 0; + } + } else { + Util.Debug("Ignoring empty message"); + } + } catch (exc) { + if (typeof exc.stack !== 'undefined') { + Util.Warn("recv_message, caught exception: " + exc.stack); + } else if (typeof exc.description !== 'undefined') { + Util.Warn("recv_message, caught exception: " + exc.description); + } else { + Util.Warn("recv_message, caught exception:" + exc); + } + if (typeof exc.name !== 'undefined') { + eventHandlers.error(exc.name + ": " + exc.message); + } else { + eventHandlers.error(exc); + } + } + //Util.Debug("<< recv_message"); +} + + +// Set event handlers +function on(evt, handler) { + eventHandlers[evt] = handler; +} + +function init(protocols) { + rQ = []; + rQi = 0; + sQ = []; + websocket = null; + + var bt = false, + wsbt = false, + try_binary = false; + + // Check for full typed array support + if (('Uint8Array' in window) && + ('set' in Uint8Array.prototype)) { + bt = true; + } + + // Check for full binary type support in WebSockets + // TODO: this sucks, the property should exist on the prototype + // but it does not. + try { + if (bt && ('binaryType' in (new WebSocket("wss://localhost:17523")))) { + Util.Info("Detected binaryType support in WebSockets"); + wsbt = true; + } + } catch (exc) { + // Just ignore failed test localhost connections + } + + // Default protocols if not specified + if (typeof(protocols) === "undefined") { + if (wsbt) { + protocols = ['binary', 'base64']; + } else { + protocols = 'base64'; + } + } + // If no binary support, make sure it was not requested + if (!wsbt) { + if (protocols === 'binary') { + throw("WebSocket binary sub-protocol requested but not supported"); + } + if (typeof(protocols) === "object") { + var new_protocols = []; + for (var i = 0; i < protocols.length; i++) { + if (protocols[i] === 'binary') { + Util.Error("Skipping unsupported WebSocket binary sub-protocol!"); + } else { + new_protocols.push(protocols[i]); + } + } + if (new_protocols.length > 0) { + protocols = new_protocols; + } else { + throw("Only WebSocket binary sub-protocol was requested and not supported."); + } + } + } + + return protocols; +} + +function open(uri, protocols) { + protocols = init(protocols); + if (test_mode) { + websocket = {}; + } else { + websocket = new WebSocket(uri, protocols); + if (protocols.indexOf('binary') >= 0) { + websocket.binaryType = 'arraybuffer'; + } + } + + websocket.onmessage = recv_message; + websocket.onopen = function() { + Util.Error(">> WebSock.onopen"); + if (websocket.protocol) { + mode = websocket.protocol; + Util.Error("Server chose sub-protocol: " + websocket.protocol); + } else { + mode = 'base64'; + Util.Error("Server select no sub-protocol!: " + websocket.protocol); + } + eventHandlers.open(); + Util.Debug("<< WebSock.onopen"); + }; + websocket.onclose = function(e) { + Util.Debug(">> WebSock.onclose"); + eventHandlers.close(e); + Util.Debug("<< WebSock.onclose"); + }; + websocket.onerror = function(e) { + Util.Debug(">> WebSock.onerror: " + e); + eventHandlers.error(e); + Util.Debug("<< WebSock.onerror"); + }; +} + +function close() { + if (websocket) { + if ((websocket.readyState === WebSocket.OPEN) || + (websocket.readyState === WebSocket.CONNECTING)) { + Util.Info("Closing WebSocket connection"); + websocket.close(); + } + websocket.onmessage = function (e) { return; }; + } +} + +// Override internal functions for testing +// Takes a send function, returns reference to recv function +function testMode(override_send, data_mode) { + test_mode = true; + mode = data_mode; + api.send = override_send; + api.close = function () {}; + return recv_message; +} + +function constructor() { + // Configuration settings + api.maxBufferedAmount = 200; + + // Direct access to send and receive queues + api.get_sQ = get_sQ; + api.get_rQ = get_rQ; + api.get_rQi = get_rQi; + api.set_rQi = set_rQi; + + // Routines to read from the receive queue + api.rQlen = rQlen; + api.rQpeek8 = rQpeek8; + api.rQshift8 = rQshift8; + api.rQunshift8 = rQunshift8; + api.rQshift16 = rQshift16; + api.rQshift32 = rQshift32; + api.rQshiftStr = rQshiftStr; + api.rQshiftBytes = rQshiftBytes; + api.rQslice = rQslice; + api.rQwait = rQwait; + + api.flush = flush; + api.send = send; + api.send_string = send_string; + + api.on = on; + api.init = init; + api.open = open; + api.close = close; + api.testMode = testMode; + + return api; +} + +return constructor(); + +} diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/webutil.js b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/webutil.js new file mode 100644 index 0000000000000000000000000000000000000000..ebf8e8918139b5dea0adc0499ebf87fc19ce72b4 --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/include/webutil.js @@ -0,0 +1,216 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2012 Joel Martin + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +"use strict"; +/*jslint bitwise: false, white: false */ +/*global Util, window, document */ + +// Globals defined here +var WebUtil = {}, $D; + +/* + * Simple DOM selector by ID + */ +if (!window.$D) { + window.$D = function (id) { + if (document.getElementById) { + return document.getElementById(id); + } else if (document.all) { + return document.all[id]; + } else if (document.layers) { + return document.layers[id]; + } + return undefined; + }; +} + + +/* + * ------------------------------------------------------ + * Namespaced in WebUtil + * ------------------------------------------------------ + */ + +// init log level reading the logging HTTP param +WebUtil.init_logging = function(level) { + if (typeof level !== "undefined") { + Util._log_level = level; + } else { + Util._log_level = (document.location.href.match( + /logging=([A-Za-z0-9\._\-]*)/) || + ['', Util._log_level])[1]; + } + Util.init_logging(); +}; + + +WebUtil.dirObj = function (obj, depth, parent) { + var i, msg = "", val = ""; + if (! depth) { depth=2; } + if (! parent) { parent= ""; } + + // Print the properties of the passed-in object + for (i in obj) { + if ((depth > 1) && (typeof obj[i] === "object")) { + // Recurse attributes that are objects + msg += WebUtil.dirObj(obj[i], depth-1, parent + "." + i); + } else { + //val = new String(obj[i]).replace("\n", " "); + if (typeof(obj[i]) === "undefined") { + val = "undefined"; + } else { + val = obj[i].toString().replace("\n", " "); + } + if (val.length > 30) { + val = val.substr(0,30) + "..."; + } + msg += parent + "." + i + ": " + val + "\n"; + } + } + return msg; +}; + +// Read a query string variable +WebUtil.getQueryVar = function(name, defVal) { + var re = new RegExp('[?][^#]*' + name + '=([^&#]*)'), + match = document.location.href.match(re); + if (typeof defVal === 'undefined') { defVal = null; } + if (match) { + return decodeURIComponent(match[1]); + } else { + return defVal; + } +}; + + +/* + * Cookie handling. Dervied from: http://www.quirksmode.org/js/cookies.html + */ + +// No days means only for this browser session +WebUtil.createCookie = function(name,value,days) { + var date, expires; + if (days) { + date = new Date(); + date.setTime(date.getTime()+(days*24*60*60*1000)); + expires = "; expires="+date.toGMTString(); + } + else { + expires = ""; + } + document.cookie = name+"="+value+expires+"; path=/"; +}; + +WebUtil.readCookie = function(name, defaultValue) { + var i, c, nameEQ = name + "=", ca = document.cookie.split(';'); + for(i=0; i < ca.length; i += 1) { + c = ca[i]; + while (c.charAt(0) === ' ') { c = c.substring(1,c.length); } + if (c.indexOf(nameEQ) === 0) { return c.substring(nameEQ.length,c.length); } + } + return (typeof defaultValue !== 'undefined') ? defaultValue : null; +}; + +WebUtil.eraseCookie = function(name) { + WebUtil.createCookie(name,"",-1); +}; + +/* + * Setting handling. + */ + +WebUtil.initSettings = function(callback) { + var callbackArgs = Array.prototype.slice.call(arguments, 1); + if (window.chrome && window.chrome.storage) { + window.chrome.storage.sync.get(function (cfg) { + WebUtil.settings = cfg; + console.log(WebUtil.settings); + if (callback) { + callback.apply(this, callbackArgs); + } + }); + } else { + // No-op + if (callback) { + callback.apply(this, callbackArgs); + } + } +}; + +// No days means only for this browser session +WebUtil.writeSetting = function(name, value) { + if (window.chrome && window.chrome.storage) { + //console.log("writeSetting:", name, value); + if (WebUtil.settings[name] !== value) { + WebUtil.settings[name] = value; + window.chrome.storage.sync.set(WebUtil.settings); + } + } else { + localStorage.setItem(name, value); + } +}; + +WebUtil.readSetting = function(name, defaultValue) { + var value; + if (window.chrome && window.chrome.storage) { + value = WebUtil.settings[name]; + } else { + value = localStorage.getItem(name); + } + if (typeof value === "undefined") { + value = null; + } + if (value === null && typeof defaultValue !== undefined) { + return defaultValue; + } else { + return value; + } +}; + +WebUtil.eraseSetting = function(name) { + if (window.chrome && window.chrome.storage) { + window.chrome.storage.sync.remove(name); + delete WebUtil.settings[name]; + } else { + localStorage.removeItem(name); + } +}; + +/* + * Alternate stylesheet selection + */ +WebUtil.getStylesheets = function() { var i, links, sheets = []; + links = document.getElementsByTagName("link"); + for (i = 0; i < links.length; i += 1) { + if (links[i].title && + links[i].rel.toUpperCase().indexOf("STYLESHEET") > -1) { + sheets.push(links[i]); + } + } + return sheets; +}; + +// No sheet means try and use value from cookie, null sheet used to +// clear all alternates. +WebUtil.selectStylesheet = function(sheet) { + var i, link, sheets = WebUtil.getStylesheets(); + if (typeof sheet === 'undefined') { + sheet = 'default'; + } + for (i=0; i < sheets.length; i += 1) { + link = sheets[i]; + if (link.title === sheet) { + Util.Debug("Using stylesheet " + sheet); + link.disabled = false; + } else { + //Util.Debug("Skipping stylesheet " + link.title); + link.disabled = true; + } + } + return sheet; +}; diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/more/keyboard.css b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/more/keyboard.css new file mode 100644 index 0000000000000000000000000000000000000000..446dd53458f9f0932d0e99fa3039d98fc0bd952f --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/more/keyboard.css @@ -0,0 +1,271 @@ +#keyboardInputMaster { + position:absolute; + font:normal 11px Arial,sans-serif; + border-top:1px solid #eeeeee; + border-right:1px solid #888888; + border-bottom:1px solid #444444; + border-left:1px solid #cccccc; + -webkit-border-radius:0.6em; + -moz-border-radius:0.6em; + border-radius:0.6em; + -webkit-box-shadow:0px 2px 10px #444444; + -moz-box-shadow:0px 2px 10px #444444; + box-shadow:0px 2px 10px #444444; + opacity:0.95; + filter:alpha(opacity=95); + background-color:#dddddd; + text-align:left; + z-index:1000000; + width:auto; + height:auto; + min-width:0; + min-height:0; + margin:0px; + padding:0px; + line-height:normal; + -moz-user-select:none; + cursor:default; +} +#keyboardInputMaster * { + position:static; + color:#000000; + background:transparent; + font:normal 11px Arial,sans-serif; + width:auto; + height:auto; + min-width:0; + min-height:0; + margin:0px; + padding:0px; + border:0px none; + outline:0px; + vertical-align:baseline; + line-height:1.3em; +} +#keyboardInputMaster table { + table-layout:auto; +} +#keyboardInputMaster.keyboardInputSize1, +#keyboardInputMaster.keyboardInputSize1 * { + font-size:9px; +} +#keyboardInputMaster.keyboardInputSize3, +#keyboardInputMaster.keyboardInputSize3 * { + font-size:13px; +} +#keyboardInputMaster.keyboardInputSize4, +#keyboardInputMaster.keyboardInputSize4 * { + font-size:16px; +} +#keyboardInputMaster.keyboardInputSize5, +#keyboardInputMaster.keyboardInputSize5 * { + font-size:20px; +} + +#keyboardInputMaster thead tr th { + padding:0.3em 0.3em 0.1em 0.3em; + background-color:#999999; + white-space:nowrap; + text-align:right; + -webkit-border-radius:0.6em 0.6em 0px 0px; + -moz-border-radius:0.6em 0.6em 0px 0px; + border-radius:0.6em 0.6em 0px 0px; +} +#keyboardInputMaster thead tr th div { + float:left; + font-size:130% !important; + height:1.3em; + font-weight:bold; + position:relative; + z-index:1; + margin-right:0.5em; + cursor:pointer; + background-color:transparent; +} +#keyboardInputMaster thead tr th div ol { + position:absolute; + left:0px; + top:90%; + list-style-type:none; + height:9.4em; + overflow-y:auto; + overflow-x:hidden; + background-color:#f6f6f6; + border:1px solid #999999; + display:none; + text-align:left; + width:12em; +} +#keyboardInputMaster thead tr th div ol li { + padding:0.2em 0.4em; + cursor:pointer; + white-space:nowrap; + width:12em; +} +#keyboardInputMaster thead tr th div ol li.selected { + background-color:#ffffcc; +} +#keyboardInputMaster thead tr th div ol li:hover, +#keyboardInputMaster thead tr th div ol li.hover { + background-color:#dddddd; +} +#keyboardInputMaster thead tr th span, +#keyboardInputMaster thead tr th strong, +#keyboardInputMaster thead tr th small, +#keyboardInputMaster thead tr th big { + display:inline-block; + padding:0px 0.4em; + height:1.4em; + line-height:1.4em; + border-top:1px solid #e5e5e5; + border-right:1px solid #5d5d5d; + border-bottom:1px solid #5d5d5d; + border-left:1px solid #e5e5e5; + background-color:#cccccc; + cursor:pointer; + margin:0px 0px 0px 0.3em; + -webkit-border-radius:0.3em; + -moz-border-radius:0.3em; + border-radius:0.3em; + vertical-align:middle; + -webkit-transition:background-color .15s ease-in-out; + -o-transition:background-color .15s ease-in-out; + transition:background-color .15s ease-in-out; +} +#keyboardInputMaster thead tr th strong { + font-weight:bold; +} +#keyboardInputMaster thead tr th small { + -webkit-border-radius:0.3em 0px 0px 0.3em; + -moz-border-radius:0.3em 0px 0px 0.3em; + border-radius:0.3em 0px 0px 0.3em; + border-right:1px solid #aaaaaa; + padding:0px 0.2em 0px 0.3em; +} +#keyboardInputMaster thead tr th big { + -webkit-border-radius:0px 0.3em 0.3em 0px; + -moz-border-radius:0px 0.3em 0.3em 0px; + border-radius:0px 0.3em 0.3em 0px; + border-left:0px none; + margin:0px; + padding:0px 0.3em 0px 0.2em; +} +#keyboardInputMaster thead tr th span:hover, +#keyboardInputMaster thead tr th span.hover, +#keyboardInputMaster thead tr th strong:hover, +#keyboardInputMaster thead tr th strong.hover, +#keyboardInputMaster thead tr th small:hover, +#keyboardInputMaster thead tr th small.hover, +#keyboardInputMaster thead tr th big:hover, +#keyboardInputMaster thead tr th big.hover { + background-color:#dddddd; +} + +#keyboardInputMaster tbody tr td { + text-align:left; + padding:0.2em 0.3em 0.3em 0.3em; + vertical-align:top; +} +#keyboardInputMaster tbody tr td div { + text-align:center; + position:relative; + zoom:1; +} +#keyboardInputMaster tbody tr td table { + white-space:nowrap; + width:100%; + border-collapse:separate; + border-spacing:0px; +} +#keyboardInputMaster tbody tr td#keyboardInputNumpad table { + margin-left:0.2em; + width:auto; +} +#keyboardInputMaster tbody tr td table.keyboardInputCenter { + width:auto; + margin:0px auto; +} +#keyboardInputMaster tbody tr td table tbody tr td { + vertical-align:middle; + padding:0px 0.45em; + white-space:pre; + height:1.8em; + font-family:'Lucida Console','Arial Unicode MS',monospace; + border-top:1px solid #e5e5e5; + border-right:1px solid #5d5d5d; + border-bottom:1px solid #5d5d5d; + border-left:1px solid #e5e5e5; + background-color:#eeeeee; + cursor:default; + min-width:0.75em; + -webkit-border-radius:0.2em; + -moz-border-radius:0.2em; + border-radius:0.2em; + -webkit-transition:background-color .15s ease-in-out; + -o-transition:background-color .15s ease-in-out; + transition:background-color .15s ease-in-out; +} +#keyboardInputMaster tbody tr td table tbody tr td.last { + width:99%; +} +#keyboardInputMaster tbody tr td table tbody tr td.space { + padding:0px 4em; +} +#keyboardInputMaster tbody tr td table tbody tr td.deadkey { + background-color:#ccccdd; +} +#keyboardInputMaster tbody tr td table tbody tr td.target { + background-color:#ddddcc; +} +#keyboardInputMaster tbody tr td table tbody tr td:hover, +#keyboardInputMaster tbody tr td table tbody tr td.hover { + border-top:1px solid #d5d5d5; + border-right:1px solid #555555; + border-bottom:1px solid #555555; + border-left:1px solid #d5d5d5; + background-color:#cccccc; +} +#keyboardInputMaster thead tr th span:active, +#keyboardInputMaster thead tr th span.pressed, +#keyboardInputMaster tbody tr td table tbody tr td:active, +#keyboardInputMaster tbody tr td table tbody tr td.pressed { + border-top:1px solid #555555 !important; + border-right:1px solid #d5d5d5; + border-bottom:1px solid #d5d5d5; + border-left:1px solid #555555; + background-color:#cccccc; +} + +#keyboardInputMaster tbody tr td table tbody tr td small { + display:block; + text-align:center; + font-size:0.6em !important; + line-height:1.1em; +} + +#keyboardInputMaster tbody tr td div label { + position:absolute; + bottom:0.2em; + left:0.3em; +} +#keyboardInputMaster tbody tr td div label input { + background-color:#f6f6f6; + vertical-align:middle; + font-size:inherit; + width:1.1em; + height:1.1em; +} +#keyboardInputMaster tbody tr td div var { + position:absolute; + bottom:0px; + right:3px; + font-weight:bold; + font-style:italic; + color:#444444; +} + +.keyboardInputInitiator { + margin:0px 3px; + vertical-align:middle; + cursor:pointer; +} diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/more/keyboard.js b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/more/keyboard.js new file mode 100644 index 0000000000000000000000000000000000000000..d43c6ed43325da7cc995a2f2888141eb47a8158c --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/more/keyboard.js @@ -0,0 +1,1798 @@ +/* ******************************************************************** + ********************************************************************** + * HTML Virtual Keyboard Interface Script - v1.49 + * Copyright (c) 2011 - GreyWyvern + * + * - Licenced for free distribution under the BSDL + * http://www.opensource.org/licenses/bsd-license.php + * + * Add a script-driven keyboard interface to text fields, password + * fields and textareas. + * + * See http://www.greywyvern.com/code/javascript/keyboard for examples + * and usage instructions. + * + * Version 1.49 - November 8, 2011 + * - Don't display language drop-down if only one keyboard available + * + * See full changelog at: + * http://www.greywyvern.com/code/javascript/keyboard.changelog.txt + * + * Keyboard Credits + * - Yiddish (Yidish Lebt) keyboard layout by Simche Taub (jidysz.net) + * - Urdu Phonetic keyboard layout by Khalid Malik + * - Yiddish keyboard layout by Helmut Wollmersdorfer + * - Khmer keyboard layout by Sovann Heng (km-kh.com) + * - Dari keyboard layout by Saif Fazel + * - Kurdish keyboard layout by Ara Qadir + * - Assamese keyboard layout by Kanchan Gogoi + * - Bulgarian BDS keyboard layout by Milen Georgiev + * - Basic Japanese Hiragana/Katakana keyboard layout by Damjan + * - Ukrainian keyboard layout by Dmitry Nikitin + * - Macedonian keyboard layout by Damjan Dimitrioski + * - Pashto keyboard layout by Ahmad Wali Achakzai (qamosona.com) + * - Armenian Eastern and Western keyboard layouts by Hayastan Project (www.hayastan.co.uk) + * - Pinyin keyboard layout from a collaboration with Lou Winklemann + * - Kazakh keyboard layout by Alex Madyankin + * - Danish keyboard layout by Verner Kjærsgaard + * - Slovak keyboard layout by Daniel Lara (www.learningslovak.com) + * - Belarusian and Serbian Cyrillic keyboard layouts by Evgeniy Titov + * - Bulgarian Phonetic keyboard layout by Samuil Gospodinov + * - Swedish keyboard layout by HÃ¥kan Sandberg + * - Romanian keyboard layout by Aurel + * - Farsi (Persian) keyboard layout by Kaveh Bakhtiyari (www.bakhtiyari.com) + * - Burmese keyboard layout by Cetanapa + * - Bosnian/Croatian/Serbian Latin/Slovenian keyboard layout by Miran Zeljko + * - Hungarian keyboard layout by Antal Sall 'Hiromacu' + * - Arabic keyboard layout by Srinivas Reddy + * - Italian and Spanish (Spain) keyboard layouts by dictionarist.com + * - Lithuanian and Russian keyboard layouts by Ramunas + * - German keyboard layout by QuHno + * - French keyboard layout by Hidden Evil + * - Polish Programmers layout by moose + * - Turkish keyboard layouts by offcu + * - Dutch and US Int'l keyboard layouts by jerone + * + */ +var VKI_attach, VKI_close; +(function() { + var self = this; + + this.VKI_version = "1.49"; + this.VKI_showVersion = true; + this.VKI_target = false; + this.VKI_shift = this.VKI_shiftlock = false; + this.VKI_altgr = this.VKI_altgrlock = false; + this.VKI_dead = false; + this.VKI_deadBox = true; // Show the dead keys checkbox + this.VKI_deadkeysOn = false; // Turn dead keys on by default + this.VKI_numberPad = true; // Allow user to open and close the number pad + this.VKI_numberPadOn = false; // Show number pad by default + this.VKI_kts = this.VKI_kt = "US International"; // Default keyboard layout + this.VKI_langAdapt = true; // Use lang attribute of input to select keyboard + this.VKI_size = 2; // Default keyboard size (1-5) + this.VKI_sizeAdj = true; // Allow user to adjust keyboard size + this.VKI_clearPasswords = false; // Clear password fields on focus + this.VKI_imageURI = "keyboard.png"; // If empty string, use imageless mode + this.VKI_clickless = 0; // 0 = disabled, > 0 = delay in ms + this.VKI_activeTab = 0; // Tab moves to next: 1 = element, 2 = keyboard enabled element + this.VKI_enterSubmit = true; // Submit forms when Enter is pressed + this.VKI_keyCenter = 3; + + this.VKI_isIE = /*@cc_on!@*/false; + this.VKI_isIE6 = /*@if(@_jscript_version == 5.6)!@end@*/false; + this.VKI_isIElt8 = /*@if(@_jscript_version < 5.8)!@end@*/false; + this.VKI_isWebKit = RegExp("KHTML").test(navigator.userAgent); + this.VKI_isOpera = RegExp("Opera").test(navigator.userAgent); + this.VKI_isMoz = (!this.VKI_isWebKit && navigator.product == "Gecko"); + + /* ***** i18n text strings ************************************* */ + this.VKI_i18n = { + '00': "Display Number Pad", + '01': "Display virtual keyboard interface", + '02': "Select keyboard layout", + '03': "Dead keys", + '04': "On", + '05': "Off", + '06': "Close the keyboard", + '07': "Clear", + '08': "Clear this input", + '09': "Version", + '10': "Decrease keyboard size", + '11': "Increase keyboard size" + }; + + + /* ***** Create keyboards ************************************** */ + this.VKI_layout = {}; + + // - Lay out each keyboard in rows of sub-arrays. Each sub-array + // represents one key. + // + // - Each sub-array consists of four slots described as follows: + // example: ["a", "A", "\u00e1", "\u00c1"] + // + // a) Normal character + // A) Character + Shift/Caps + // \u00e1) Character + Alt/AltGr/AltLk + // \u00c1) Character + Shift/Caps + Alt/AltGr/AltLk + // + // You may include sub-arrays which are fewer than four slots. + // In these cases, the missing slots will be blanked when the + // corresponding modifier key (Shift or AltGr) is pressed. + // + // - If the second slot of a sub-array matches one of the following + // strings: + // "Tab", "Caps", "Shift", "Enter", "Bksp", + // "Alt" OR "AltGr", "AltLk" + // then the function of the key will be the following, + // respectively: + // - Insert a tab + // - Toggle Caps Lock (technically a Shift Lock) + // - Next entered character will be the shifted character + // - Insert a newline (textarea), or close the keyboard + // - Delete the previous character + // - Next entered character will be the alternate character + // - Toggle Alt/AltGr Lock + // + // The first slot of this sub-array will be the text to display + // on the corresponding key. This allows for easy localisation + // of key names. + // + // - Layout dead keys (diacritic + letter) should be added as + // property/value pairs of objects with hash keys equal to the + // diacritic. See the "this.VKI_deadkey" object below the layout + // definitions. In each property/value pair, the value is what + // the diacritic would change the property name to. + // + // - Note that any characters beyond the normal ASCII set should be + // entered in escaped Unicode format. (eg \u00a3 = Pound symbol) + // You can find Unicode values for characters here: + // http://unicode.org/charts/ + // + // - To remove a keyboard, just delete it, or comment it out of the + // source code. If you decide to remove the US International + // keyboard layout, make sure you change the default layout + // (this.VKI_kt) above so it references an existing layout. + + this.VKI_layout['\u0627\u0644\u0639\u0631\u0628\u064a\u0629'] = { + 'name': "Arabic", 'keys': [ + [["\u0630", "\u0651 "], ["1", "!", "\u00a1", "\u00b9"], ["2", "@", "\u00b2"], ["3", "#", "\u00b3"], ["4", "$", "\u00a4", "\u00a3"], ["5", "%", "\u20ac"], ["6", "^", "\u00bc"], ["7", "&", "\u00bd"], ["8", "*", "\u00be"], ["9", "(", "\u2018"], ["0", ")", "\u2019"], ["-", "_", "\u00a5"], ["=", "+", "\u00d7", "\u00f7"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0636", "\u064e"], ["\u0635", "\u064b"], ["\u062b", "\u064f"], ["\u0642", "\u064c"], ["\u0641", "\u0644"], ["\u063a", "\u0625"], ["\u0639", "\u2018"], ["\u0647", "\u00f7"], ["\u062e", "\u00d7"], ["\u062d", "\u061b"], ["\u062c", "<"], ["\u062f", ">"], ["\\", "|"]], + [["Caps", "Caps"], ["\u0634", "\u0650"], ["\u0633", "\u064d"], ["\u064a", "]"], ["\u0628", "["], ["\u0644", "\u0644"], ["\u0627", "\u0623"], ["\u062a", "\u0640"], ["\u0646", "\u060c"], ["\u0645", "/"], ["\u0643", ":"], ["\u0637", '"'], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0626", "~"], ["\u0621", "\u0652"], ["\u0624", "}"], ["\u0631", "{"], ["\u0644", "\u0644"], ["\u0649", "\u0622"], ["\u0629", "\u2019"], ["\u0648", ","], ["\u0632", "."], ["\u0638", "\u061f"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["Alt", "Alt"]] + ], 'lang': ["ar"] }; + + this.VKI_layout['\u0985\u09b8\u09ae\u09c0\u09df\u09be'] = { + 'name': "Assamese", 'keys': [ + [["+", "?"], ["\u09E7", "{", "\u09E7"], ["\u09E8", "}", "\u09E8"], ["\u09E9", "\u09CD\u09F0", "\u09E9"], ["\u09EA", "\u09F0\u09CD", "\u09EA"], ["\u09EB", "\u099C\u09CD\u09F0", "\u09EB"], ["\u09EC", "\u0995\u09CD\u09B7", "\u09EC"], ["\u09ED", "\u0995\u09CD\u09F0", "\u09ED"], ["\u09EE", "\u09B6\u09CD\u09F0", "\u09EE"], ["\u09EF", "(", "\u09EF"], ["\u09E6", ")", "\u09E6"], ["-", ""], ["\u09C3", "\u098B", "\u09E2", "\u09E0"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u09CC", "\u0994", "\u09D7"], ["\u09C8", "\u0990"], ["\u09BE", "\u0986"], ["\u09C0", "\u0988", "\u09E3", "\u09E1"], ["\u09C2", "\u098A"], ["\u09F1", "\u09AD"], ["\u09B9", "\u0999"], ["\u0997", "\u0998"], ["\u09A6", "\u09A7"], ["\u099C", "\u099D"], ["\u09A1", "\u09A2", "\u09DC", "\u09DD"], ["Enter", "Enter"]], + [["Caps", "Caps"], ["\u09CB", "\u0993", "\u09F4", "\u09F5"], ["\u09C7", "\u098F", "\u09F6", "\u09F7"], ["\u09CD", "\u0985", "\u09F8", "\u09F9"], ["\u09BF", "\u0987", "\u09E2", "\u098C"], ["\u09C1", "\u0989"], ["\u09AA", "\u09AB"], ["\u09F0", "", "\u09F0", "\u09F1"], ["\u0995", "\u0996"], ["\u09A4", "\u09A5"], ["\u099A", "\u099B"], ["\u099F", "\u09A0"], ["\u09BC", "\u099E"]], + [["Shift", "Shift"], ["\u09CE", "\u0983"], ["\u0982", "\u0981", "\u09FA"], ["\u09AE", "\u09A3"], ["\u09A8", "\u09F7"], ["\u09AC", "\""], ["\u09B2", "'"], ["\u09B8", "\u09B6"], [",", "\u09B7"], [".", ";"], ["\u09AF", "\u09DF"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["as"] }; + + this.VKI_layout['\u0410\u0437\u04d9\u0440\u0431\u0430\u0458\u04b9\u0430\u043d\u04b9\u0430'] = { + 'name': "Azerbaijani Cyrillic", 'keys': [ + [["`", "~"], ["1", "!"], ["2", '"'], ["3", "\u2116"], ["4", ";"], ["5", "%"], ["6", ":"], ["7", "?"], ["8", "*"], ["9", "("], ["0", ")"], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0458", "\u0408"], ["\u04AF", "\u04AE"], ["\u0443", "\u0423"], ["\u043A", "\u041A"], ["\u0435", "\u0415"], ["\u043D", "\u041D"], ["\u0433", "\u0413"], ["\u0448", "\u0428"], ["\u04BB", "\u04BA"], ["\u0437", "\u0417"], ["\u0445", "\u0425"], ["\u04B9", "\u04B8"], ["\\", "/"]], + [["Caps", "Caps"], ["\u0444", "\u0424"], ["\u044B", "\u042B"], ["\u0432", "\u0412"], ["\u0430", "\u0410"], ["\u043F", "\u041F"], ["\u0440", "\u0420"], ["\u043E", "\u041E"], ["\u043B", "\u041B"], ["\u0434", "\u0414"], ["\u0436", "\u0416"], ["\u049D", "\u049C"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\\", "|"], ["\u04D9", "\u04D8"], ["\u0447", "\u0427"], ["\u0441", "\u0421"], ["\u043C", "\u041C"], ["\u0438", "\u0418"], ["\u0442", "\u0422"], ["\u0493", "\u0492"], ["\u0431", "\u0411"], ["\u04E9", "\u04E8"], [".", ","], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["az-Cyrl"] }; + + this.VKI_layout['Az\u0259rbaycanca'] = { + 'name': "Azerbaijani Latin", 'keys': [ + [["`", "~"], ["1", "!"], ["2", '"'], ["3", "\u2166"], ["4", ";"], ["5", "%"], ["6", ":"], ["7", "?"], ["8", "*"], ["9", "("], ["0", ")"], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["\u00FC", "\u00DC"], ["e", "E"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "\u0130"], ["o", "O"], ["p", "P"], ["\u00F6", "\u00D6"], ["\u011F", "\u011E"], ["\\", "/"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["\u0131", "I"], ["\u0259", "\u018F"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["z", "Z"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M"], ["\u00E7", "\u00C7"], ["\u015F", "\u015E"], [".", ","], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["az"] }; + + this.VKI_layout['\u0411\u0435\u043b\u0430\u0440\u0443\u0441\u043a\u0430\u044f'] = { + 'name': "Belarusian", 'keys': [ + [["\u0451", "\u0401"], ["1", "!"], ["2", '"'], ["3", "\u2116"], ["4", ";"], ["5", "%"], ["6", ":"], ["7", "?"], ["8", "*"], ["9", "("], ["0", ")"], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0439", "\u0419"], ["\u0446", "\u0426"], ["\u0443", "\u0423"], ["\u043a", "\u041a"], ["\u0435", "\u0415"], ["\u043d", "\u041d"], ["\u0433", "\u0413"], ["\u0448", "\u0428"], ["\u045e", "\u040e"], ["\u0437", "\u0417"], ["\u0445", "\u0425"], ["'", "'"], ["\\", "/"]], + [["Caps", "Caps"], ["\u0444", "\u0424"], ["\u044b", "\u042b"], ["\u0432", "\u0412"], ["\u0430", "\u0410"], ["\u043f", "\u041f"], ["\u0440", "\u0420"], ["\u043e", "\u041e"], ["\u043b", "\u041b"], ["\u0434", "\u0414"], ["\u0436", "\u0416"], ["\u044d", "\u042d"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["/", "|"], ["\u044f", "\u042f"], ["\u0447", "\u0427"], ["\u0441", "\u0421"], ["\u043c", "\u041c"], ["\u0456", "\u0406"], ["\u0442", "\u0422"], ["\u044c", "\u042c"], ["\u0431", "\u0411"], ["\u044e", "\u042e"], [".", ","], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["be"] }; + + this.VKI_layout['Belgische / Belge'] = { + 'name': "Belgian", 'keys': [ + [["\u00b2", "\u00b3"], ["&", "1", "|"], ["\u00e9", "2", "@"], ['"', "3", "#"], ["'", "4"], ["(", "5"], ["\u00a7", "6", "^"], ["\u00e8", "7"], ["!", "8"], ["\u00e7", "9", "{"], ["\u00e0", "0", "}"], [")", "\u00b0"], ["-", "_"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["a", "A"], ["z", "Z"], ["e", "E", "\u20ac"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["^", "\u00a8", "["], ["$", "*", "]"], ["\u03bc", "\u00a3", "`"]], + [["Caps", "Caps"], ["q", "Q"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["m", "M"], ["\u00f9", "%", "\u00b4"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">", "\\"], ["w", "W"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], [",", "?"], [";", "."], [":", "/"], ["=", "+", "~"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["nl-BE", "fr-BE"] }; + + this.VKI_layout['\u0411\u044a\u043b\u0433\u0430\u0440\u0441\u043a\u0438 \u0424\u043e\u043d\u0435\u0442\u0438\u0447\u0435\u043d'] = { + 'name': "Bulgarian Phonetic", 'keys': [ + [["\u0447", "\u0427"], ["1", "!"], ["2", "@"], ["3", "#"], ["4", "$"], ["5", "%"], ["6", "^"], ["7", "&"], ["8", "*"], ["9", "("], ["0", ")"], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u044F", "\u042F"], ["\u0432", "\u0412"], ["\u0435", "\u0415"], ["\u0440", "\u0420"], ["\u0442", "\u0422"], ["\u044A", "\u042A"], ["\u0443", "\u0423"], ["\u0438", "\u0418"], ["\u043E", "\u041E"], ["\u043F", "\u041F"], ["\u0448", "\u0428"], ["\u0449", "\u0429"], ["\u044E", "\u042E"]], + [["Caps", "Caps"], ["\u0430", "\u0410"], ["\u0441", "\u0421"], ["\u0434", "\u0414"], ["\u0444", "\u0424"], ["\u0433", "\u0413"], ["\u0445", "\u0425"], ["\u0439", "\u0419"], ["\u043A", "\u041A"], ["\u043B", "\u041B"], [";", ":"], ["'", '"'], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0437", "\u0417"], ["\u044C", "\u042C"], ["\u0446", "\u0426"], ["\u0436", "\u0416"], ["\u0431", "\u0411"], ["\u043D", "\u041D"], ["\u043C", "\u041C"], [",", "<"], [".", ">"], ["/", "?"], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["bg"] }; + + this.VKI_layout['\u0411\u044a\u043b\u0433\u0430\u0440\u0441\u043a\u0438'] = { + 'name': "Bulgarian BDS", 'keys': [ + [["`", "~"], ["1", "!"], ["2", "?"], ["3", "+"], ["4", '"'], ["5", "%"], ["6", "="], ["7", ":"], ["8", "/"], ["9", "_"], ["0", "\u2116"], ["-", "\u0406"], ["=", "V"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], [",", "\u044b"], ["\u0443", "\u0423"], ["\u0435", "\u0415"], ["\u0438", "\u0418"], ["\u0448", "\u0428"], ["\u0449", "\u0429"], ["\u043a", "\u041a"], ["\u0441", "\u0421"], ["\u0434", "\u0414"], ["\u0437", "\u0417"], ["\u0446", "\u0426"], [";", "\u00a7"], ["(", ")"]], + [["Caps", "Caps"], ["\u044c", "\u042c"], ["\u044f", "\u042f"], ["\u0430", "\u0410"], ["\u043e", "\u041e"], ["\u0436", "\u0416"], ["\u0433", "\u0413"], ["\u0442", "\u0422"], ["\u043d", "\u041d"], ["\u0412", "\u0412"], ["\u043c", "\u041c"], ["\u0447", "\u0427"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u042e", "\u044e"], ["\u0439", "\u0419"], ["\u044a", "\u042a"], ["\u044d", "\u042d"], ["\u0444", "\u0424"], ["\u0445", "\u0425"], ["\u043f", "\u041f"], ["\u0440", "\u0420"], ["\u043b", "\u041b"], ["\u0431", "\u0411"], ["Shift", "Shift"]], + [[" ", " "]] + ]}; + + this.VKI_layout['\u09ac\u09be\u0982\u09b2\u09be'] = { + 'name': "Bengali", 'keys': [ + [[""], ["1", "", "\u09E7"], ["2", "", "\u09E8"], ["3", "\u09CD\u09B0", "\u09E9"], ["4", "\u09B0\u09CD", "\u09EA"], ["5", "\u099C\u09CD\u09B0", "\u09EB"], ["6", "\u09A4\u09CD\u09B7", "\u09EC"], ["7", "\u0995\u09CD\u09B0", "\u09ED"], ["8", "\u09B6\u09CD\u09B0", "\u09EE"], ["9", "(", "\u09EF"], ["0", ")", "\u09E6"], ["-", "\u0983"], ["\u09C3", "\u098B", "\u09E2", "\u09E0"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u09CC", "\u0994", "\u09D7"], ["\u09C8", "\u0990"], ["\u09BE", "\u0986"], ["\u09C0", "\u0988", "\u09E3", "\u09E1"], ["\u09C2", "\u098A"], ["\u09AC", "\u09AD"], ["\u09B9", "\u0999"], ["\u0997", "\u0998"], ["\u09A6", "\u09A7"], ["\u099C", "\u099D"], ["\u09A1", "\u09A2", "\u09DC", "\u09DD"], ["Enter", "Enter"]], + [["Caps", "Caps"], ["\u09CB", "\u0993", "\u09F4", "\u09F5"], ["\u09C7", "\u098F", "\u09F6", "\u09F7"], ["\u09CD", "\u0985", "\u09F8", "\u09F9"], ["\u09BF", "\u0987", "\u09E2", "\u098C"], ["\u09C1", "\u0989"], ["\u09AA", "\u09AB"], ["\u09B0", "", "\u09F0", "\u09F1"], ["\u0995", "\u0996"], ["\u09A4", "\u09A5"], ["\u099A", "\u099B"], ["\u099F", "\u09A0"], ["\u09BC", "\u099E"]], + [["Shift", "Shift"], [""], ["\u0982", "\u0981", "\u09FA"], ["\u09AE", "\u09A3"], ["\u09A8"], ["\u09AC"], ["\u09B2"], ["\u09B8", "\u09B6"], [",", "\u09B7"], [".", "{"], ["\u09AF", "\u09DF"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["bn"] }; + + this.VKI_layout['Bosanski'] = { + 'name': "Bosnian", 'keys': [ + [["\u00B8", "\u00A8"], ["1", "!", "~"], ["2", '"', "\u02C7"], ["3", "#", "^"], ["4", "$", "\u02D8"], ["5", "%", "\u00B0"], ["6", "&", "\u02DB"], ["7", "/", "`"], ["8", "(", "\u02D9"], ["9", ")", "\u00B4"], ["0", "=", "\u02DD"], ["'", "?", "\u00A8"], ["+", "*", "\u00B8"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q", "\\"], ["w", "W", "|"], ["e", "E", "\u20AC"], ["r", "R"], ["t", "T"], ["z", "Z"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["\u0161", "\u0160", "\u00F7"], ["\u0111", "\u0110", "\u00D7"], ["\u017E", "\u017D", "\u00A4"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F", "["], ["g", "G", "]"], ["h", "H"], ["j", "J"], ["k", "K", "\u0142"], ["l", "L", "\u0141"], ["\u010D", "\u010C"], ["\u0107", "\u0106", "\u00DF"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">"], ["y", "Y"], ["x", "X"], ["c", "C"], ["v", "V", "@"], ["b", "B", "{"], ["n", "N", "}"], ["m", "M", "\u00A7"], [",", ";", "<"], [".", ":", ">"], ["-", "_", "\u00A9"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["bs"] }; + + this.VKI_layout['Canadienne-fran\u00e7aise'] = { + 'name': "Canadian French", 'keys': [ + [["#", "|", "\\"], ["1", "!", "\u00B1"], ["2", '"', "@"], ["3", "/", "\u00A3"], ["4", "$", "\u00A2"], ["5", "%", "\u00A4"], ["6", "?", "\u00AC"], ["7", "&", "\u00A6"], ["8", "*", "\u00B2"], ["9", "(", "\u00B3"], ["0", ")", "\u00BC"], ["-", "_", "\u00BD"], ["=", "+", "\u00BE"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O", "\u00A7"], ["p", "P", "\u00B6"], ["^", "^", "["], ["\u00B8", "\u00A8", "]"], ["<", ">", "}"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], [";", ":", "~"], ["`", "`", "{"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u00AB", "\u00BB", "\u00B0"], ["z", "Z"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M", "\u00B5"], [",", "'", "\u00AF"], [".", ".", "\u00AD"], ["\u00E9", "\u00C9", "\u00B4"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["fr-CA"] }; + + this.VKI_layout['\u010cesky'] = { + 'name': "Czech", 'keys': [ + [[";", "\u00b0", "`", "~"], ["+", "1", "!"], ["\u011B", "2", "@"], ["\u0161", "3", "#"], ["\u010D", "4", "$"], ["\u0159", "5", "%"], ["\u017E", "6", "^"], ["\u00FD", "7", "&"], ["\u00E1", "8", "*"], ["\u00ED", "9", "("], ["\u00E9", "0", ")"], ["=", "%", "-", "_"], ["\u00B4", "\u02c7", "=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E", "\u20AC"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["\u00FA", "/", "[", "{"], [")", "(", "]", "}"], ["\u00A8", "'", "\\", "|"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["\u016F", '"', ";", ":"], ["\u00A7", "!", "\u00a4", "^"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\\", "|", "", "\u02dd"], ["z", "Z"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M"], [",", "?", "<", "\u00d7"], [".", ":", ">", "\u00f7"], ["-", "_", "/", "?"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["Alt", "Alt"]] + ], 'lang': ["cs"] }; + + this.VKI_layout['Dansk'] = { + 'name': "Danish", 'keys': [ + [["\u00bd", "\u00a7"], ["1", "!"], ["2", '"', "@"], ["3", "#", "\u00a3"], ["4", "\u00a4", "$"], ["5", "%", "\u20ac"], ["6", "&"], ["7", "/", "{"], ["8", "(", "["], ["9", ")", "]"], ["0", "=", "}"], ["+", "?"], ["\u00b4", "`", "|"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E", "\u20ac"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["\u00e5", "\u00c5"], ["\u00a8", "^", "~"], ["'", "*"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["\u00e6", "\u00c6"], ["\u00f8", "\u00d8"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">", "\\"], ["z", "Z"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M", "\u03bc", "\u039c"], [",", ";"], [".", ":"], ["-", "_"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["da"] }; + + this.VKI_layout['Deutsch'] = { + 'name': "German", 'keys': [ + [["^", "\u00b0"], ["1", "!"], ["2", '"', "\u00b2"], ["3", "\u00a7", "\u00b3"], ["4", "$"], ["5", "%"], ["6", "&"], ["7", "/", "{"], ["8", "(", "["], ["9", ")", "]"], ["0", "=", "}"], ["\u00df", "?", "\\"], ["\u00b4", "`"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q", "@"], ["w", "W"], ["e", "E", "\u20ac"], ["r", "R"], ["t", "T"], ["z", "Z"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["\u00fc", "\u00dc"], ["+", "*", "~"], ["#", "'"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["\u00f6", "\u00d6"], ["\u00e4", "\u00c4"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">", "\u00a6"], ["y", "Y"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M", "\u00b5"], [",", ";"], [".", ":"], ["-", "_"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["de"] }; + + this.VKI_layout['Dingbats'] = { + 'name': "Dingbats", 'keys': [ + [["\u2764", "\u2765", "\u2766", "\u2767"], ["\u278a", "\u2780", "\u2776", "\u2768"], ["\u278b", "\u2781", "\u2777", "\u2769"], ["\u278c", "\u2782", "\u2778", "\u276a"], ["\u278d", "\u2783", "\u2779", "\u276b"], ["\u278e", "\u2784", "\u277a", "\u276c"], ["\u278f", "\u2785", "\u277b", "\u276d"], ["\u2790", "\u2786", "\u277c", "\u276e"], ["\u2791", "\u2787", "\u277d", "\u276f"], ["\u2792", "\u2788", "\u277e", "\u2770"], ["\u2793", "\u2789", "\u277f", "\u2771"], ["\u2795", "\u2796", "\u274c", "\u2797"], ["\u2702", "\u2704", "\u2701", "\u2703"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u2714", "\u2705", "\u2713"], ["\u2718", "\u2715", "\u2717", "\u2716"], ["\u271a", "\u2719", "\u271b", "\u271c"], ["\u271d", "\u271e", "\u271f", "\u2720"], ["\u2722", "\u2723", "\u2724", "\u2725"], ["\u2726", "\u2727", "\u2728", "\u2756"], ["\u2729", "\u272a", "\u272d", "\u2730"], ["\u272c", "\u272b", "\u272e", "\u272f"], ["\u2736", "\u2731", "\u2732", "\u2749"], ["\u273b", "\u273c", "\u273d", "\u273e"], ["\u2744", "\u2745", "\u2746", "\u2743"], ["\u2733", "\u2734", "\u2735", "\u2721"], ["\u2737", "\u2738", "\u2739", "\u273a"]], + [["Caps", "Caps"], ["\u2799", "\u279a", "\u2798", "\u2758"], ["\u27b5", "\u27b6", "\u27b4", "\u2759"], ["\u27b8", "\u27b9", "\u27b7", "\u275a"], ["\u2794", "\u279c", "\u27ba", "\u27bb"], ["\u279d", "\u279e", "\u27a1", "\u2772"], ["\u27a9", "\u27aa", "\u27ab", "\u27ac"], ["\u27a4", "\u27a3", "\u27a2", "\u279b"], ["\u27b3", "\u27bc", "\u27bd", "\u2773"], ["\u27ad", "\u27ae", "\u27af", "\u27b1"], ["\u27a8", "\u27a6", "\u27a5", "\u27a7"], ["\u279f", "\u27a0", "\u27be", "\u27b2"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u270c", "\u270b", "\u270a", "\u270d"], ["\u274f", "\u2750", "\u2751", "\u2752"], ["\u273f", "\u2740", "\u2741", "\u2742"], ["\u2747", "\u2748", "\u274a", "\u274b"], ["\u2757", "\u2755", "\u2762", "\u2763"], ["\u2753", "\u2754", "\u27b0", "\u27bf"], ["\u270f", "\u2710", "\u270e", "\u2774"], ["\u2712", "\u2711", "\u274d", "\u274e"], ["\u2709", "\u2706", "\u2708", "\u2707"], ["\u275b", "\u275d", "\u2761", "\u2775"], ["\u275c", "\u275e", "\u275f", "\u2760"], ["Shift", "Shift"]], + [["AltLk", "AltLk"], [" ", " ", " ", " "], ["AltGr", "AltGr"]] + ]}; + + this.VKI_layout['\u078b\u07a8\u0788\u07ac\u0780\u07a8\u0784\u07a6\u0790\u07b0'] = { + 'name': "Divehi", 'keys': [ + [["`", "~"], ["1", "!"], ["2", "@"], ["3", "#"], ["4", "$"], ["5", "%"], ["6", "^"], ["7", "&"], ["8", "*"], ["9", ")"], ["0", "("], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u07ab", "\u00d7"], ["\u07ae", "\u2019"], ["\u07a7", "\u201c"], ["\u07a9", "/"], ["\u07ad", ":"], ["\u078e", "\u07a4"], ["\u0783", "\u079c"], ["\u0789", "\u07a3"], ["\u078c", "\u07a0"], ["\u0780", "\u0799"], ["\u078d", "\u00f7"], ["[", "{"], ["]", "}"]], + [["Caps", "Caps"], ["\u07a8", "<"], ["\u07aa", ">"], ["\u07b0", ".", ",", ","], ["\u07a6", "\u060c"], ["\u07ac", '"'], ["\u0788", "\u07a5"], ["\u0787", "\u07a2"], ["\u0782", "\u0798"], ["\u0786", "\u079a"], ["\u078a", "\u07a1"], ["\ufdf2", "\u061b", ";", ";"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\\", "|"], ["\u0792", "\u0796"], ["\u0791", "\u0795"], ["\u0790", "\u078f"], ["\u0794", "\u0797", "\u200D"], ["\u0785", "\u079f", "\u200C"], ["\u078b", "\u079b", "\u200E"], ["\u0784", "\u079D", "\u200F"], ["\u0781", "\\"], ["\u0793", "\u079e"], ["\u07af", "\u061f"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["dv"] }; + + this.VKI_layout['Dvorak'] = { + 'name': "Dvorak", 'keys': [ + [["`", "~"], ["1", "!"], ["2", "@"], ["3", "#"], ["4", "$"], ["5", "%"], ["6", "^"], ["7", "&"], ["8", "*"], ["9", "("], ["0", ")"], ["[", "{"], ["]", "}"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["'", '"'], [",", "<"], [".", ">"], ["p", "P"], ["y", "Y"], ["f", "F"], ["g", "G"], ["c", "C"], ["r", "R"], ["l", "L"], ["/", "?"], ["=", "+"], ["\\", "|"]], + [["Caps", "Caps"], ["a", "A"], ["o", "O"], ["e", "E"], ["u", "U"], ["i", "I"], ["d", "D"], ["h", "H"], ["t", "T"], ["n", "N"], ["s", "S"], ["-", "_"], ["Enter", "Enter"]], + [["Shift", "Shift"], [";", ":"], ["q", "Q"], ["j", "J"], ["k", "K"], ["x", "X"], ["b", "B"], ["m", "M"], ["w", "W"], ["v", "V"], ["z", "Z"], ["Shift", "Shift"]], + [[" ", " "]] + ]}; + + this.VKI_layout['\u0395\u03bb\u03bb\u03b7\u03bd\u03b9\u03ba\u03ac'] = { + 'name': "Greek", 'keys': [ + [["`", "~"], ["1", "!"], ["2", "@", "\u00b2"], ["3", "#", "\u00b3"], ["4", "$", "\u00a3"], ["5", "%", "\u00a7"], ["6", "^", "\u00b6"], ["7", "&"], ["8", "*", "\u00a4"], ["9", "(", "\u00a6"], ["0", ")", "\u00ba"], ["-", "_", "\u00b1"], ["=", "+", "\u00bd"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], [";", ":"], ["\u03c2", "^"], ["\u03b5", "\u0395"], ["\u03c1", "\u03a1"], ["\u03c4", "\u03a4"], ["\u03c5", "\u03a5"], ["\u03b8", "\u0398"], ["\u03b9", "\u0399"], ["\u03bf", "\u039f"], ["\u03c0", "\u03a0"], ["[", "{", "\u201c"], ["]", "}", "\u201d"], ["\\", "|", "\u00ac"]], + [["Caps", "Caps"], ["\u03b1", "\u0391"], ["\u03c3", "\u03a3"], ["\u03b4", "\u0394"], ["\u03c6", "\u03a6"], ["\u03b3", "\u0393"], ["\u03b7", "\u0397"], ["\u03be", "\u039e"], ["\u03ba", "\u039a"], ["\u03bb", "\u039b"], ["\u0384", "\u00a8", "\u0385"], ["'", '"'], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">"], ["\u03b6", "\u0396"], ["\u03c7", "\u03a7"], ["\u03c8", "\u03a8"], ["\u03c9", "\u03a9"], ["\u03b2", "\u0392"], ["\u03bd", "\u039d"], ["\u03bc", "\u039c"], [",", "<"], [".", ">"], ["/", "?"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["el"] }; + + this.VKI_layout['Eesti'] = { + 'name': "Estonian", 'keys': [ + [["\u02C7", "~"], ["1", "!"], ["2", '"', "@", "@"], ["3", "#", "\u00A3", "\u00A3"], ["4", "\u00A4", "$", "$"], ["5", "%", "\u20AC"], ["6", "&"], ["7", "/", "{", "{"], ["8", "(", "[", "["], ["9", ")", "]", "]"], ["0", "=", "}", "}"], ["+", "?", "\\", "\\"], ["\u00B4", "`"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E", "\u20AC"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["\u00FC", "\u00DC"], ["\u00F5", "\u00D5", "\u00A7", "\u00A7"], ["'", "*", "\u00BD", "\u00BD"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S", "\u0161", "\u0160"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["\u00F6", "\u00D6"], ["\u00E4", "\u00C4", "^", "^"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">", "|", "|"], ["z", "Z", "\u017E", "\u017D"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M"], [",", ";"], [".", ":"], ["-", "_"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["et"] }; + + this.VKI_layout['Espa\u00f1ol'] = { + 'name': "Spanish", 'keys': [ + [["\u00ba", "\u00aa", "\\"], ["1", "!", "|"], ["2", '"', "@"], ["3", "'", "#"], ["4", "$", "~"], ["5", "%", "\u20ac"], ["6", "&", "\u00ac"], ["7", "/"], ["8", "("], ["9", ")"], ["0", "="], ["'", "?"], ["\u00a1", "\u00bf"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["`", "^", "["], ["+", "*", "]"], ["\u00e7", "\u00c7", "}"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["\u00f1", "\u00d1"], ["\u00b4", "\u00a8", "{"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">"], ["z", "Z"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M"], [",", ";"], [".", ":"], ["-", "_"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["es"] }; + + this.VKI_layout['\u062f\u0631\u06cc'] = { + 'name': "Dari", 'keys': [ + [["\u200D", "\u00F7", "~"], ["\u06F1", "!", "`"], ["\u06F2", "\u066C", "@"], ["\u06F3", "\u066B", "#"], ["\u06F4", "\u060B", "$"], ["\u06F5", "\u066A", "%"], ["\u06F6", "\u00D7", "^"], ["\u06F7", "\u060C", "&"], ["\u06F8", "*", "\u2022"], ["\u06F9", ")", "\u200E"], ["\u06F0", "(", "\u200F"], ["-", "\u0640", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0636", "\u0652", "\u00B0"], ["\u0635", "\u064C"], ["\u062B", "\u064D", "\u20AC"], ["\u0642", "\u064B", "\uFD3E"], ["\u0641", "\u064F", "\uFD3F"], ["\u063A", "\u0650", "\u0656"], ["\u0639", "\u064E", "\u0659"], ["\u0647", "\u0651", "\u0655"], ["\u062E", "]", "'"], ["\u062D", "[", '"'], ["\u062C", "}", "\u0681"], ["\u0686", "{", "\u0685"], ["\\", "|", "?"]], + [["Caps", "Caps"], ["\u0634", "\u0624", "\u069A"], ["\u0633", "\u0626", "\u06CD"], ["\u06CC", "\u064A", "\u0649"], ["\u0628", "\u0625", "\u06D0"], ["\u0644", "\u0623", "\u06B7"], ["\u0627", "\u0622", "\u0671"], ["\u062A", "\u0629", "\u067C"], ["\u0646", "\u00BB", "\u06BC"], ["\u0645", "\u00AB", "\u06BA"], ["\u06A9", ":", ";"], ["\u06AF", "\u061B", "\u06AB"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0638", "\u0643", "\u06D2"], ["\u0637", "\u0653", "\u0691"], ["\u0632", "\u0698", "\u0696"], ["\u0631", "\u0670", "\u0693"], ["\u0630", "\u200C", "\u0688"], ["\u062F", "\u0654", "\u0689"], ["\u067E", "\u0621", "\u0679"], ["\u0648", ">", ","], [".", "<", "\u06C7"], ["/", "\u061F", "\u06C9"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["fa-AF"] }; + + this.VKI_layout['\u0641\u0627\u0631\u0633\u06cc'] = { + 'name': "Farsi", 'keys': [ + [["\u067e", "\u0651 "], ["1", "!", "\u00a1", "\u00b9"], ["2", "@", "\u00b2"], ["3", "#", "\u00b3"], ["4", "$", "\u00a4", "\u00a3"], ["5", "%", "\u20ac"], ["6", "^", "\u00bc"], ["7", "&", "\u00bd"], ["8", "*", "\u00be"], ["9", "(", "\u2018"], ["0", ")", "\u2019"], ["-", "_", "\u00a5"], ["=", "+", "\u00d7", "\u00f7"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0636", "\u064e"], ["\u0635", "\u064b"], ["\u062b", "\u064f"], ["\u0642", "\u064c"], ["\u0641", "\u0644"], ["\u063a", "\u0625"], ["\u0639", "\u2018"], ["\u0647", "\u00f7"], ["\u062e", "\u00d7"], ["\u062d", "\u061b"], ["\u062c", "<"], ["\u0686", ">"], ["\u0698", "|"]], + [["Caps", "Caps"], ["\u0634", "\u0650"], ["\u0633", "\u064d"], ["\u064a", "]"], ["\u0628", "["], ["\u0644", "\u0644"], ["\u0627", "\u0623"], ["\u062a", "\u0640"], ["\u0646", "\u060c"], ["\u0645", "\\"], ["\u06af", ":"], ["\u0643", '"'], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0638", "~"], ["\u0637", "\u0652"], ["\u0632", "}"], ["\u0631", "{"], ["\u0630", "\u0644"], ["\u062f", "\u0622"], ["\u0626", "\u0621"], ["\u0648", ","], [".", "."], ["/", "\u061f"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["Alt", "Alt"]] + ], 'lang': ["fa"] }; + + this.VKI_layout['F\u00f8royskt'] = { + 'name': "Faeroese", 'keys': [ + [["\u00BD", "\u00A7"], ["1", "!"], ["2", '"', "@"], ["3", "#", "\u00A3"], ["4", "\u00A4", "$"], ["5", "%", "\u20AC"], ["6", "&"], ["7", "/", "{"], ["8", "(", "["], ["9", ")", "]"], ["0", "=", "}"], ["+", "?"], ["\u00B4", "`", "|"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E", "\u20AC"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["\u00E5", "\u00C5", "\u00A8"], ["\u00F0", "\u00D0", "~"], ["'", "*"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["\u00E6", "\u00C6"], ["\u00F8", "\u00D8", "^"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">", "\\"], ["z", "Z"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M", "\u00B5"], [",", ";"], [".", ":"], ["-", "_"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["fo"] }; + + this.VKI_layout['Fran\u00e7ais'] = { + 'name': "French", 'keys': [ + [["\u00b2", "\u00b3"], ["&", "1"], ["\u00e9", "2", "~"], ['"', "3", "#"], ["'", "4", "{"], ["(", "5", "["], ["-", "6", "|"], ["\u00e8", "7", "`"], ["_", "8", "\\"], ["\u00e7", "9", "^"], ["\u00e0", "0", "@"], [")", "\u00b0", "]"], ["=", "+", "}"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["a", "A"], ["z", "Z"], ["e", "E", "\u20ac"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["^", "\u00a8"], ["$", "\u00a3", "\u00a4"], ["*", "\u03bc"]], + [["Caps", "Caps"], ["q", "Q"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["m", "M"], ["\u00f9", "%"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">"], ["w", "W"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], [",", "?"], [";", "."], [":", "/"], ["!", "\u00a7"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["fr"] }; + + this.VKI_layout['Gaeilge'] = { + 'name': "Irish / Gaelic", 'keys': [ + [["`", "\u00AC", "\u00A6", "\u00A6"], ["1", "!"], ["2", '"'], ["3", "\u00A3"], ["4", "$", "\u20AC"], ["5", "%"], ["6", "^"], ["7", "&"], ["8", "*"], ["9", "("], ["0", ")"], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E", "\u00E9", "\u00C9"], ["r", "R"], ["t", "T"], ["y", "Y", "\u00FD", "\u00DD"], ["u", "U", "\u00FA", "\u00DA"], ["i", "I", "\u00ED", "\u00CD"], ["o", "O", "\u00F3", "\u00D3"], ["p", "P"], ["[", "{"], ["]", "}"], ["#", "~"]], + [["Caps", "Caps"], ["a", "A", "\u00E1", "\u00C1"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], [";", ":"], ["'", "@", "\u00B4", "`"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\\", "|"], ["z", "Z"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M"], [",", "<"], [".", ">"], ["/", "?"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["ga", "gd"] }; + + this.VKI_layout['\u0a97\u0ac1\u0a9c\u0ab0\u0abe\u0aa4\u0ac0'] = { + 'name': "Gujarati", 'keys': [ + [[""], ["1", "\u0A8D", "\u0AE7"], ["2", "\u0AC5", "\u0AE8"], ["3", "\u0ACD\u0AB0", "\u0AE9"], ["4", "\u0AB0\u0ACD", "\u0AEA"], ["5", "\u0A9C\u0ACD\u0A9E", "\u0AEB"], ["6", "\u0AA4\u0ACD\u0AB0", "\u0AEC"], ["7", "\u0A95\u0ACD\u0AB7", "\u0AED"], ["8", "\u0AB6\u0ACD\u0AB0", "\u0AEE"], ["9", "(", "\u0AEF"], ["0", ")", "\u0AE6"], ["-", "\u0A83"], ["\u0AC3", "\u0A8B", "\u0AC4", "\u0AE0"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0ACC", "\u0A94"], ["\u0AC8", "\u0A90"], ["\u0ABE", "\u0A86"], ["\u0AC0", "\u0A88"], ["\u0AC2", "\u0A8A"], ["\u0AAC", "\u0AAD"], ["\u0AB9", "\u0A99"], ["\u0A97", "\u0A98"], ["\u0AA6", "\u0AA7"], ["\u0A9C", "\u0A9D"], ["\u0AA1", "\u0AA2"], ["\u0ABC", "\u0A9E"], ["\u0AC9", "\u0A91"]], + [["Caps", "Caps"], ["\u0ACB", "\u0A93"], ["\u0AC7", "\u0A8F"], ["\u0ACD", "\u0A85"], ["\u0ABF", "\u0A87"], ["\u0AC1", "\u0A89"], ["\u0AAA", "\u0AAB"], ["\u0AB0"], ["\u0A95", "\u0A96"], ["\u0AA4", "\u0AA5"], ["\u0A9A", "\u0A9B"], ["\u0A9F", "\u0AA0"], ["Enter", "Enter"]], + [["Shift", "Shift"], [""], ["\u0A82", "\u0A81", "", "\u0AD0"], ["\u0AAE", "\u0AA3"], ["\u0AA8"], ["\u0AB5"], ["\u0AB2", "\u0AB3"], ["\u0AB8", "\u0AB6"], [",", "\u0AB7"], [".", "\u0964", "\u0965", "\u0ABD"], ["\u0AAF"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["gu"] }; + + this.VKI_layout['\u05e2\u05d1\u05e8\u05d9\u05ea'] = { + 'name': "Hebrew", 'keys': [ + [["~", "`"], ["1", "!"], ["2", "@"], ["3", "#"], ["4" , "$", "\u20aa"], ["5" , "%"], ["6", "^"], ["7", "&"], ["8", "*"], ["9", ")"], ["0", "("], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["/", "Q"], ["'", "W"], ["\u05e7", "E", "\u20ac"], ["\u05e8", "R"], ["\u05d0", "T"], ["\u05d8", "Y"], ["\u05d5", "U", "\u05f0"], ["\u05df", "I"], ["\u05dd", "O"], ["\u05e4", "P"], ["\\", "|"], ["Enter", "Enter"]], + [["Caps", "Caps"], ["\u05e9", "A"], ["\u05d3", "S"], ["\u05d2", "D"], ["\u05db", "F"], ["\u05e2", "G"], ["\u05d9", "H", "\u05f2"], ["\u05d7", "J", "\u05f1"], ["\u05dc", "K"], ["\u05da", "L"], ["\u05e3", ":"], ["," , '"'], ["]", "}"], ["[", "{"]], + [["Shift", "Shift"], ["\u05d6", "Z"], ["\u05e1", "X"], ["\u05d1", "C"], ["\u05d4", "V"], ["\u05e0", "B"], ["\u05de", "N"], ["\u05e6", "M"], ["\u05ea", ">"], ["\u05e5", "<"], [".", "?"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["he"] }; + + this.VKI_layout['\u0926\u0947\u0935\u0928\u093e\u0917\u0930\u0940'] = { + 'name': "Devanagari", 'keys': [ + [["\u094A", "\u0912"], ["1", "\u090D", "\u0967"], ["2", "\u0945", "\u0968"], ["3", "\u094D\u0930", "\u0969"], ["4", "\u0930\u094D", "\u096A"], ["5", "\u091C\u094D\u091E", "\u096B"], ["6", "\u0924\u094D\u0930", "\u096C"], ["7", "\u0915\u094D\u0937", "\u096D"], ["8", "\u0936\u094D\u0930", "\u096E"], ["9", "(", "\u096F"], ["0", ")", "\u0966"], ["-", "\u0903"], ["\u0943", "\u090B", "\u0944", "\u0960"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u094C", "\u0914"], ["\u0948", "\u0910"], ["\u093E", "\u0906"], ["\u0940", "\u0908", "\u0963", "\u0961"], ["\u0942", "\u090A"], ["\u092C", "\u092D"], ["\u0939", "\u0919"], ["\u0917", "\u0918", "\u095A"], ["\u0926", "\u0927"], ["\u091C", "\u091D", "\u095B"], ["\u0921", "\u0922", "\u095C", "\u095D"], ["\u093C", "\u091E"], ["\u0949", "\u0911"]], + [["Caps", "Caps"], ["\u094B", "\u0913"], ["\u0947", "\u090F"], ["\u094D", "\u0905"], ["\u093F", "\u0907", "\u0962", "\u090C"], ["\u0941", "\u0909"], ["\u092A", "\u092B", "", "\u095E"], ["\u0930", "\u0931"], ["\u0915", "\u0916", "\u0958", "\u0959"], ["\u0924", "\u0925"], ["\u091A", "\u091B", "\u0952"], ["\u091F", "\u0920", "", "\u0951"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0946", "\u090E", "\u0953"], ["\u0902", "\u0901", "", "\u0950"], ["\u092E", "\u0923", "\u0954"], ["\u0928", "\u0929"], ["\u0935", "\u0934"], ["\u0932", "\u0933"], ["\u0938", "\u0936"], [",", "\u0937", "\u0970"], [".", "\u0964", "\u0965", "\u093D"], ["\u092F", "\u095F"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["hi-Deva"] }; + + this.VKI_layout['\u0939\u093f\u0902\u0926\u0940'] = { + 'name': "Hindi", 'keys': [ + [["\u200d", "\u200c", "`", "~"], ["1", "\u090D", "\u0967", "!"], ["2", "\u0945", "\u0968", "@"], ["3", "\u094D\u0930", "\u0969", "#"], ["4", "\u0930\u094D", "\u096A", "$"], ["5", "\u091C\u094D\u091E", "\u096B", "%"], ["6", "\u0924\u094D\u0930", "\u096C", "^"], ["7", "\u0915\u094D\u0937", "\u096D", "&"], ["8", "\u0936\u094D\u0930", "\u096E", "*"], ["9", "(", "\u096F", "("], ["0", ")", "\u0966", ")"], ["-", "\u0903", "-", "_"], ["\u0943", "\u090B", "=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u094C", "\u0914"], ["\u0948", "\u0910"], ["\u093E", "\u0906"], ["\u0940", "\u0908"], ["\u0942", "\u090A"], ["\u092C", "\u092D"], ["\u0939", "\u0919"], ["\u0917", "\u0918"], ["\u0926", "\u0927"], ["\u091C", "\u091D"], ["\u0921", "\u0922", "[", "{"], ["\u093C", "\u091E", "]", "}"], ["\u0949", "\u0911", "\\", "|"]], + [["Caps", "Caps"], ["\u094B", "\u0913"], ["\u0947", "\u090F"], ["\u094D", "\u0905"], ["\u093F", "\u0907"], ["\u0941", "\u0909"], ["\u092A", "\u092B"], ["\u0930", "\u0931"], ["\u0915", "\u0916"], ["\u0924", "\u0925"], ["\u091A", "\u091B", ";", ":"], ["\u091F", "\u0920", "'", '"'], ["Enter", "Enter"]], + [["Shift", "Shift"], [""], ["\u0902", "\u0901", "", "\u0950"], ["\u092E", "\u0923"], ["\u0928"], ["\u0935"], ["\u0932", "\u0933"], ["\u0938", "\u0936"], [",", "\u0937", ",", "<"], [".", "\u0964", ".", ">"], ["\u092F", "\u095F", "/", "?"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["hi"] }; + + this.VKI_layout['Hrvatski'] = { + 'name': "Croatian", 'keys': this.VKI_layout['Bosanski'].keys.slice(0), 'lang': ["hr"] + }; + + this.VKI_layout['\u0540\u0561\u0575\u0565\u0580\u0565\u0576 \u0561\u0580\u0565\u0582\u0574\u0578\u0582\u057f\u0584'] = { + 'name': "Western Armenian", 'keys': [ + [["\u055D", "\u055C"], [":", "1"], ["\u0571", "\u0541"], ["\u0575", "\u0545"], ["\u055B", "3"], [",", "4"], ["-", "9"], [".", "\u0587"], ["\u00AB", "("], ["\u00BB", ")"], ["\u0585", "\u0555"], ["\u057C", "\u054C"], ["\u056A", "\u053A"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u056D", "\u053D"], ["\u057E", "\u054E"], ["\u0567", "\u0537"], ["\u0580", "\u0550"], ["\u0564", "\u0534"], ["\u0565", "\u0535"], ["\u0568", "\u0538"], ["\u056B", "\u053B"], ["\u0578", "\u0548"], ["\u0562", "\u0532"], ["\u0579", "\u0549"], ["\u057B", "\u054B"], ["'", "\u055E"]], + [["Caps", "Caps"], ["\u0561", "\u0531"], ["\u057D", "\u054D"], ["\u057F", "\u054F"], ["\u0586", "\u0556"], ["\u056F", "\u053F"], ["\u0570", "\u0540"], ["\u0573", "\u0543"], ["\u0584", "\u0554"], ["\u056C", "\u053C"], ["\u0569", "\u0539"], ["\u0583", "\u0553"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0566", "\u0536"], ["\u0581", "\u0551"], ["\u0563", "\u0533"], ["\u0582", "\u0552"], ["\u057A", "\u054A"], ["\u0576", "\u0546"], ["\u0574", "\u0544"], ["\u0577", "\u0547"], ["\u0572", "\u0542"], ["\u056E", "\u053E"], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["hy-arevmda"] }; + + this.VKI_layout['\u0540\u0561\u0575\u0565\u0580\u0565\u0576 \u0561\u0580\u0565\u0582\u0565\u056c\u0584'] = { + 'name': "Eastern Armenian", 'keys': [ + [["\u055D", "\u055C"], [":", "1"], ["\u0571", "\u0541"], ["\u0575", "\u0545"], ["\u055B", "3"], [",", "4"], ["-", "9"], [".", "\u0587"], ["\u00AB", "("], ["\u00BB", ")"], ["\u0585", "\u0555"], ["\u057C", "\u054C"], ["\u056A", "\u053A"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u056D", "\u053D"], ["\u0582", "\u0552"], ["\u0567", "\u0537"], ["\u0580", "\u0550"], ["\u057F", "\u054F"], ["\u0565", "\u0535"], ["\u0568", "\u0538"], ["\u056B", "\u053B"], ["\u0578", "\u0548"], ["\u057A", "\u054A"], ["\u0579", "\u0549"], ["\u057B", "\u054B"], ["'", "\u055E"]], + [["Caps", "Caps"], ["\u0561", "\u0531"], ["\u057D", "\u054D"], ["\u0564", "\u0534"], ["\u0586", "\u0556"], ["\u0584", "\u0554"], ["\u0570", "\u0540"], ["\u0573", "\u0543"], ["\u056F", "\u053F"], ["\u056C", "\u053C"], ["\u0569", "\u0539"], ["\u0583", "\u0553"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0566", "\u0536"], ["\u0581", "\u0551"], ["\u0563", "\u0533"], ["\u057E", "\u054E"], ["\u0562", "\u0532"], ["\u0576", "\u0546"], ["\u0574", "\u0544"], ["\u0577", "\u0547"], ["\u0572", "\u0542"], ["\u056E", "\u053E"], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["hy"] }; + + this.VKI_layout['\u00cdslenska'] = { + 'name': "Icelandic", 'keys': [ + [["\u00B0", "\u00A8", "\u00B0"], ["1", "!"], ["2", '"'], ["3", "#"], ["4", "$"], ["5", "%", "\u20AC"], ["6", "&"], ["7", "/", "{"], ["8", "(", "["], ["9", ")", "]"], ["0", "=", "}"], ["\u00F6", "\u00D6", "\\"], ["-", "_"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q", "@"], ["w", "W"], ["e", "E", "\u20AC"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["\u00F0", "\u00D0"], ["'", "?", "~"], ["+", "*", "`"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["\u00E6", "\u00C6"], ["\u00B4", "'", "^"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">", "|"], ["z", "Z"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M", "\u00B5"], [",", ";"], [".", ":"], ["\u00FE", "\u00DE"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["is"] }; + + this.VKI_layout['Italiano'] = { + 'name': "Italian", 'keys': [ + [["\\", "|"], ["1", "!"], ["2", '"'], ["3", "\u00a3"], ["4", "$", "\u20ac"], ["5", "%"], ["6", "&"], ["7", "/"], ["8", "("], ["9", ")"], ["0", "="], ["'", "?"], ["\u00ec", "^"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E", "\u20ac"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["\u00e8", "\u00e9", "[", "{"], ["+", "*", "]", "}"], ["\u00f9", "\u00a7"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["\u00f2", "\u00e7", "@"], ["\u00e0", "\u00b0", "#"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">"], ["z", "Z"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M"], [",", ";"], [".", ":"], ["-", "_"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["it"] }; + + this.VKI_layout['\u65e5\u672c\u8a9e'] = { + 'name': "Japanese Hiragana/Katakana", 'keys': [ + [["\uff5e"], ["\u306c", "\u30cc"], ["\u3075", '\u30d5'], ["\u3042", "\u30a2", "\u3041", "\u30a1"], ["\u3046", "\u30a6", "\u3045", "\u30a5"], ["\u3048", "\u30a8", "\u3047", "\u30a7"], ["\u304a", "\u30aa", "\u3049", "\u30a9"], ["\u3084", "\u30e4", "\u3083", "\u30e3"], ["\u3086", "\u30e6", "\u3085", "\u30e5"], ["\u3088", "\u30e8", "\u3087", "\u30e7"], ["\u308f", "\u30ef", "\u3092", "\u30f2"], ["\u307b", "\u30db", "\u30fc", "\uff1d"], ["\u3078", "\u30d8" , "\uff3e", "\uff5e"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u305f", "\u30bf"], ["\u3066", "\u30c6"], ["\u3044", "\u30a4", "\u3043", "\u30a3"], ["\u3059", "\u30b9"], ["\u304b", "\u30ab"], ["\u3093", "\u30f3"], ["\u306a", "\u30ca"], ["\u306b", "\u30cb"], ["\u3089", "\u30e9"], ["\u305b", "\u30bb"], ["\u3001", "\u3001", "\uff20", "\u2018"], ["\u3002", "\u3002", "\u300c", "\uff5b"], ["\uffe5", "", "", "\uff0a"], ['\u309B', '"', "\uffe5", "\uff5c"]], + [["Caps", "Caps"], ["\u3061", "\u30c1"], ["\u3068", "\u30c8"], ["\u3057", "\u30b7"], ["\u306f", "\u30cf"], ["\u304d", "\u30ad"], ["\u304f", "\u30af"], ["\u307e", "\u30de"], ["\u306e", "\u30ce"], ["\u308c", "\u30ec", "\uff1b", "\uff0b"], ["\u3051", "\u30b1", "\uff1a", "\u30f6"], ["\u3080", "\u30e0", "\u300d", "\uff5d"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u3064", "\u30c4"], ["\u3055", "\u30b5"], ["\u305d", "\u30bd"], ["\u3072", "\u30d2"], ["\u3053", "\u30b3"], ["\u307f", "\u30df"], ["\u3082", "\u30e2"], ["\u306d", "\u30cd", "\u3001", "\uff1c"], ["\u308b", "\u30eb", "\u3002", "\uff1e"], ["\u3081", "\u30e1", "\u30fb", "\uff1f"], ["\u308d", "\u30ed", "", "\uff3f"], ["Shift", "Shift"]], + [["AltLk", "AltLk"], [" ", " ", " ", " "], ["Alt", "Alt"]] + ], 'lang': ["ja"] }; + + this.VKI_layout['\u10e5\u10d0\u10e0\u10d7\u10e3\u10da\u10d8'] = { + 'name': "Georgian", 'keys': [ + [["\u201E", "\u201C"], ["!", "1"], ["?", "2"], ["\u2116", "3"], ["\u00A7", "4"], ["%", "5"], [":", "6"], [".", "7"], [";", "8"], [",", "9"], ["/", "0"], ["\u2013", "-"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u10E6", "\u10E6"], ["\u10EF", "\u10EF"], ["\u10E3", "\u10E3"], ["\u10D9", "\u10D9"], ["\u10D4", "\u10D4", "\u10F1"], ["\u10DC", "\u10DC"], ["\u10D2", "\u10D2"], ["\u10E8", "\u10E8"], ["\u10EC", "\u10EC"], ["\u10D6", "\u10D6"], ["\u10EE", "\u10EE", "\u10F4"], ["\u10EA", "\u10EA"], ["(", ")"]], + [["Caps", "Caps"], ["\u10E4", "\u10E4", "\u10F6"], ["\u10EB", "\u10EB"], ["\u10D5", "\u10D5", "\u10F3"], ["\u10D7", "\u10D7"], ["\u10D0", "\u10D0"], ["\u10DE", "\u10DE"], ["\u10E0", "\u10E0"], ["\u10DD", "\u10DD"], ["\u10DA", "\u10DA"], ["\u10D3", "\u10D3"], ["\u10DF", "\u10DF"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u10ED", "\u10ED"], ["\u10E9", "\u10E9"], ["\u10E7", "\u10E7"], ["\u10E1", "\u10E1"], ["\u10DB", "\u10DB"], ["\u10D8", "\u10D8", "\u10F2"], ["\u10E2", "\u10E2"], ["\u10E5", "\u10E5"], ["\u10D1", "\u10D1"], ["\u10F0", "\u10F0", "\u10F5"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["ka"] }; + + this.VKI_layout['\u049a\u0430\u0437\u0430\u049b\u0448\u0430'] = { + 'name': "Kazakh", 'keys': [ + [["(", ")"], ['"', "!"], ["\u04d9", "\u04d8"], ["\u0456", "\u0406"], ["\u04a3", "\u04a2"], ["\u0493", "\u0492"], [",", ";"], [".", ":"], ["\u04af", "\u04ae"], ["\u04b1", "\u04b0"], ["\u049b", "\u049a"], ["\u04e9", "\u04e8"], ["\u04bb", "\u04ba"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0439", "\u0419"], ["\u0446", "\u0426"], ["\u0443", "\u0423"], ["\u043A", "\u041A"], ["\u0435", "\u0415"], ["\u043D", "\u041D"], ["\u0433", "\u0413"], ["\u0448", "\u0428"], ["\u0449", "\u0429"], ["\u0437", "\u0417"], ["\u0445", "\u0425"], ["\u044A", "\u042A"], ["\\", "/"]], + [["Caps", "Caps"], ["\u0444", "\u0424"], ["\u044B", "\u042B"], ["\u0432", "\u0412"], ["\u0430", "\u0410"], ["\u043F", "\u041F"], ["\u0440", "\u0420"], ["\u043E", "\u041E"], ["\u043B", "\u041B"], ["\u0434", "\u0414"], ["\u0436", "\u0416"], ["\u044D", "\u042D"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\\", "|"], ["\u044F", "\u042F"], ["\u0447", "\u0427"], ["\u0441", "\u0421"], ["\u043C", "\u041C"], ["\u0438", "\u0418"], ["\u0442", "\u0422"], ["\u044C", "\u042C"], ["\u0431", "\u0411"], ["\u044E", "\u042E"], ["\u2116", "?"], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["kk"] }; + + this.VKI_layout['\u1797\u17b6\u179f\u17b6\u1781\u17d2\u1798\u17c2\u179a'] = { + 'name': "Khmer", 'keys': [ + [["\u00AB", "\u00BB","\u200D"], ["\u17E1", "!","\u200C","\u17F1"], ["\u17E2", "\u17D7", "@", "\u17F2"], ["\u17E3", '"', "\u17D1", "\u17F3"], ["\u17E4", "\u17DB", "$", "\u17F4"], ["\u17E5", "%" ,"\u20AC", "\u17F5"], ["\u17E6", "\u17CD", "\u17D9", "\u17F6"], ["\u17E7", "\u17D0", "\u17DA", "\u17F7"], ["\u17E8", "\u17CF", "*", "\u17F8"], ["\u17E9", "(", "{", "\u17F9"], ["\u17E0", ")", "}", "\u17F0"], ["\u17A5", "\u17CC", "x"], ["\u17B2", "=", "\u17CE"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u1786", "\u1788", "\u17DC", "\u19E0"], ["\u17B9", "\u17BA", "\u17DD", "\u19E1"], ["\u17C1", "\u17C2", "\u17AF", "\u19E2"], ["\u179A", "\u17AC", "\u17AB", "\u19E3"], ["\u178F", "\u1791", "\u17A8", "\u19E4"], ["\u1799", "\u17BD", "\u1799\u17BE\u1784", "\u19E5"], ["\u17BB", "\u17BC", "", "\u19E6"], ["\u17B7", "\u17B8", "\u17A6", "\u19E7"], ["\u17C4", "\u17C5", "\u17B1", "\u19E8"], ["\u1795", "\u1797", "\u17B0", "\u19E9"], ["\u17C0", "\u17BF", "\u17A9", "\u19EA"], ["\u17AA", "\u17A7", "\u17B3", "\u19EB"], ["\u17AE", "\u17AD", "\\"]], + [["Caps", "Caps"], ["\u17B6", "\u17B6\u17C6", "\u17B5", "\u19EC"], ["\u179F", "\u17C3", "", "\u19ED"], ["\u178A", "\u178C", "\u17D3", "\u19EE"], ["\u1790", "\u1792", "", "\u19EF"], ["\u1784", "\u17A2", "\u17A4", "\u19F0"], ["\u17A0", "\u17C7", "\u17A3", "\u19F1"], ["\u17D2", "\u1789", "\u17B4", "\u19F2"], ["\u1780", "\u1782", "\u179D", "\u19F3"], ["\u179B", "\u17A1", "\u17D8", "\u19F4"], ["\u17BE", "\u17C4\u17C7", "\u17D6", "\u19F5"], ["\u17CB", "\u17C9", "\u17C8", "\u19F6"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u178B", "\u178D", "|", "\u19F7"], ["\u1781", "\u1783", "\u1781\u17D2\u1789\u17BB\u17C6", "\u19F8"], ["\u1785", "\u1787", "-", "\u19F9"], ["\u179C", "\u17C1\u17C7", "+", "\u19FA"], ["\u1794", "\u1796", "\u179E", "\u19FB"], ["\u1793", "\u178E", "[", "\u19FC"], ["\u1798", "\u17C6", "]", "\u19FD"], ["\u17BB\u17C6", "\u17BB\u17C7", ",", "\u19FE"], ["\u17D4", "\u17D5", ".", "\u19FF"], ["\u17CA", "?", "/"], ["Shift", "Shift"]], + [["\u200B", " ", "\u00A0", " "], ["AltGr", "AltGr"]] + ], 'lang': ["km"] }; + + this.VKI_layout['\u0c95\u0ca8\u0ccd\u0ca8\u0ca1'] = { + 'name': "Kannada", 'keys': [ + [["\u0CCA", "\u0C92"], ["1", "", "\u0CE7"], ["2", "", "\u0CE8"], ["3", "\u0CCD\u0CB0", "\u0CE9"], ["4", "\u0CB0\u0CCD", "\u0CEA"], ["5", "\u0C9C\u0CCD\u0C9E", "\u0CEB"], ["6", "\u0CA4\u0CCD\u0CB0", "\u0CEC"], ["7", "\u0C95\u0CCD\u0CB7", "\u0CED"], ["8", "\u0CB6\u0CCD\u0CB0", "\u0CEE"], ["9", "(", "\u0CEF"], ["0", ")", "\u0CE6"], ["-", "\u0C83"], ["\u0CC3", "\u0C8B", "\u0CC4", "\u0CE0"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0CCC", "\u0C94"], ["\u0CC8", "\u0C90", "\u0CD6"], ["\u0CBE", "\u0C86"], ["\u0CC0", "\u0C88", "", "\u0CE1"], ["\u0CC2", "\u0C8A"], ["\u0CAC", "\u0CAD"], ["\u0CB9", "\u0C99"], ["\u0C97", "\u0C98"], ["\u0CA6", "\u0CA7"], ["\u0C9C", "\u0C9D"], ["\u0CA1", "\u0CA2"], ["Enter", "Enter"]], + [["Caps", "Caps"], ["\u0CCB", "\u0C93"], ["\u0CC7", "\u0C8F", "\u0CD5"], ["\u0CCD", "\u0C85"], ["\u0CBF", "\u0C87", "", "\u0C8C"], ["\u0CC1", "\u0C89"], ["\u0CAA", "\u0CAB", "", "\u0CDE"], ["\u0CB0", "\u0CB1"], ["\u0C95", "\u0C96"], ["\u0CA4", "\u0CA5"], ["\u0C9A", "\u0C9B"], ["\u0C9F", "\u0CA0"], ["", "\u0C9E"]], + [["Shift", "Shift"], ["\u0CC6", "\u0C8F"], ["\u0C82"], ["\u0CAE", "\u0CA3"], ["\u0CA8"], ["\u0CB5"], ["\u0CB2", "\u0CB3"], ["\u0CB8", "\u0CB6"], [",", "\u0CB7"], [".", "|"], ["\u0CAF"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["kn"] }; + + this.VKI_layout['\ud55c\uad6d\uc5b4'] = { + 'name': "Korean", 'keys': [ + [["`", "~", "`", "~"], ["1", "!", "1", "!"], ["2", "@", "2", "@"], ["3", "#", "3", "#"], ["4", "$", "4", "$"], ["5", "%", "5", "%"], ["6", "^", "6", "^"], ["7", "&", "7", "&"], ["8", "*", "8", "*"], ["9", ")", "9", ")"], ["0", "(", "0", "("], ["-", "_", "-", "_"], ["=", "+", "=", "+"], ["\u20A9", "|", "\u20A9", "|"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u1107", "\u1108", "q", "Q"], ["\u110C", "\u110D", "w", "W"], ["\u1103", "\u1104", "e", "E"], ["\u1100", "\u1101", "r", "R"], ["\u1109", "\u110A", "t", "T"], ["\u116D", "", "y", "Y"], ["\u1167", "", "u", "U"], ["\u1163", "", "i", "I"], ["\u1162", "\u1164", "o", "O"], ["\u1166", "\u1168", "p", "P"], ["[", "{", "[", "{"], ["]", "}", "]", "}"]], + [["Caps", "Caps"], ["\u1106", "", "a", "A"], ["\u1102", "", "s", "S"], ["\u110B", "", "d", "D"], ["\u1105", "", "f", "F"], ["\u1112", "", "g", "G"], ["\u1169", "", "h", "H"], ["\u1165", "", "j", "J"], ["\u1161", "", "k", "K"], ["\u1175", "", "l", "L"], [";", ":", ";", ":"], ["'", '"', "'", '"'], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u110F", "", "z", "Z"], ["\u1110", "", "x", "X"], ["\u110E", "", "c", "C"], ["\u1111", "", "v", "V"], ["\u1172", "", "b", "B"], ["\u116E", "", "n", "N"], ["\u1173", "", "m", "M"], [",", "<", ",", "<"], [".", ">", ".", ">"], ["/", "?", "/", "?"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["Kor", "Alt"]] + ], 'lang': ["ko"] }; + + this.VKI_layout['Kurd\u00ee'] = { + 'name': "Kurdish", 'keys': [ + [["\u20ac", "~"], ["\u0661", "!"], ["\u0662", "@"], ["\u0663", "#"], ["\u0664", "$"], ["\u0665", "%"], ["\u0666", "^"], ["\u0667", "&"], ["\u0668", "*"], ["\u0669", "("], ["\u0660", ")"], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0642", "`"], ["\u0648", "\u0648\u0648"], ["\u06d5", "\u064a"], ["\u0631", "\u0695"], ["\u062a", "\u0637"], ["\u06cc", "\u06ce"], ["\u0626", "\u0621"], ["\u062d", "\u0639"], ["\u06c6", "\u0624"], ["\u067e", "\u062b"], ["[", "{"], ["]", "}"], ["\\", "|"]], + [["Caps", "Caps"], ["\u0627", "\u0622"], ["\u0633", "\u0634"], ["\u062f", "\u0630"], ["\u0641", "\u0625"], ["\u06af", "\u063a"], ["\u0647", "\u200c"], ["\u0698", "\u0623"], ["\u06a9", "\u0643"], ["\u0644", "\u06b5"], ["\u061b", ":"], ["'", '"'], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0632", "\u0636"], ["\u062e", "\u0635"], ["\u062c", "\u0686"], ["\u06a4", "\u0638"], ["\u0628", "\u0649"], ["\u0646", "\u0629"], ["\u0645", "\u0640"], ["\u060c", "<"], [".", ">"], ["/", "\u061f"], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["ku"] }; + + this.VKI_layout['\u041a\u044b\u0440\u0433\u044b\u0437\u0447\u0430'] = { + 'name': "Kyrgyz", 'keys': [ + [["\u0451", "\u0401"], ["1", "!"], ["2", '"'], ["3", "\u2116"], ["4", ";"], ["5", "%"], ["6", ":"], ["7", "?"], ["8", "*"], ["9", "("], ["0", ")"], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0439", "\u0419"], ["\u0446", "\u0426"], ["\u0443", "\u0423", "\u04AF", "\u04AE"], ["\u043A", "\u041A"], ["\u0435", "\u0415"], ["\u043D", "\u041D", "\u04A3", "\u04A2"], ["\u0433", "\u0413"], ["\u0448", "\u0428"], ["\u0449", "\u0429"], ["\u0437", "\u0417"], ["\u0445", "\u0425"], ["\u044A", "\u042A"], ["\\", "/"]], + [["Caps", "Caps"], ["\u0444", "\u0424"], ["\u044B", "\u042B"], ["\u0432", "\u0412"], ["\u0430", "\u0410"], ["\u043F", "\u041F"], ["\u0440", "\u0420"], ["\u043E", "\u041E", "\u04E9", "\u04E8"], ["\u043B", "\u041B"], ["\u0434", "\u0414"], ["\u0436", "\u0416"], ["\u044D", "\u042D"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u044F", "\u042F"], ["\u0447", "\u0427"], ["\u0441", "\u0421"], ["\u043C", "\u041C"], ["\u0438", "\u0418"], ["\u0442", "\u0422"], ["\u044C", "\u042C"], ["\u0431", "\u0411"], ["\u044E", "\u042E"], [".", ","], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["ky"] }; + + this.VKI_layout['Latvie\u0161u'] = { + 'name': "Latvian", 'keys': [ + [["\u00AD", "?"], ["1", "!", "\u00AB"], ["2", "\u00AB", "", "@"], ["3", "\u00BB", "", "#"], ["4", "$", "\u20AC", "$"], ["5", "%", '"', "~"], ["6", "/", "\u2019", "^"], ["7", "&", "", "\u00B1"], ["8", "\u00D7", ":"], ["9", "("], ["0", ")"], ["-", "_", "\u2013", "\u2014"], ["f", "F", "=", ";"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u016B", "\u016A", "q", "Q"], ["g", "G", "\u0123", "\u0122"], ["j", "J"], ["r", "R", "\u0157", "\u0156"], ["m", "M", "w", "W"], ["v", "V", "y", "Y"], ["n", "N"], ["z", "Z"], ["\u0113", "\u0112"], ["\u010D", "\u010C"], ["\u017E", "\u017D", "[", "{"], ["h", "H", "]", "}"], ["\u0137", "\u0136"]], + [["Caps", "Caps"], ["\u0161", "\u0160"], ["u", "U"], ["s", "S"], ["i", "I"], ["l", "L"], ["d", "D"], ["a", "A"], ["t", "T"], ["e", "E", "\u20AC"], ["c", "C"], ["\u00B4", "\u00B0", "\u00B4", "\u00A8"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0146", "\u0145"], ["b", "B", "x", "X"], ["\u012B", "\u012A"], ["k", "K", "\u0137", "\u0136"], ["p", "P"], ["o", "O", "\u00F5", "\u00D5"], ["\u0101", "\u0100"], [",", ";", "<"], [".", ":", ">"], ["\u013C", "\u013B"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["lv"] }; + + this.VKI_layout['Lietuvi\u0173'] = { + 'name': "Lithuanian", 'keys': [ + [["`", "~"], ["\u0105", "\u0104"], ["\u010D", "\u010C"], ["\u0119", "\u0118"], ["\u0117", "\u0116"], ["\u012F", "\u012E"], ["\u0161", "\u0160"], ["\u0173", "\u0172"], ["\u016B", "\u016A"], ["\u201E", "("], ["\u201C", ")"], ["-", "_"], ["\u017E", "\u017D"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["[", "{"], ["]", "}"], ["\\", "|"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], [";", ":"], ["'", '"'], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u2013", "\u20AC"], ["z", "Z"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M"], [",", "<"], [".", ">"], ["/", "?"], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["lt"] }; + + this.VKI_layout['Magyar'] = { + 'name': "Hungarian", 'keys': [ + [["0", "\u00a7"], ["1", "'", "~"], ["2", '"', "\u02c7"], ["3", "+", "\u02c6"], ["4", "!", "\u02d8"], ["5", "%", "\u00b0"], ["6", "/", "\u02db"], ["7", "=", "`"], ["8", "(", "\u02d9"], ["9", ")", "\u00b4"], ["\u00f6", "\u00d6", "\u02dd"], ["\u00fc", "\u00dc", "\u00a8"], ["\u00f3", "\u00d3", "\u00b8"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q", "\\"], ["w", "W", "|"], ["e", "E", "\u00c4"], ["r", "R"], ["t", "T"], ["z", "Z"], ["u", "U", "\u20ac"], ["i", "I", "\u00cd"], ["o", "O"], ["p", "P"], ["\u0151", "\u0150", "\u00f7"], ["\u00fa", "\u00da", "\u00d7"], ["\u0171", "\u0170", "\u00a4"]], + [["Caps", "Caps"], ["a", "A", "\u00e4"], ["s", "S", "\u0111"], ["d", "D", "\u0110"], ["f", "F", "["], ["g", "G", "]"], ["h", "H"], ["j", "J", "\u00ed"], ["k", "K", "\u0141"], ["l", "L", "\u0142"], ["\u00e9", "\u00c9", "$"], ["\u00e1", "\u00c1", "\u00df"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u00ed", "\u00cd", "<"], ["y", "Y", ">"], ["x", "X", "#"], ["c", "C", "&"], ["v", "V", "@"], ["b", "B", "{"], ["n", "N", "}"], ["m", "M", "<"], [",", "?", ";"], [".", ":", ">"], ["-", "_", "*"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["hu"] }; + + this.VKI_layout['Malti'] = { + 'name': "Maltese 48", 'keys': [ + [["\u010B", "\u010A", "`"], ["1", "!"], ["2", '"'], ["3", "\u20ac", "\u00A3"], ["4", "$"], ["5", "%"], ["6", "^"], ["7", "&"], ["8", "*"], ["9", "("], ["0", ")"], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E", "\u00E8", "\u00C8"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U", "\u00F9", "\u00D9"], ["i", "I", "\u00EC", "\u00cc"], ["o", "O", "\u00F2", "\u00D2"], ["p", "P"], ["\u0121", "\u0120", "[", "{"], ["\u0127", "\u0126", "]", "}"], ["#", "\u017e"]], + [["Caps", "Caps"], ["a", "A", "\u00E0", "\u00C0"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], [";", ":"], ["'", "@"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u017c", "\u017b", "\\", "|"], ["z", "Z"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M"], [",", "<"], [".", ">"], ["/", "?", "", "`"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["mt"] }; + + this.VKI_layout['\u041c\u0430\u043a\u0435\u0434\u043e\u043d\u0441\u043a\u0438'] = { + 'name': "Macedonian Cyrillic", 'keys': [ + [["`", "~"], ["1", "!"], ["2", "\u201E"], ["3", "\u201C"], ["4", "\u2019"], ["5", "%"], ["6", "\u2018"], ["7", "&"], ["8", "*"], ["9", "("], ["0", ")"], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0459", "\u0409"], ["\u045A", "\u040A"], ["\u0435", "\u0415", "\u20AC"], ["\u0440", "\u0420"], ["\u0442", "\u0422"], ["\u0455", "\u0405"], ["\u0443", "\u0423"], ["\u0438", "\u0418"], ["\u043E", "\u041E"], ["\u043F", "\u041F"], ["\u0448", "\u0428", "\u0402"], ["\u0453", "\u0403", "\u0452"], ["\u0436", "\u0416"]], + [["Caps", "Caps"], ["\u0430", "\u0410"], ["\u0441", "\u0421"], ["\u0434", "\u0414"], ["\u0444", "\u0424", "["], ["\u0433", "\u0413", "]"], ["\u0445", "\u0425"], ["\u0458", "\u0408"], ["\u043A", "\u041A"], ["\u043B", "\u041B"], ["\u0447", "\u0427", "\u040B"], ["\u045C", "\u040C", "\u045B"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0451", "\u0401"], ["\u0437", "\u0417"], ["\u045F", "\u040F"], ["\u0446", "\u0426"], ["\u0432", "\u0412", "@"], ["\u0431", "\u0411", "{"], ["\u043D", "\u041D", "}"], ["\u043C", "\u041C", "\u00A7"], [",", ";"], [".", ":"], ["/", "?"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["mk"] }; + + this.VKI_layout['\u0d2e\u0d32\u0d2f\u0d3e\u0d33\u0d02'] = { + 'name': "Malayalam", 'keys': [ + [["\u0D4A", "\u0D12"], ["1", "", "\u0D67"], ["2", "", "\u0D68"], ["3", "\u0D4D\u0D30", "\u0D69"], ["4", "", "\u0D6A"], ["5", "", "\u0D6B"], ["6", "", "\u0D6C"], ["7", "\u0D15\u0D4D\u0D37", "\u0D6D"], ["8", "", "\u0D6E"], ["9", "(", "\u0D6F"], ["0", ")", "\u0D66"], ["-", "\u0D03"], ["\u0D43", "\u0D0B", "", "\u0D60"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0D4C", "\u0D14", "\u0D57"], ["\u0D48", "\u0D10"], ["\u0D3E", "\u0D06"], ["\u0D40", "\u0D08", "", "\u0D61"], ["\u0D42", "\u0D0A"], ["\u0D2C", "\u0D2D"], ["\u0D39", "\u0D19"], ["\u0D17", "\u0D18"], ["\u0D26", "\u0D27"], ["\u0D1C", "\u0D1D"], ["\u0D21", "\u0D22"], ["", "\u0D1E"]], + [["Caps", "Caps"], ["\u0D4B", "\u0D13"], ["\u0D47", "\u0D0F"], ["\u0D4D", "\u0D05", "", "\u0D0C"], ["\u0D3F", "\u0D07"], ["\u0D41", "\u0D09"], ["\u0D2A", "\u0D2B"], ["\u0D30", "\u0D31"], ["\u0D15", "\u0D16"], ["\u0D24", "\u0D25"], ["\u0D1A", "\u0D1B"], ["\u0D1F", "\u0D20"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0D46", "\u0D0F"], ["\u0D02"], ["\u0D2E", "\u0D23"], ["\u0D28"], ["\u0D35", "\u0D34"], ["\u0D32", "\u0D33"], ["\u0D38", "\u0D36"], [",", "\u0D37"], ["."], ["\u0D2F"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["ml"] }; + + this.VKI_layout['Misc. Symbols'] = { + 'name': "Misc. Symbols", 'keys': [ + [["\u2605", "\u2606", "\u260e", "\u260f"], ["\u2648", "\u2673", "\u2659", "\u2630"], ["\u2649", "\u2674", "\u2658", "\u2631"], ["\u264a", "\u2675", "\u2657", "\u2632"], ["\u264b", "\u2676", "\u2656", "\u2633"], ["\u264c", "\u2677", "\u2655", "\u2634"], ["\u264d", "\u2678", "\u2654", "\u2635"], ["\u264e", "\u2679", "\u265f", "\u2636"], ["\u264f", "\u267a", "\u265e", "\u2637"], ["\u2650", "\u267b", "\u265d", "\u2686"], ["\u2651", "\u267c", "\u265c", "\u2687"], ["\u2652", "\u267d", "\u265b", "\u2688"], ["\u2653", "\u2672", "\u265a", "\u2689"], ["Bksp", "Bksp"]], + [["\u263f", "\u2680", "\u268a", "\u26a2"], ["\u2640", "\u2681", "\u268b", "\u26a3"], ["\u2641", "\u2682", "\u268c", "\u26a4"], ["\u2642", "\u2683", "\u268d", "\u26a5"], ["\u2643", "\u2684", "\u268e", "\u26a6"], ["\u2644", "\u2685", "\u268f", "\u26a7"], ["\u2645", "\u2620", "\u26ff", "\u26a8"], ["\u2646", "\u2622", "\u2692", "\u26a9"], ["\u2647", "\u2623", "\u2693", "\u26b2"], ["\u2669", "\u266d", "\u2694", "\u26ac"], ["\u266a", "\u266e", "\u2695", "\u26ad"], ["\u266b", "\u266f", "\u2696", "\u26ae"], ["\u266c", "\u2607", "\u2697", "\u26af"], ["\u26f9", "\u2608", "\u2698", "\u26b0"], ["\u267f", "\u262e", "\u2638", "\u2609"]], + [["Tab", "Tab"], ["\u261e", "\u261c", "\u261d", "\u261f"], ["\u261b", "\u261a", "\u2618", "\u2619"], ["\u2602", "\u2614", "\u26f1", "\u26d9"], ["\u2615", "\u2668", "\u26fe", "\u26d8"], ["\u263a", "\u2639", "\u263b", "\u26dc"], ["\u2617", "\u2616", "\u26ca", "\u26c9"], ["\u2660", "\u2663", "\u2665", "\u2666"], ["\u2664", "\u2667", "\u2661", "\u2662"], ["\u26c2", "\u26c0", "\u26c3", "\u26c1"], ["\u2624", "\u2625", "\u269a", "\u26b1"], ["\u2610", "\u2611", "\u2612", "\u2613"], ["\u2628", "\u2626", "\u2627", "\u2629"], ["\u262a", "\u262b", "\u262c", "\u262d"], ["\u26fa", "\u26fb", "\u26fc", "\u26fd"]], + [["Caps", "Caps"], ["\u262f", "\u2670", "\u2671", "\u267e"], ["\u263c", "\u2699", "\u263d", "\u263e"], ["\u26c4", "\u2603", "\u26c7", "\u26c6"], ["\u26a0", "\u26a1", "\u2621", "\u26d4"], ["\u26e4", "\u26e5", "\u26e6", "\u26e7"], ["\u260a", "\u260b", "\u260c", "\u260d"], ["\u269c", "\u269b", "\u269d", "\u2604"], ["\u26b3", "\u26b4", "\u26b5", "\u26b6"], ["\u26b7", "\u26bf", "\u26b8", "\u26f8"], ["\u26b9", "\u26ba", "\u26bb", "\u26bc"], ["\u26bd", "\u26be", "\u269f", "\u269e"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u2600", "\u2601", "\u26c5", "\u26c8"], ["\u2691", "\u2690", "\u26ab", "\u26aa"], ["\u26cb", "\u26cc", "\u26cd", "\u26ce"], ["\u26cf", "\u26d0", "\u26d1", "\u26d2"], ["\u26d3", "\u26d5", "\u26d6", "\u26d7"], ["\u26da", "\u26db", "\u26dd", "\u26de"], ["\u26df", "\u26e0", "\u26e1", "\u26e2"], ["\u26e3", "\u26e8", "\u26e9", "\u26ea"], ["\u26eb", "\u26ec", "\u26ed", "\u26ee"], ["\u26ef", "\u26f0", "\u26f2", "\u26f3"], ["\u26f4", "\u26f5", "\u26f6", "\u26f7"], ["Shift", "Shift"]], + [["AltLk", "AltLk"], [" ", " ", " ", " "], ["Alt", "Alt"]] + ]}; + + this.VKI_layout['\u041c\u043e\u043d\u0433\u043e\u043b'] = { + 'name': "Mongolian Cyrillic", 'keys': [ + [["=", "+"], ["\u2116", "1"], ["-", "2"], ['"', "3"], ["\u20AE", "4"], [":", "5"], [".", "6"], ["_", "7"], [",", "8"], ["%", "9"], ["?", "0"], ["\u0435", "\u0415"], ["\u0449", "\u0429"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0444", "\u0424"], ["\u0446", "\u0426"], ["\u0443", "\u0423"], ["\u0436", "\u0416"], ["\u044d", "\u042d"], ["\u043D", "\u041D"], ["\u0433", "\u0413"], ["\u0448", "\u0428"], ["\u04af", "\u04AE"], ["\u0437", "\u0417"], ["\u043A", "\u041a"], ["\u044A", "\u042A"], ["\\", "|"]], + [["Caps", "Caps"], ["\u0439", "\u0419"], ["\u044B", "\u042B"], ["\u0431", "\u0411"], ["\u04e9", "\u04e8"], ["\u0430", "\u0410"], ["\u0445", "\u0425"], ["\u0440", "\u0420"], ["\u043e", "\u041e"], ["\u043B", "\u041b"], ["\u0434", "\u0414"], ["\u043f", "\u041f"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u044F", "\u042F"], ["\u0447", "\u0427"], ["\u0451", "\u0401"], ["\u0441", "\u0421"], ["\u043c", "\u041c"], ["\u0438", "\u0418"], ["\u0442", "\u0422"], ["\u044c", "\u042c"], ["\u0432", "\u0412"], ["\u044e", "\u042e"], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["mn"] }; + + this.VKI_layout['\u092e\u0930\u093e\u0920\u0940'] = { + 'name': "Marathi", 'keys': [ + [["", "", "`", "~"], ["\u0967", "\u090D", "1", "!"], ["\u0968", "\u0945", "2", "@"], ["\u0969", "\u094D\u0930", "3", "#"], ["\u096A", "\u0930\u094D", "4", "$"], ["\u096B", "\u091C\u094D\u091E", "5", "%"], ["\u096C", "\u0924\u094D\u0930", "6", "^"], ["\u096D", "\u0915\u094D\u0937", "7", "&"], ["\u096E", "\u0936\u094D\u0930", "8", "*"], ["\u096F", "(", "9", "("], ["\u0966", ")", "0", ")"], ["-", "\u0903", "-", "_"], ["\u0943", "\u090B", "=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u094C", "\u0914"], ["\u0948", "\u0910"], ["\u093E", "\u0906"], ["\u0940", "\u0908"], ["\u0942", "\u090A"], ["\u092C", "\u092D"], ["\u0939", "\u0919"], ["\u0917", "\u0918"], ["\u0926", "\u0927"], ["\u091C", "\u091D"], ["\u0921", "\u0922", "[", "{"], ["\u093C", "\u091E", "]", "}"], ["\u0949", "\u0911", "\\", "|"]], + [["Caps", "Caps"], ["\u094B", "\u0913"], ["\u0947", "\u090F"], ["\u094D", "\u0905"], ["\u093F", "\u0907"], ["\u0941", "\u0909"], ["\u092A", "\u092B"], ["\u0930", "\u0931"], ["\u0915", "\u0916"], ["\u0924", "\u0925"], ["\u091A", "\u091B", ";", ":"], ["\u091F", "\u0920", "'", '"'], ["Enter", "Enter"]], + [["Shift", "Shift"], [""], ["\u0902", "\u0901", "", "\u0950"], ["\u092E", "\u0923"], ["\u0928"], ["\u0935"], ["\u0932", "\u0933"], ["\u0938", "\u0936"], [",", "\u0937", ",", "<"], [".", "\u0964", ".", ">"], ["\u092F", "\u095F", "/", "?"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["mr"] }; + + this.VKI_layout['\u1019\u103c\u1014\u103a\u1019\u102c\u1018\u102c\u101e\u102c'] = { + 'name': "Burmese", 'keys': [ + [["\u1039`", "~"], ["\u1041", "\u100D"], ["\u1042", "\u100E"], ["\u1043", "\u100B"], ["\u1044", "\u1000\u103B\u1015\u103A"], ["\u1045", "%"], ["\u1046", "/"], ["\u1047", "\u101B"], ["\u1048", "\u1002"], ["\u1049", "("], ["\u1040", ")"], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u1006", "\u1029"], ["\u1010", "\u1040"], ["\u1014", "\u103F"], ["\u1019", "\u1023"], ["\u1021", "\u1024"], ["\u1015", "\u104C"], ["\u1000", "\u1009"], ["\u1004", "\u104D"], ["\u101E", "\u1025"], ["\u1005", "\u100F"], ["\u101F", "\u1027"], ["\u2018", "\u2019"], ["\u104F", "\u100B\u1039\u100C"]], + [["Caps", "Caps"], ["\u200B\u1031", "\u1017"], ["\u200B\u103B", "\u200B\u103E"], ["\u200B\u102D", "\u200B\u102E"], ["\u200B\u103A", "\u1004\u103A\u1039\u200B"], ["\u200B\u102B", "\u200B\u103D"], ["\u200B\u1037", "\u200B\u1036"], ["\u200B\u103C", "\u200B\u1032"], ["\u200B\u102F", "\u200B\u102F"], ["\u200B\u1030", "\u200B\u1030"], ["\u200B\u1038", "\u200B\u102B\u103A"], ["\u1012", "\u1013"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u1016", "\u1007"], ["\u1011", "\u100C"], ["\u1001", "\u1003"], ["\u101C", "\u1020"], ["\u1018", "\u1026"], ["\u100A", "\u1008"], ["\u200B\u102C", "\u102A"], ["\u101A", "\u101B"], [".", "\u101B"], ["\u104B", "\u104A"], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["my"] }; + + this.VKI_layout['Nederlands'] = { + 'name': "Dutch", 'keys': [ + [["@", "\u00a7", "\u00ac"], ["1", "!", "\u00b9"], ["2", '"', "\u00b2"], ["3", "#", "\u00b3"], ["4", "$", "\u00bc"], ["5", "%", "\u00bd"], ["6", "&", "\u00be"], ["7", "_", "\u00a3"], ["8", "(", "{"], ["9", ")", "}"], ["0", "'"], ["/", "?", "\\"], ["\u00b0", "~", "\u00b8"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E", "\u20ac"], ["r", "R", "\u00b6"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["\u00a8", "^"], ["*", "|"], ["<", ">"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S", "\u00df"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["+", "\u00b1"], ["\u00b4", "`"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["]", "[", "\u00a6"], ["z", "Z", "\u00ab"], ["x", "X", "\u00bb"], ["c", "C", "\u00a2"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M", "\u00b5"], [",", ";"], [".", ":", "\u00b7"], ["-", "="], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["nl"] }; + + this.VKI_layout['Norsk'] = { + 'name': "Norwegian", 'keys': [ + [["|", "\u00a7"], ["1", "!"], ["2", '"', "@"], ["3", "#", "\u00a3"], ["4", "\u00a4", "$"], ["5", "%"], ["6", "&"], ["7", "/", "{"], ["8", "(", "["], ["9", ")", "]"], ["0", "=", "}"], ["+", "?"], ["\\", "`", "\u00b4"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E", "\u20ac"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["\u00e5", "\u00c5"], ["\u00a8", "^", "~"], ["'", "*"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["\u00f8", "\u00d8"], ["\u00e6", "\u00c6"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">"], ["z", "Z"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M", "\u03bc", "\u039c"], [",", ";"], [".", ":"], ["-", "_"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["no", "nb", "nn"] }; + + this.VKI_layout['\u067e\u069a\u062a\u0648'] = { + 'name': "Pashto", 'keys': [ + [["\u200d", "\u00f7", "`"], ["\u06f1", "!", "`"], ["\u06f2", "\u066c", "@"], ["\u06f3", "\u066b", "\u066b"], ["\u06f4", "\u00a4", "\u00a3"], ["\u06f5", "\u066a", "%"], ["\u06f6", "\u00d7", "^"], ["\u06f7", "\u00ab", "&"], ["\u06f8", "\u00bb", "*"], ["\u06f9", "(", "\ufdf2"], ["\u06f0", ")", "\ufefb"], ["-", "\u0640", "_"], ["=", "+", "\ufe87", "\u00f7"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0636", "\u0652", "\u06d5"], ["\u0635", "\u064c", "\u0653"], ["\u062b", "\u064d", "\u20ac"], ["\u0642", "\u064b", "\ufef7"], ["\u0641", "\u064f", "\ufef5"], ["\u063a", "\u0650", "'"], ["\u0639", "\u064e", "\ufe84"], ["\u0647", "\u0651", "\u0670"], ["\u062e", "\u0681", "'"], ["\u062d", "\u0685", '"'], ["\u062c", "]", "}"], ["\u0686", "[", "{"], ["\\", "\u066d", "|"]], + [["Caps", "Caps"], ["\u0634", "\u069a", "\ufbb0"], ["\u0633", "\u06cd", "\u06d2"], ["\u06cc", "\u064a", "\u06d2"], ["\u0628", "\u067e", "\u06ba"], ["\u0644", "\u0623", "\u06b7"], ["\u0627", "\u0622", "\u0671"], ["\u062a", "\u067c", "\u0679"], ["\u0646", "\u06bc", "<"], ["\u0645", "\u0629", ">"], ["\u06a9", ":", "\u0643"], ["\u06af", "\u061b", "\u06ab"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0638", "\u0626", "?"], ["\u0637", "\u06d0", ";"], ["\u0632", "\u0698", "\u0655"], ["\u0631", "\u0621", "\u0654"], ["\u0630", "\u200c", "\u0625"], ["\u062f", "\u0689", "\u0688"], ["\u0693", "\u0624", "\u0691"], ["\u0648", "\u060c", ","], ["\u0696", ".", "\u06c7"], ["/", "\u061f", "\u06c9"], ["Shift", "Shift", "\u064d"]], + [[" ", " ", " ", " "], ["Alt", "Alt"]] + ], 'lang': ["ps"] }; + + this.VKI_layout['\u0a2a\u0a70\u0a1c\u0a3e\u0a2c\u0a40'] = { + 'name': "Punjabi (Gurmukhi)", 'keys': [ + [[""], ["1", "\u0A4D\u0A35", "\u0A67", "\u0A67"], ["2", "\u0A4D\u0A2F", "\u0A68", "\u0A68"], ["3", "\u0A4D\u0A30", "\u0A69", "\u0A69"], ["4", "\u0A71", "\u0A6A", "\u0A6A"], ["5", "", "\u0A6B", "\u0A6B"], ["6", "", "\u0A6C", "\u0A6C"], ["7", "", "\u0A6D", "\u0A6D"], ["8", "", "\u0A6E", "\u0A6E"], ["9", "(", "\u0A6F", "\u0A6F"], ["0", ")", "\u0A66", "\u0A66"], ["-"], [""], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0A4C", "\u0A14"], ["\u0A48", "\u0A10"], ["\u0A3E", "\u0A06"], ["\u0A40", "\u0A08"], ["\u0A42", "\u0A0A"], ["\u0A2C", "\u0A2D"], ["\u0A39", "\u0A19"], ["\u0A17", "\u0A18", "\u0A5A", "\u0A5A"], ["\u0A26", "\u0A27"], ["\u0A1C", "\u0A1D", "\u0A5B", "\u0A5B"], ["\u0A21", "\u0A22", "\u0A5C", "\u0A5C"], ["Enter", "Enter"]], + [["Caps", "Caps"], ["\u0A4B", "\u0A13"], ["\u0A47", "\u0A0F"], ["\u0A4D", "\u0A05"], ["\u0A3F", "\u0A07"], ["\u0A41", "\u0A09"], ["\u0A2A", "\u0A2B", "\u0A5E", "\u0A5E"], ["\u0A30"], ["\u0A15", "\u0A16", "\u0A59", "\u0A59"], ["\u0A24", "\u0A25"], ["\u0A1A", "\u0A1B"], ["\u0A1F", "\u0A20"], ["\u0A3C", "\u0A1E"]], + [["Shift", "Shift"], [""], ["\u0A02", "\u0A02"], ["\u0A2E", "\u0A23"], ["\u0A28"], ["\u0A35", "\u0A72", "\u0A73", "\u0A73"], ["\u0A32", "\u0A33"], ["\u0A38", "\u0A36"], [","], [".", "|", "\u0965", "\u0965"], ["\u0A2F"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["pa"] }; + + this.VKI_layout['\u62fc\u97f3 (Pinyin)'] = { + 'name': "Pinyin", 'keys': [ + [["`", "~", "\u4e93", "\u301C"], ["1", "!", "\uFF62"], ["2", "@", "\uFF63"], ["3", "#", "\u301D"], ["4", "$", "\u301E"], ["5", "%", "\u301F"], ["6", "^", "\u3008"], ["7", "&", "\u3009"], ["8", "*", "\u302F"], ["9", "(", "\u300A"], ["0", ")", "\u300B"], ["-", "_", "\u300E"], ["=", "+", "\u300F"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q", "\u0101", "\u0100"], ["w", "W", "\u00E1", "\u00C1"], ["e", "E", "\u01CE", "\u01CD"], ["r", "R", "\u00E0", "\u00C0"], ["t", "T", "\u0113", "\u0112"], ["y", "Y", "\u00E9", "\u00C9"], ["u", "U", "\u011B", "\u011A"], ["i", "I", "\u00E8", "\u00C8"], ["o", "O", "\u012B", "\u012A"], ["p", "P", "\u00ED", "\u00CD"], ["[", "{", "\u01D0", "\u01CF"], ["]", "}", "\u00EC", "\u00CC"], ["\\", "|", "\u3020"]], + [["Caps", "Caps"], ["a", "A", "\u014D", "\u014C"], ["s", "S", "\u00F3", "\u00D3"], ["d", "D", "\u01D2", "\u01D1"], ["f", "F", "\u00F2", "\u00D2"], ["g", "G", "\u00fc", "\u00dc"], ["h", "H", "\u016B", "\u016A"], ["j", "J", "\u00FA", "\u00DA"], ["k", "K", "\u01D4", "\u01D3"], ["l", "L", "\u00F9", "\u00D9"], [";", ":"], ["'", '"'], ["Enter", "Enter"]], + [["Shift", "Shift"], ["z", "Z", "\u01D6", "\u01D5"], ["x", "X", "\u01D8", "\u01D7"], ["c", "C", "\u01DA", "\u01D9"], ["v", "V", "\u01DC", "\u01DB"], ["b", "B"], ["n", "N"], ["m", "M"], [",", "<", "\u3001"], [".", ">", "\u3002"], ["/", "?"], ["Shift", "Shift"]], + [["AltLk", "AltLk"], [" ", " ", " ", " "], ["Alt", "Alt"]] + ], 'lang': ["zh-Latn"] }; + + this.VKI_layout['Polski'] = { + 'name': "Polish (214)", 'keys': [ + [["\u02DB", "\u00B7"], ["1", "!", "~"], ["2", '"', "\u02C7"], ["3", "#", "^"], ["4", "\u00A4", "\u02D8"], ["5", "%", "\u00B0"], ["6", "&", "\u02DB"], ["7", "/", "`"], ["8", "(", "\u00B7"], ["9", ")", "\u00B4"], ["0", "=", "\u02DD"], ["+", "?", "\u00A8"], ["'", "*", "\u00B8"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q", "\\"], ["w", "W", "\u00A6"], ["e", "E"], ["r", "R"], ["t", "T"], ["z", "Z"], ["u", "U", "\u20AC"], ["i", "I"], ["o", "O"], ["p", "P"], ["\u017C", "\u0144", "\u00F7"], ["\u015B", "\u0107", "\u00D7"], ["\u00F3", "\u017A"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S", "\u0111"], ["d", "D", "\u0110"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["\u0142", "\u0141", "$"], ["\u0105", "\u0119", "\u00DF"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">"], ["y", "Y"], ["x", "X"], ["c", "C"], ["v", "V", "@"], ["b", "B", "{"], ["n", "N", "}"], ["m", "M", "\u00A7"], [",", ";", "<"], [".", ":", ">"], ["-", "_"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ]}; + + this.VKI_layout['Polski Programisty'] = { + 'name': "Polish Programmers", 'keys': [ + [["`", "~"], ["1", "!"], ["2", "@"], ["3", "#"], ["4", "$"], ["5", "%"], ["6", "^"], ["7", "&"], ["8", "*"], ["9", "("], ["0", ")"], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E", "\u0119", "\u0118"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O", "\u00f3", "\u00d3"], ["p", "P"], ["[", "{"], ["]", "}"], ["\\", "|"]], + [["Caps", "Caps"], ["a", "A", "\u0105", "\u0104"], ["s", "S", "\u015b", "\u015a"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L", "\u0142", "\u0141"], [";", ":"], ["'", '"'], ["Enter", "Enter"]], + [["Shift", "Shift"], ["z", "Z", "\u017c", "\u017b"], ["x", "X", "\u017a", "\u0179"], ["c", "C", "\u0107", "\u0106"], ["v", "V"], ["b", "B"], ["n", "N", "\u0144", "\u0143"], ["m", "M"], [",", "<"], [".", ">"], ["/", "?"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["Alt", "Alt"]] + ], 'lang': ["pl"] }; + + this.VKI_layout['Portugu\u00eas Brasileiro'] = { + 'name': "Portuguese (Brazil)", 'keys': [ + [["'", '"'], ["1", "!", "\u00b9"], ["2", "@", "\u00b2"], ["3", "#", "\u00b3"], ["4", "$", "\u00a3"], ["5", "%", "\u00a2"], ["6", "\u00a8", "\u00ac"], ["7", "&"], ["8", "*"], ["9", "("], ["0", ")"], ["-", "_"], ["=", "+", "\u00a7"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q", "/"], ["w", "W", "?"], ["e", "E", "\u20ac"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["\u00b4", "`"], ["[", "{", "\u00aa"], ["Enter", "Enter"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["\u00e7", "\u00c7"], ["~", "^"], ["]", "}", "\u00ba"], ["/", "?"]], + [["Shift", "Shift"], ["\\", "|"], ["z", "Z"], ["x", "X"], ["c", "C", "\u20a2"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M"], [",", "<"], [".", ">"], [":", ":"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["pt-BR"] }; + + this.VKI_layout['Portugu\u00eas'] = { + 'name': "Portuguese", 'keys': [ + [["\\", "|"], ["1", "!"], ["2", '"', "@"], ["3", "#", "\u00a3"], ["4", "$", "\u00a7"], ["5", "%"], ["6", "&"], ["7", "/", "{"], ["8", "(", "["], ["9", ")", "]"], ["0", "=", "}"], ["'", "?"], ["\u00ab", "\u00bb"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E", "\u20ac"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["+", "*", "\u00a8"], ["\u00b4", "`"], ["~", "^"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["\u00e7", "\u00c7"], ["\u00ba", "\u00aa"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">", "\\"], ["z", "Z"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M"], [",", ";"], [".", ":"], ["-", "_"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["pt"] }; + + this.VKI_layout['Rom\u00e2n\u0103'] = { + 'name': "Romanian", 'keys': [ + [["\u201E", "\u201D", "`", "~"], ["1", "!", "~"], ["2", "@", "\u02C7"], ["3", "#", "^"], ["4", "$", "\u02D8"], ["5", "%", "\u00B0"], ["6", "^", "\u02DB"], ["7", "&", "`"], ["8", "*", "\u02D9"], ["9", "(", "\u00B4"], ["0", ")", "\u02DD"], ["-", "_", "\u00A8"], ["=", "+", "\u00B8", "\u00B1"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E", "\u20AC"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P", "\u00A7"], ["\u0103", "\u0102", "[", "{"], ["\u00EE", "\u00CE", "]", "}"], ["\u00E2", "\u00C2", "\\", "|"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S", "\u00df"], ["d", "D", "\u00f0", "\u00D0"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L", "\u0142", "\u0141"], [(this.VKI_isIElt8) ? "\u015F" : "\u0219", (this.VKI_isIElt8) ? "\u015E" : "\u0218", ";", ":"], [(this.VKI_isIElt8) ? "\u0163" : "\u021B", (this.VKI_isIElt8) ? "\u0162" : "\u021A", "\'", "\""], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\\", "|"], ["z", "Z"], ["x", "X"], ["c", "C", "\u00A9"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M"], [",", ";", "<", "\u00AB"], [".", ":", ">", "\u00BB"], ["/", "?"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["ro"] }; + + this.VKI_layout['\u0420\u0443\u0441\u0441\u043a\u0438\u0439'] = { + 'name': "Russian", 'keys': [ + [["\u0451", "\u0401"], ["1", "!"], ["2", '"'], ["3", "\u2116"], ["4", ";"], ["5", "%"], ["6", ":"], ["7", "?"], ["8", "*"], ["9", "("], ["0", ")"], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0439", "\u0419"], ["\u0446", "\u0426"], ["\u0443", "\u0423"], ["\u043A", "\u041A"], ["\u0435", "\u0415"], ["\u043D", "\u041D"], ["\u0433", "\u0413"], ["\u0448", "\u0428"], ["\u0449", "\u0429"], ["\u0437", "\u0417"], ["\u0445", "\u0425"], ["\u044A", "\u042A"], ["\\", "/"]], + [["Caps", "Caps"], ["\u0444", "\u0424"], ["\u044B", "\u042B"], ["\u0432", "\u0412"], ["\u0430", "\u0410"], ["\u043F", "\u041F"], ["\u0440", "\u0420"], ["\u043E", "\u041E"], ["\u043B", "\u041B"], ["\u0434", "\u0414"], ["\u0436", "\u0416"], ["\u044D", "\u042D"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["/", "|"], ["\u044F", "\u042F"], ["\u0447", "\u0427"], ["\u0441", "\u0421"], ["\u043C", "\u041C"], ["\u0438", "\u0418"], ["\u0442", "\u0422"], ["\u044C", "\u042C"], ["\u0431", "\u0411"], ["\u044E", "\u042E"], [".", ","], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["ru"] }; + + this.VKI_layout['Schweizerdeutsch'] = { + 'name': "Swiss German", 'keys': [ + [["\u00A7", "\u00B0"], ["1", "+", "\u00A6"], ["2", '"', "@"], ["3", "*", "#"], ["4", "\u00E7", "\u00B0"], ["5", "%", "\u00A7"], ["6", "&", "\u00AC"], ["7", "/", "|"], ["8", "(", "\u00A2"], ["9", ")"], ["0", "="], ["'", "?", "\u00B4"], ["^", "`", "~"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E", "\u20AC"], ["r", "R"], ["t", "T"], ["z", "Z"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["\u00FC", "\u00E8", "["], ["\u00A8", "!", "]"], ["$", "\u00A3", "}"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["\u00F6", "\u00E9"], ["\u00E4", "\u00E0", "{"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">", "\\"], ["y", "Y"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M"], [",", ";"], [".", ":"], ["-", "_"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["de-CH"] }; + + this.VKI_layout['Shqip'] = { + 'name': "Albanian", 'keys': [ + [["\\", "|"], ["1", "!", "~"], ["2", '"', "\u02C7"], ["3", "#", "^"], ["4", "$", "\u02D8"], ["5", "%", "\u00B0"], ["6", "^", "\u02DB"], ["7", "&", "`"], ["8", "*", "\u02D9"], ["9", "(", "\u00B4"], ["0", ")", "\u02DD"], ["-", "_", "\u00A8"], ["=", "+", "\u00B8"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q", "\\"], ["w", "W", "|"], ["e", "E"], ["r", "R"], ["t", "T"], ["z", "Z"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["\u00E7", "\u00C7", "\u00F7"], ["[", "{", "\u00DF"], ["]", "}", "\u00A4"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S", "\u0111"], ["d", "D", "\u0110"], ["f", "F", "["], ["g", "G", "]"], ["h", "H"], ["j", "J"], ["k", "K", "\u0142"], ["l", "L", "\u0141"], ["\u00EB", "\u00CB", "$"], ["@", "'", "\u00D7"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">"], ["y", "Y"], ["x", "X"], ["c", "C"], ["v", "V", "@"], ["b", "B", "{"], ["n", "N", "}"], ["m", "M", "\u00A7"], [",", ";", "<"], [".", ":", ">"], ["/", "?"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["sq"] }; + + this.VKI_layout['Sloven\u010dina'] = { + 'name': "Slovak", 'keys': [ + [[";", "\u00b0"], ["+", "1", "~"], ["\u013E", "2", "\u02C7"], ["\u0161", "3", "^"], ["\u010D", "4", "\u02D8"], ["\u0165", "5", "\u00B0"], ["\u017E", "6", "\u02DB"], ["\u00FD", "7", "`"], ["\u00E1", "8", "\u02D9"], ["\u00ED", "9", "\u00B4"], ["\u00E9", "0", "\u02DD"], ["=", "%", "\u00A8"], ["\u00B4", "\u02c7", "\u00B8"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q", "\\"], ["w", "W", "|"], ["e", "E", "\u20AC"], ["r", "R"], ["t", "T"], ["z", "Z"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P", "'"], ["\u00FA", "/", "\u00F7"], ["\u00E4", "(", "\u00D7"], ["\u0148", ")", "\u00A4"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S", "\u0111"], ["d", "D", "\u0110"], ["f", "F", "["], ["g", "G", "]"], ["h", "H"], ["j", "J"], ["k", "K", "\u0142"], ["l", "L", "\u0141"], ["\u00F4", '"', "$"], ["\u00A7", "!", "\u00DF"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["&", "*", "<"], ["y", "Y", ">"], ["x", "X", "#"], ["c", "C", "&"], ["v", "V", "@"], ["b", "B", "{"], ["n", "N", "}"], ["m", "M"], [",", "?", "<"], [".", ":", ">"], ["-", "_", "*", ], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["sk"] }; + + this.VKI_layout['Sloven\u0161\u010dina'] = { + 'name': "Slovenian", 'keys': this.VKI_layout['Bosanski'].keys.slice(0), 'lang': ["sl"] + }; + + this.VKI_layout['\u0441\u0440\u043f\u0441\u043a\u0438'] = { + 'name': "Serbian Cyrillic", 'keys': [ + [["`", "~"], ["1", "!"], ["2", '"'], ["3", "#"], ["4", "$"], ["5", "%"], ["6", "&"], ["7", "/"], ["8", "("], ["9", ")"], ["0", "="], ["'", "?"], ["+", "*"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0459", "\u0409"], ["\u045a", "\u040a"], ["\u0435", "\u0415", "\u20ac"], ["\u0440", "\u0420"], ["\u0442", "\u0422"], ["\u0437", "\u0417"], ["\u0443", "\u0423"], ["\u0438", "\u0418"], ["\u043e", "\u041e"], ["\u043f", "\u041f"], ["\u0448", "\u0428"], ["\u0452", "\u0402"], ["\u0436", "\u0416"]], + [["Caps", "Caps"], ["\u0430", "\u0410"], ["\u0441", "\u0421"], ["\u0434", "\u0414"], ["\u0444", "\u0424"], ["\u0433", "\u0413"], ["\u0445", "\u0425"], ["\u0458", "\u0408"], ["\u043a", "\u041a"], ["\u043b", "\u041b"], ["\u0447", "\u0427"], ["\u045b", "\u040b"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">"], ["\u0455", "\u0405"], ["\u045f", "\u040f"], ["\u0446", "\u0426"], ["\u0432", "\u0412"], ["\u0431", "\u0411"], ["\u043d", "\u041d"], ["\u043c", "\u041c"], [",", ";", "<"], [".", ":", ">"], ["-", "_", "\u00a9"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["sr-Cyrl"] }; + + this.VKI_layout['Srpski'] = { + 'name': "Serbian Latin", 'keys': this.VKI_layout['Bosanski'].keys.slice(0), 'lang': ["sr"] + }; + + this.VKI_layout['Suomi'] = { + 'name': "Finnish", 'keys': [ + [["\u00a7", "\u00BD"], ["1", "!"], ["2", '"', "@"], ["3", "#", "\u00A3"], ["4", "\u00A4", "$"], ["5", "%", "\u20AC"], ["6", "&"], ["7", "/", "{"], ["8", "(", "["], ["9", ")", "]"], ["0", "=", "}"], ["+", "?", "\\"], ["\u00B4", "`"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q", "\u00E2", "\u00C2"], ["w", "W"], ["e", "E", "\u20AC"], ["r", "R"], ["t", "T", "\u0167", "\u0166"], ["y", "Y"], ["u", "U"], ["i", "I", "\u00ef", "\u00CF"], ["o", "O", "\u00f5", "\u00D5"], ["p", "P"], ["\u00E5", "\u00C5"], ["\u00A8", "^", "~"], ["'", "*"]], + [["Caps", "Caps"], ["a", "A", "\u00E1", "\u00C1"], ["s", "S", "\u0161", "\u0160"], ["d", "D", "\u0111", "\u0110"], ["f", "F", "\u01e5", "\u01E4"], ["g", "G", "\u01E7", "\u01E6"], ["h", "H", "\u021F", "\u021e"], ["j", "J"], ["k", "K", "\u01e9", "\u01E8"], ["l", "L"], ["\u00F6", "\u00D6", "\u00F8", "\u00D8"], ["\u00E4", "\u00C4", "\u00E6", "\u00C6"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">", "|"], ["z", "Z", "\u017E", "\u017D"], ["x", "X"], ["c", "C", "\u010d", "\u010C"], ["v", "V", "\u01EF", "\u01EE"], ["b", "B", "\u0292", "\u01B7"], ["n", "N", "\u014B", "\u014A"], ["m", "M", "\u00B5"], [",", ";"], [".", ":"], ["-", "_"], ["Shift", "Shift"]], + [["Alt", "Alt"], [" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["fi"] }; + + this.VKI_layout['Svenska'] = { + 'name': "Swedish", 'keys': [ + [["\u00a7", "\u00bd"], ["1", "!"], ["2", '"', "@"], ["3", "#", "\u00a3"], ["4", "\u00a4", "$"], ["5", "%", "\u20ac"], ["6", "&"], ["7", "/", "{"], ["8", "(", "["], ["9", ")", "]"], ["0", "=", "}"], ["+", "?", "\\"], ["\u00b4", "`"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E", "\u20ac"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["\u00e5", "\u00c5"], ["\u00a8", "^", "~"], ["'", "*"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["\u00f6", "\u00d6"], ["\u00e4", "\u00c4"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">", "|"], ["z", "Z"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M", "\u03bc", "\u039c"], [",", ";"], [".", ":"], ["-", "_"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["sv"] }; + + this.VKI_layout['Swiss Fran\u00e7ais'] = { + 'name': "Swiss French", 'keys': [ + [["\u00A7", "\u00B0"], ["1", "+", "\u00A6"], ["2", '"', "@"], ["3", "*", "#"], ["4", "\u00E7", "\u00B0"], ["5", "%", "\u00A7"], ["6", "&", "\u00AC"], ["7", "/", "|"], ["8", "(", "\u00A2"], ["9", ")"], ["0", "="], ["'", "?", "\u00B4"], ["^", "`", "~"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E", "\u20AC"], ["r", "R"], ["t", "T"], ["z", "Z"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["\u00E8", "\u00FC", "["], ["\u00A8", "!", "]"], ["$", "\u00A3", "}"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["\u00E9", "\u00F6"], ["\u00E0", "\u00E4", "{"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">", "\\"], ["y", "Y"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M"], [",", ";"], [".", ":"], ["-", "_"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["fr-CH"] }; + + this.VKI_layout['\u0723\u0718\u072a\u071d\u071d\u0710'] = { + 'name': "Syriac", 'keys': [ + [["\u070f", "\u032e", "\u0651", "\u0651"], ["1", "!", "\u0701", "\u0701"], ["2", "\u030a", "\u0702", "\u0702"], ["3", "\u0325", "\u0703", "\u0703"], ["4", "\u0749", "\u0704", "\u0704"], ["5", "\u2670", "\u0705", "\u0705"], ["6", "\u2671", "\u0708", "\u0708"], ["7", "\u070a", "\u0709", "\u0709"], ["8", "\u00bb", "\u070B", "\u070B"], ["9", ")", "\u070C", "\u070C"], ["0", "(", "\u070D", "\u070D"], ["-", "\u00ab", "\u250C", "\u250C"], ["=", "+", "\u2510", "\u2510"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0714", "\u0730", "\u064E", "\u064E"], ["\u0728", "\u0733", "\u064B", "\u064B"], ["\u0716", "\u0736", "\u064F", "\u064F"], ["\u0729", "\u073A", "\u064C", "\u064C"], ["\u0726", "\u073D", "\u0653", "\u0653"], ["\u071c", "\u0740", "\u0654", "\u0654"], ["\u0725", "\u0741", "\u0747", "\u0747"], ["\u0717", "\u0308", "\u0743", "\u0743"], ["\u071e", "\u0304", "\u0745", "\u0745"], ["\u071a", "\u0307", "\u032D", "\u032D"], ["\u0713", "\u0303"], ["\u0715", "\u074A"], ["\u0706", ":"]], + [["Caps", "Caps"], ["\u072b", "\u0731", "\u0650", "\u0650"], ["\u0723", "\u0734", "\u064d", "\u064d"], ["\u071d", "\u0737"], ["\u0712", "\u073b", "\u0621", "\u0621"], ["\u0720", "\u073e", "\u0655", "\u0655"], ["\u0710", "\u0711", "\u0670", "\u0670"], ["\u072c", "\u0640", "\u0748", "\u0748"], ["\u0722", "\u0324", "\u0744", "\u0744"], ["\u0721", "\u0331", "\u0746", "\u0746"], ["\u071f", "\u0323"], ["\u071b", "\u0330"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["]", "\u0732"], ["[", "\u0735", "\u0652", "\u0652"], ["\u0724", "\u0738"], ["\u072a", "\u073c", "\u200D"], ["\u0727", "\u073f", "\u200C"], ["\u0700", "\u0739", "\u200E"], [".", "\u0742", "\u200F"], ["\u0718", "\u060c"], ["\u0719", "\u061b"], ["\u0707", "\u061F"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["syc"] }; + + this.VKI_layout['\u0ba4\u0bae\u0bbf\u0bb4\u0bcd'] = { + 'name': "Tamil", 'keys': [ + [["\u0BCA", "\u0B92"], ["1", "", "\u0BE7"], ["2", "", "\u0BE8"], ["3", "", "\u0BE9"], ["4", "", "\u0BEA"], ["5", "", "\u0BEB"], ["6", "\u0BA4\u0BCD\u0BB0", "\u0BEC"], ["7", "\u0B95\u0BCD\u0BB7", "\u0BED"], ["8", "\u0BB7\u0BCD\u0BB0", "\u0BEE"], ["9", "", "\u0BEF"], ["0", "", "\u0BF0"], ["-", "\u0B83", "\u0BF1"], ["", "", "\u0BF2"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0BCC", "\u0B94"], ["\u0BC8", "\u0B90"], ["\u0BBE", "\u0B86"], ["\u0BC0", "\u0B88"], ["\u0BC2", "\u0B8A"], ["\u0BAA", "\u0BAA"], ["\u0BB9", "\u0B99"], ["\u0B95", "\u0B95"], ["\u0BA4", "\u0BA4"], ["\u0B9C", "\u0B9A"], ["\u0B9F", "\u0B9F"], ["\u0B9E"]], + [["Caps", "Caps"], ["\u0BCB", "\u0B93"], ["\u0BC7", "\u0B8F"], ["\u0BCD", "\u0B85"], ["\u0BBF", "\u0B87"], ["\u0BC1", "\u0B89"], ["\u0BAA", "\u0BAA"], ["\u0BB0", "\u0BB1"], ["\u0B95", "\u0B95"], ["\u0BA4", "\u0BA4"], ["\u0B9A", "\u0B9A"], ["\u0B9F", "\u0B9F"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0BC6", "\u0B8E"], [""], ["\u0BAE", "\u0BA3"], ["\u0BA8", "\u0BA9"], ["\u0BB5", "\u0BB4"], ["\u0BB2", "\u0BB3"], ["\u0BB8", "\u0BB7"], [",", "\u0BB7"], [".", "\u0BB8\u0BCD\u0BB0\u0BC0"], ["\u0BAF", "\u0BAF"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["ta"] }; + + this.VKI_layout['\u0c24\u0c46\u0c32\u0c41\u0c17\u0c41'] = { + 'name': "Telugu", 'keys': [ + [["\u0C4A", "\u0C12"], ["1", "", "\u0C67"], ["2", "", "\u0C68"], ["3", "\u0C4D\u0C30", "\u0C69"], ["4", "", "\u0C6A"], ["5", "\u0C1C\u0C4D\u0C1E", "\u0C6B"], ["6", "\u0C24\u0C4D\u0C30", "\u0C6C"], ["7", "\u0C15\u0C4D\u0C37", "\u0C6D"], ["8", "\u0C36\u0C4D\u0C30", "\u0C6E"], ["9", "(", "\u0C6F"], ["0", ")", "\u0C66"], ["-", "\u0C03"], ["\u0C43", "\u0C0B", "\u0C44"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0C4C", "\u0C14"], ["\u0C48", "\u0C10", "\u0C56"], ["\u0C3E", "\u0C06"], ["\u0C40", "\u0C08", "", "\u0C61"], ["\u0C42", "\u0C0A"], ["\u0C2C"], ["\u0C39", "\u0C19"], ["\u0C17", "\u0C18"], ["\u0C26", "\u0C27"], ["\u0C1C", "\u0C1D"], ["\u0C21", "\u0C22"], ["", "\u0C1E"]], + [["Caps", "Caps"], ["\u0C4B", "\u0C13"], ["\u0C47", "\u0C0F", "\u0C55"], ["\u0C4D", "\u0C05"], ["\u0C3F", "\u0C07", "", "\u0C0C"], ["\u0C41", "\u0C09"], ["\u0C2A", "\u0C2B"], ["\u0C30", "\u0C31"], ["\u0C15", "\u0C16"], ["\u0C24", "\u0C25"], ["\u0C1A", "\u0C1B"], ["\u0C1F", "\u0C25"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0C46", "\u0C0E"], ["\u0C02", "\u0C01"], ["\u0C2E", "\u0C23"], ["\u0C28", "\u0C28"], ["\u0C35"], ["\u0C32", "\u0C33"], ["\u0C38", "\u0C36"], [",", "\u0C37"], ["."], ["\u0C2F"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["te"] }; + + this.VKI_layout['Ti\u1ebfng Vi\u1ec7t'] = { + 'name': "Vietnamese", 'keys': [ + [["`", "~", "`", "~"], ["\u0103", "\u0102", "1", "!"], ["\u00E2", "\u00C2", "2", "@"], ["\u00EA", "\u00CA", "3", "#"], ["\u00F4", "\u00D4", "4", "$"], ["\u0300", "\u0300", "5", "%"], ["\u0309", "\u0309", "6", "^"], ["\u0303", "\u0303", "7", "&"], ["\u0301", "\u0301", "8", "*"], ["\u0323", "\u0323", "9", "("], ["\u0111", "\u0110", "0", ")"], ["-", "_", "-", "_"], ["\u20AB", "+", "=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q", "q", "Q"], ["w", "W", "w", "W"], ["e", "E", "e", "E"], ["r", "R", "r", "R"], ["t", "T", "t", "T"], ["y", "Y", "y", "Y"], ["u", "U", "u", "U"], ["i", "I", "i", "I"], ["o", "O", "o", "O"], ["p", "P", "p", "P"], ["\u01B0", "\u01AF", "[", "{"], ["\u01A1", "\u01A0", "]", "}"], ["\\", "|", "\\", "|"]], + [["Caps", "Caps"], ["a", "A", "a", "A"], ["s", "S", "s", "S"], ["d", "D", "d", "D"], ["f", "F", "f", "F"], ["g", "G", "g", "G"], ["h", "H", "h", "H"], ["j", "J", "j", "J"], ["k", "K", "k", "K"], ["l", "L", "l", "L"], [";", ":", ";", ":"], ["'", '"', "'", '"'], ["Enter", "Enter"]], + [["Shift", "Shift"], ["z", "Z", "z", "Z"], ["x", "X", "x", "X"], ["c", "C", "c", "C"], ["v", "V", "v", "V"], ["b", "B", "b", "B"], ["n", "N", "n", "N"], ["m", "M", "m", "M"], [",", "<", ",", "<"], [".", ">", ".", ">"], ["/", "?", "/", "?"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["vi"] }; + + this.VKI_layout['\u0e44\u0e17\u0e22 Kedmanee'] = { + 'name': "Thai Kedmanee", 'keys': [ + [["_", "%"], ["\u0E45", "+"], ["/", "\u0E51"], ["-", "\u0E52"], ["\u0E20", "\u0E53"], ["\u0E16", "\u0E54"], ["\u0E38", "\u0E39"], ["\u0E36", "\u0E3F"], ["\u0E04", "\u0E55"], ["\u0E15", "\u0E56"], ["\u0E08", "\u0E57"], ["\u0E02", "\u0E58"], ["\u0E0A", "\u0E59"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0E46", "\u0E50"], ["\u0E44", '"'], ["\u0E33", "\u0E0E"], ["\u0E1E", "\u0E11"], ["\u0E30", "\u0E18"], ["\u0E31", "\u0E4D"], ["\u0E35", "\u0E4A"], ["\u0E23", "\u0E13"], ["\u0E19", "\u0E2F"], ["\u0E22", "\u0E0D"], ["\u0E1A", "\u0E10"], ["\u0E25", ","], ["\u0E03", "\u0E05"]], + [["Caps", "Caps"], ["\u0E1F", "\u0E24"], ["\u0E2B", "\u0E06"], ["\u0E01", "\u0E0F"], ["\u0E14", "\u0E42"], ["\u0E40", "\u0E0C"], ["\u0E49", "\u0E47"], ["\u0E48", "\u0E4B"], ["\u0E32", "\u0E29"], ["\u0E2A", "\u0E28"], ["\u0E27", "\u0E0B"], ["\u0E07", "."], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0E1C", "("], ["\u0E1B", ")"], ["\u0E41", "\u0E09"], ["\u0E2D", "\u0E2E"], ["\u0E34", "\u0E3A"], ["\u0E37", "\u0E4C"], ["\u0E17", "?"], ["\u0E21", "\u0E12"], ["\u0E43", "\u0E2C"], ["\u0E1D", "\u0E26"], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["th"] }; + + this.VKI_layout['\u0e44\u0e17\u0e22 Pattachote'] = { + 'name': "Thai Pattachote", 'keys': [ + [["_", "\u0E3F"], ["=", "+"], ["\u0E52", '"'], ["\u0E53", "/"], ["\u0E54", ","], ["\u0E55", "?"], ["\u0E39", "\u0E38"], ["\u0E57", "_"], ["\u0E58", "."], ["\u0E59", "("], ["\u0E50", ")"], ["\u0E51", "-"], ["\u0E56", "%"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0E47", "\u0E4A"], ["\u0E15", "\u0E24"], ["\u0E22", "\u0E46"], ["\u0E2D", "\u0E0D"], ["\u0E23", "\u0E29"], ["\u0E48", "\u0E36"], ["\u0E14", "\u0E1D"], ["\u0E21", "\u0E0B"], ["\u0E27", "\u0E16"], ["\u0E41", "\u0E12"], ["\u0E43", "\u0E2F"], ["\u0E0C", "\u0E26"], ["\uF8C7", "\u0E4D"]], + [["Caps", "Caps"], ["\u0E49", "\u0E4B"], ["\u0E17", "\u0E18"], ["\u0E07", "\u0E33"], ["\u0E01", "\u0E13"], ["\u0E31", "\u0E4C"], ["\u0E35", "\u0E37"], ["\u0E32", "\u0E1C"], ["\u0E19", "\u0E0A"], ["\u0E40", "\u0E42"], ["\u0E44", "\u0E06"], ["\u0E02", "\u0E11"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0E1A", "\u0E0E"], ["\u0E1B", "\u0E0F"], ["\u0E25", "\u0E10"], ["\u0E2B", "\u0E20"], ["\u0E34", "\u0E31"], ["\u0E04", "\u0E28"], ["\u0E2A", "\u0E2E"], ["\u0E30", "\u0E1F"], ["\u0E08", "\u0E09"], ["\u0E1E", "\u0E2C"], ["Shift", "Shift"]], + [[" ", " "]] + ]}; + + this.VKI_layout['\u0422\u0430\u0442\u0430\u0440\u0447\u0430'] = { + 'name': "Tatar", 'keys': [ + [["\u04BB", "\u04BA", "\u0451", "\u0401"], ["1", "!"], ["2", '"', "@"], ["3", "\u2116", "#"], ["4", ";", "$"], ["5", "%"], ["6", ":"], ["7", "?", "["], ["8", "*", "]"], ["9", "(", "{"], ["0", ")", "}"], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0439", "\u0419"], ["\u04E9", "\u04E8", "\u0446", "\u0426"], ["\u0443", "\u0423"], ["\u043A", "\u041A"], ["\u0435", "\u0415"], ["\u043D", "\u041D"], ["\u0433", "\u0413"], ["\u0448", "\u0428"], ["\u04D9", "\u04D8", "\u0449", "\u0429"], ["\u0437", "\u0417"], ["\u0445", "\u0425"], ["\u04AF", "\u04AE", "\u044A", "\u042A"], ["\\", "/"]], + [["Caps", "Caps"], ["\u0444", "\u0424"], ["\u044B", "\u042B"], ["\u0432", "\u0412"], ["\u0430", "\u0410"], ["\u043F", "\u041F"], ["\u0440", "\u0420"], ["\u043E", "\u041E"], ["\u043B", "\u041B"], ["\u0434", "\u0414"], ["\u04A3", "\u04A2", "\u0436", "\u0416"], ["\u044D", "\u042D", "'"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0491", "\u0490"], ["\u044F", "\u042F"], ["\u0447", "\u0427"], ["\u0441", "\u0421"], ["\u043C", "\u041C"], ["\u0438", "\u0418"], ["\u0442", "\u0422"], ["\u0497", "\u0496", "\u044C", "\u042C"], ["\u0431", "\u0411", "<"], ["\u044E", "\u042E", ">"], [".", ","], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["tt"] }; + + this.VKI_layout['T\u00fcrk\u00e7e F'] = { + 'name': "Turkish F", 'keys': [ + [['+', "*", "\u00ac"], ["1", "!", "\u00b9", "\u00a1"], ["2", '"', "\u00b2"], ["3", "^", "#", "\u00b3"], ["4", "$", "\u00bc", "\u00a4"], ["5", "%", "\u00bd"], ["6", "&", "\u00be"], ["7", "'", "{"], ["8", "(", '['], ["9", ")", ']'], ["0", "=", "}"], ["/", "?", "\\", "\u00bf"], ["-", "_", "|"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["f", "F", "@"], ["g", "G"], ["\u011f", "\u011e"], ["\u0131", "I", "\u00b6", "\u00ae"], ["o", "O"], ["d", "D", "\u00a5"], ["r", "R"], ["n", "N"], ["h", "H", "\u00f8", "\u00d8"], ["p", "P", "\u00a3"], ["q", "Q", "\u00a8"], ["w", "W", "~"], ["x", "X", "`"]], + [["Caps", "Caps"], ["u", "U", "\u00e6", "\u00c6"], ["i", "\u0130", "\u00df", "\u00a7"], ["e", "E", "\u20ac"], ["a", "A", " ", "\u00aa"], ["\u00fc", "\u00dc"], ["t", "T"], ["k", "K"], ["m", "M"], ["l", "L"], ["y", "Y", "\u00b4"], ["\u015f", "\u015e"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">", "|", "\u00a6"], ["j", "J", "\u00ab", "<"], ["\u00f6", "\u00d6", "\u00bb", ">"], ["v", "V", "\u00a2", "\u00a9"], ["c", "C"], ["\u00e7", "\u00c7"], ["z", "Z"], ["s", "S", "\u00b5", "\u00ba"], ["b", "B", "\u00d7"], [".", ":", "\u00f7"], [",", ";", "-"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ]}; + + this.VKI_layout['T\u00fcrk\u00e7e Q'] = { + 'name': "Turkish Q", 'keys': [ + [['"', "\u00e9", "<"], ["1", "!", ">"], ["2", "'", "\u00a3"], ["3", "^", "#"], ["4", "+", "$"], ["5", "%", "\u00bd"], ["6", "&"], ["7", "/", "{"], ["8", "(", '['], ["9", ")", ']'], ["0", "=", "}"], ["*", "?", "\\"], ["-", "_", "|"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q", "@"], ["w", "W"], ["e", "E", "\u20ac"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["\u0131", "I", "i", "\u0130"], ["o", "O"], ["p", "P"], ["\u011f", "\u011e", "\u00a8"], ["\u00fc", "\u00dc", "~"], [",", ";", "`"]], + [["Caps", "Caps"], ["a", "A", "\u00e6", "\u00c6"], ["s", "S", "\u00df"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], ["\u015f", "\u015e", "\u00b4"], ["i", "\u0130"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["<", ">", "|"], ["z", "Z"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M"], ["\u00f6", "\u00d6"], ["\u00e7", "\u00c7"], [".", ":"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["tr"] }; + + this.VKI_layout['\u0423\u043a\u0440\u0430\u0457\u043d\u0441\u044c\u043a\u0430'] = { + 'name': "Ukrainian", 'keys': [ + [["\u00b4", "~"], ["1", "!"], ["2", '"'], ["3", "\u2116"], ["4", ";"], ["5", "%"], ["6", ":"], ["7", "?"], ["8", "*"], ["9", "("], ["0", ")"], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0439", "\u0419"], ["\u0446", "\u0426"], ["\u0443", "\u0423"], ["\u043A", "\u041A"], ["\u0435", "\u0415"], ["\u043D", "\u041D"], ["\u0433", "\u0413"], ["\u0448", "\u0428"], ["\u0449", "\u0429"], ["\u0437", "\u0417"], ["\u0445", "\u0425"], ["\u0457", "\u0407"], ["\u0491", "\u0490"]], + [["Caps", "Caps"], ["\u0444", "\u0424"], ["\u0456", "\u0406"], ["\u0432", "\u0412"], ["\u0430", "\u0410"], ["\u043F", "\u041F"], ["\u0440", "\u0420"], ["\u043E", "\u041E"], ["\u043B", "\u041B"], ["\u0434", "\u0414"], ["\u0436", "\u0416"], ["\u0454", "\u0404"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u044F", "\u042F"], ["\u0447", "\u0427"], ["\u0441", "\u0421"], ["\u043C", "\u041C"], ["\u0438", "\u0418"], ["\u0442", "\u0422"], ["\u044C", "\u042C"], ["\u0431", "\u0411"], ["\u044E", "\u042E"], [".", ","], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["uk"] }; + + this.VKI_layout['United Kingdom'] = { + 'name': "United Kingdom", 'keys': [ + [["`", "\u00ac", "\u00a6"], ["1", "!"], ["2", '"'], ["3", "\u00a3"], ["4", "$", "\u20ac"], ["5", "%"], ["6", "^"], ["7", "&"], ["8", "*"], ["9", "("], ["0", ")"], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E", "\u00e9", "\u00c9"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U", "\u00fa", "\u00da"], ["i", "I", "\u00ed", "\u00cd"], ["o", "O", "\u00f3", "\u00d3"], ["p", "P"], ["[", "{"], ["]", "}"], ["#", "~"]], + [["Caps", "Caps"], ["a", "A", "\u00e1", "\u00c1"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], [";", ":"], ["'", "@"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\\", "|"], ["z", "Z"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M"], [",", "<"], [".", ">"], ["/", "?"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["AltGr", "AltGr"]] + ], 'lang': ["en-gb"] }; + + this.VKI_layout['\u0627\u0631\u062f\u0648'] = { + 'name': "Urdu", 'keys': [ + [["`", "~"], ["1", "!"], ["2", "@"], ["3", "#"], ["4", "$"], ["5", "\u066A"], ["6", "^"], ["7", "\u06D6"], ["8", "\u066D"], ["9", ")"], ["0", "("], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0637", "\u0638"], ["\u0635", "\u0636"], ["\u06be", "\u0630"], ["\u062f", "\u0688"], ["\u0679", "\u062B"], ["\u067e", "\u0651"], ["\u062a", "\u06C3"], ["\u0628", "\u0640"], ["\u062c", "\u0686"], ["\u062d", "\u062E"], ["]", "}"], ["[", "{"], ["\\", "|"]], + [["Caps", "Caps"], ["\u0645", "\u0698"], ["\u0648", "\u0632"], ["\u0631", "\u0691"], ["\u0646", "\u06BA"], ["\u0644", "\u06C2"], ["\u06c1", "\u0621"], ["\u0627", "\u0622"], ["\u06A9", "\u06AF"], ["\u06CC", "\u064A"], ["\u061b", ":"], ["'", '"'], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0642", "\u200D"], ["\u0641", "\u200C"], ["\u06D2", "\u06D3"], ["\u0633", "\u200E"], ["\u0634", "\u0624"], ["\u063a", "\u0626"], ["\u0639", "\u200F"], ["\u060C", ">"], ["\u06D4", "<"], ["/", "\u061F"], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["ur"] }; + + this.VKI_layout['\u0627\u0631\u062f\u0648 Phonetic'] = { + 'name': "Urdu Phonetic", 'keys': [ + [["\u064D", "\u064B", "~"], ["\u06F1", "1", "!"], ["\u06F2", "2", "@"], ["\u06F3", "3", "#"], ["\u06F4", "4", "$"], ["\u06F5", "5", "\u066A"], ["\u06F6", "6", "^"], ["\u06F7", "7", "&"], ["\u06F8", "8", "*"], ["\u06F9", "9", "("], ["\u06F0", "0", ")"], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0642", "\u0652"], ["\u0648", "\u0651", "\u0602"], ["\u0639", "\u0670", "\u0656"], ["\u0631", "\u0691", "\u0613"], ["\u062A", "\u0679", "\u0614"], ["\u06D2", "\u064E", "\u0601"], ["\u0621", "\u0626", "\u0654"], ["\u06CC", "\u0650", "\u0611"], ["\u06C1", "\u06C3"], ["\u067E", "\u064F", "\u0657"], ["[", "{"], ["]", "}"], ["\\", "|"]], + [["Caps", "Caps"], ["\u0627", "\u0622", "\uFDF2"], ["\u0633", "\u0635", "\u0610"], ["\u062F", "\u0688", "\uFDFA"], ["\u0641"], ["\u06AF", "\u063A"], ["\u062D", "\u06BE", "\u0612"], ["\u062C", "\u0636", "\uFDFB"], ["\u06A9", "\u062E"], ["\u0644"], ["\u061B", ":"], ["'", '"'], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u0632", "\u0630", "\u060F"], ["\u0634", "\u0698", "\u060E"], ["\u0686", "\u062B", "\u0603"], ["\u0637", "\u0638"], ["\u0628", "", "\uFDFD"], ["\u0646", "\u06BA", "\u0600"], ["\u0645", "\u0658"], ["\u060C", "", "<"], ["\u06D4", "\u066B", ">"], ["/", "\u061F"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["Alt", "Alt"]] + ]}; + + this.VKI_layout['US Standard'] = { + 'name': "US Standard", 'keys': [ + [["`", "~"], ["1", "!"], ["2", "@"], ["3", "#"], ["4", "$"], ["5", "%"], ["6", "^"], ["7", "&"], ["8", "*"], ["9", "("], ["0", ")"], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q"], ["w", "W"], ["e", "E"], ["r", "R"], ["t", "T"], ["y", "Y"], ["u", "U"], ["i", "I"], ["o", "O"], ["p", "P"], ["[", "{"], ["]", "}"], ["\\", "|"]], + [["Caps", "Caps"], ["a", "A"], ["s", "S"], ["d", "D"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L"], [";", ":"], ["'", '"'], ["Enter", "Enter"]], + [["Shift", "Shift"], ["z", "Z"], ["x", "X"], ["c", "C"], ["v", "V"], ["b", "B"], ["n", "N"], ["m", "M"], [",", "<"], [".", ">"], ["/", "?"], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["en-us"] }; + + this.VKI_layout['US International'] = { + 'name': "US International", 'keys': [ + [["`", "~"], ["1", "!", "\u00a1", "\u00b9"], ["2", "@", "\u00b2"], ["3", "#", "\u00b3"], ["4", "$", "\u00a4", "\u00a3"], ["5", "%", "\u20ac"], ["6", "^", "\u00bc"], ["7", "&", "\u00bd"], ["8", "*", "\u00be"], ["9", "(", "\u2018"], ["0", ")", "\u2019"], ["-", "_", "\u00a5"], ["=", "+", "\u00d7", "\u00f7"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["q", "Q", "\u00e4", "\u00c4"], ["w", "W", "\u00e5", "\u00c5"], ["e", "E", "\u00e9", "\u00c9"], ["r", "R", "\u00ae"], ["t", "T", "\u00fe", "\u00de"], ["y", "Y", "\u00fc", "\u00dc"], ["u", "U", "\u00fa", "\u00da"], ["i", "I", "\u00ed", "\u00cd"], ["o", "O", "\u00f3", "\u00d3"], ["p", "P", "\u00f6", "\u00d6"], ["[", "{", "\u00ab"], ["]", "}", "\u00bb"], ["\\", "|", "\u00ac", "\u00a6"]], + [["Caps", "Caps"], ["a", "A", "\u00e1", "\u00c1"], ["s", "S", "\u00df", "\u00a7"], ["d", "D", "\u00f0", "\u00d0"], ["f", "F"], ["g", "G"], ["h", "H"], ["j", "J"], ["k", "K"], ["l", "L", "\u00f8", "\u00d8"], [";", ":", "\u00b6", "\u00b0"], ["'", '"', "\u00b4", "\u00a8"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["z", "Z", "\u00e6", "\u00c6"], ["x", "X"], ["c", "C", "\u00a9", "\u00a2"], ["v", "V"], ["b", "B"], ["n", "N", "\u00f1", "\u00d1"], ["m", "M", "\u00b5"], [",", "<", "\u00e7", "\u00c7"], [".", ">"], ["/", "?", "\u00bf"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["Alt", "Alt"]] + ], 'lang': ["en"] }; + + this.VKI_layout['\u040e\u0437\u0431\u0435\u043a\u0447\u0430'] = { + 'name': "Uzbek Cyrillic", 'keys': [ + [["\u0451", "\u0401"], ["1", "!"], ["2", '"'], ["3", "\u2116"], ["4", ";"], ["5", "%"], ["6", ":"], ["7", "?"], ["8", "*"], ["9", "("], ["0", ")"], ["\u0493", "\u0492"], ["\u04B3", "\u04B2"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u0439", "\u0419"], ["\u0446", "\u0426"], ["\u0443", "\u0423"], ["\u043A", "\u041A"], ["\u0435", "\u0415"], ["\u043D", "\u041D"], ["\u0433", "\u0413"], ["\u0448", "\u0428"], ["\u045E", "\u040E"], ["\u0437", "\u0417"], ["\u0445", "\u0425"], ["\u044A", "\u042A"], ["\\", "/"]], + [["Caps", "Caps"], ["\u0444", "\u0424"], ["\u049B", "\u049A"], ["\u0432", "\u0412"], ["\u0430", "\u0410"], ["\u043F", "\u041F"], ["\u0440", "\u0420"], ["\u043E", "\u041E"], ["\u043B", "\u041B"], ["\u0434", "\u0414"], ["\u0436", "\u0416"], ["\u044D", "\u042D"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u044F", "\u042F"], ["\u0447", "\u0427"], ["\u0441", "\u0421"], ["\u043C", "\u041C"], ["\u0438", "\u0418"], ["\u0442", "\u0422"], ["\u044C", "\u042C"], ["\u0431", "\u0411"], ["\u044E", "\u042E"], [".", ","], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["uz"] }; + + this.VKI_layout['\u05d9\u05d9\u05b4\u05d3\u05d9\u05e9'] = { // from http://www.yv.org/uyip/hebyidkbd.txt http://uyip.org/keyboards.html + 'name': "Yiddish", 'keys': [ + [[";", "~", "\u05B0"], ["1", "!", "\u05B1"], ["2", "@", "\u05B2"], ["3", "#", "\u05B3"], ["4", "$", "\u05B4"], ["5", "%", "\u05B5"], ["6", "^", "\u05B6"], ["7", "*", "\u05B7"], ["8", "&", "\u05B8"], ["9", "(", "\u05C2"], ["0", ")", "\u05C1"], ["-", "_", "\u05B9"], ["=", "+", "\u05BC"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["/", "\u201F", "\u201F"], ["'", "\u201E", "\u201E"], ["\u05E7", "`", "`"], ["\u05E8", "\uFB2F", "\uFB2F"], ["\u05D0", "\uFB2E", "\uFB2E"], ["\u05D8", "\u05F0", "\u05F0"], ["\u05D5", "\uFB35", "\uFB35"], ["\u05DF", "\uFB4B", "\uFB4B"], ["\u05DD", "\uFB4E", "\uFB4E"], ["\u05E4", "\uFB44", "\uFB44"], ["[", "{", "\u05BD"], ["]", "}", "\u05BF"], ["\\", "|", "\u05BB"]], + [["Caps", "Caps"], ["\u05E9", "\uFB2A", "\uFB2A"], ["\u05D3", "\uFB2B", "\uFB2B"], ["\u05D2"], ["\u05DB", "\uFB3B", "\uFB3B"], ["\u05E2", "\u05F1", "\u05F1"], ["\u05D9", "\uFB1D", "\uFB1D"], ["\u05D7", "\uFF1F", "\uFF1F"], ["\u05DC", "\u05F2", "\u05F2"], ["\u05DA"], ["\u05E3", ":", "\u05C3"], [",", '"', "\u05C0"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u05D6", "\u2260", "\u2260"], ["\u05E1", "\uFB4C", "\uFB4C"], ["\u05D1", "\uFB31", "\uFB31"], ["\u05D4", "\u05BE", "\u05BE"], ["\u05E0", "\u2013", "\u2013"], ["\u05DE", "\u2014", "\u2014"], ["\u05E6", "\uFB4A", "\uFB4A"], ["\u05EA", "<", "\u05F3"], ["\u05E5", ">", "\u05F4"], [".", "?", "\u20AA"], ["Shift", "Shift"]], + [[" ", " "], ["Alt", "Alt"]] + ], 'lang': ["yi"] }; + + this.VKI_layout['\u05d9\u05d9\u05b4\u05d3\u05d9\u05e9 \u05dc\u05e2\u05d1\u05d8'] = { // from http://jidysz.net/ + 'name': "Yiddish (Yidish Lebt)", 'keys': [ + [[";", "~"], ["1", "!", "\u05B2", "\u05B2"], ["2", "@", "\u05B3", "\u05B3"], ["3", "#", "\u05B1", "\u05B1"], ["4", "$", "\u05B4", "\u05B4"], ["5", "%", "\u05B5", "\u05B5"], ["6", "^", "\u05B7", "\u05B7"], ["7", "&", "\u05B8", "\u05B8"], ["8", "*", "\u05BB", "\u05BB"], ["9", ")", "\u05B6", "\u05B6"], ["0", "(", "\u05B0", "\u05B0"], ["-", "_", "\u05BF", "\u05BF"], ["=", "+", "\u05B9", "\u05B9"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["/", "", "\u05F4", "\u05F4"], ["'", "", "\u05F3", "\u05F3"], ["\u05E7", "", "\u20AC"], ["\u05E8"], ["\u05D0", "", "\u05D0\u05B7", "\uFB2E"], ["\u05D8", "", "\u05D0\u05B8", "\uFB2F"], ["\u05D5", "\u05D5\u05B9", "\u05D5\u05BC", "\uFB35"], ["\u05DF", "", "\u05D5\u05D5", "\u05F0"], ["\u05DD", "", "\u05BC"], ["\u05E4", "", "\u05E4\u05BC", "\uFB44"], ["]", "}", "\u201E", "\u201D"], ["[", "{", "\u201A", "\u2019"], ["\\", "|", "\u05BE", "\u05BE"]], + [["Caps", "Caps"], ["\u05E9", "\u05E9\u05C1", "\u05E9\u05C2", "\uFB2B"], ["\u05D3", "", "\u20AA"], ["\u05D2", "\u201E"], ["\u05DB", "", "\u05DB\u05BC", "\uFB3B"], ["\u05E2", "", "", "\uFB20"], ["\u05D9", "", "\u05D9\u05B4", "\uFB1D"], ["\u05D7", "", "\u05F2\u05B7", "\uFB1F"], ["\u05DC", "\u05DC\u05B9", "\u05D5\u05D9", "\u05F1"], ["\u05DA", "", "", "\u05F2"], ["\u05E3", ":", "\u05E4\u05BF", "\uFB4E"], [",", '"', ";", "\u05B2"], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u05D6", "", "\u2013", "\u2013"], ["\u05E1", "", "\u2014", "\u2014"], ["\u05D1", "\u05DC\u05B9", "\u05D1\u05BF", "\uFB4C"], ["\u05D4", "", "\u201D", "\u201C"], ["\u05E0", "", "\u059C", "\u059E"], ["\u05DE", "", "\u2019", "\u2018"], ["\u05E6", "", "\u05E9\u05C1", "\uFB2A"], ["\u05EA", ">", "\u05EA\u05BC", "\uFB4A"], ["\u05E5", "<"], [".", "?", "\u2026"], ["Shift", "Shift"]], + [[" ", " ", " ", " "], ["Alt", "Alt"]] + ], 'lang': ["yi"] }; + + this.VKI_layout['\u4e2d\u6587\u6ce8\u97f3\u7b26\u53f7'] = { + 'name': "Chinese Bopomofo IME", 'keys': [ + [["\u20AC", "~"], ["\u3105", "!"], ["\u3109", "@"], ["\u02C7", "#"], ["\u02CB", "$"], ["\u3113", "%"], ["\u02CA", "^"], ["\u02D9", "&"], ["\u311A", "*"], ["\u311E", ")"], ["\u3122", "("], ["\u3126", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u3106", "q"], ["\u310A", "w"], ["\u310D", "e"], ["\u3110", "r"], ["\u3114", "t"], ["\u3117", "y"], ["\u3127", "u"], ["\u311B", "i"], ["\u311F", "o"], ["\u3123", "p"], ["[", "{"], ["]", "}"], ["\\", "|"]], + [["Caps", "Caps"], ["\u3107", "a"], ["\u310B", "s"], ["\u310E", "d"], ["\u3111", "f"], ["\u3115", "g"], ["\u3118", "h"], ["\u3128", "j"], ["\u311C", "k"], ["\u3120", "l"], ["\u3124", ":"], ["'", '"'], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\u3108", "z"], ["\u310C", "x"], ["\u310F", "c"], ["\u3112", "v"], ["\u3116", "b"], ["\u3119", "n"], ["\u3129", "m"], ["\u311D", "<"], ["\u3121", ">"], ["\u3125", "?"], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["zh-Bopo"] }; + + this.VKI_layout['\u4e2d\u6587\u4ed3\u9889\u8f93\u5165\u6cd5'] = { + 'name': "Chinese Cangjie IME", 'keys': [ + [["\u20AC", "~"], ["1", "!"], ["2", "@"], ["3", "#"], ["4", "$"], ["5", "%"], ["6", "^"], ["7", "&"], ["8", "*"], ["9", ")"], ["0", "("], ["-", "_"], ["=", "+"], ["Bksp", "Bksp"]], + [["Tab", "Tab"], ["\u624B", "q"], ["\u7530", "w"], ["\u6C34", "e"], ["\u53E3", "r"], ["\u5EFF", "t"], ["\u535C", "y"], ["\u5C71", "u"], ["\u6208", "i"], ["\u4EBA", "o"], ["\u5FC3", "p"], ["[", "{"], ["]", "}"], ["\\", "|"]], + [["Caps", "Caps"], ["\u65E5", "a"], ["\u5C38", "s"], ["\u6728", "d"], ["\u706B", "f"], ["\u571F", "g"], ["\u7AF9", "h"], ["\u5341", "j"], ["\u5927", "k"], ["\u4E2D", "l"], [";", ":"], ["'", '"'], ["Enter", "Enter"]], + [["Shift", "Shift"], ["\uFF3A", "z"], ["\u96E3", "x"], ["\u91D1", "c"], ["\u5973", "v"], ["\u6708", "b"], ["\u5F13", "n"], ["\u4E00", "m"], [",", "<"], [".", ">"], ["/", "?"], ["Shift", "Shift"]], + [[" ", " "]] + ], 'lang': ["zh"] }; + + + /* ***** Define Dead Keys ************************************** */ + this.VKI_deadkey = {}; + + // - Lay out each dead key set as an object of property/value + // pairs. The rows below are wrapped so uppercase letters are + // below their lowercase equivalents. + // + // - The property name is the letter pressed after the diacritic. + // The property value is the letter this key-combo will generate. + // + // - Note that if you have created a new keyboard layout and want + // it included in the distributed script, PLEASE TELL ME if you + // have added additional dead keys to the ones below. + + this.VKI_deadkey['"'] = this.VKI_deadkey['\u00a8'] = this.VKI_deadkey['\u309B'] = { // Umlaut / Diaeresis / Greek Dialytika / Hiragana/Katakana Voiced Sound Mark + 'a': "\u00e4", 'e': "\u00eb", 'i': "\u00ef", 'o': "\u00f6", 'u': "\u00fc", 'y': "\u00ff", '\u03b9': "\u03ca", '\u03c5': "\u03cb", '\u016B': "\u01D6", '\u00FA': "\u01D8", '\u01D4': "\u01DA", '\u00F9': "\u01DC", + 'A': "\u00c4", 'E': "\u00cb", 'I': "\u00cf", 'O': "\u00d6", 'U': "\u00dc", 'Y': "\u0178", '\u0399': "\u03aa", '\u03a5': "\u03ab", '\u016A': "\u01D5", '\u00DA': "\u01D7", '\u01D3': "\u01D9", '\u00D9': "\u01DB", + '\u304b': "\u304c", '\u304d': "\u304e", '\u304f': "\u3050", '\u3051': "\u3052", '\u3053': "\u3054", '\u305f': "\u3060", '\u3061': "\u3062", '\u3064': "\u3065", '\u3066': "\u3067", '\u3068': "\u3069", + '\u3055': "\u3056", '\u3057': "\u3058", '\u3059': "\u305a", '\u305b': "\u305c", '\u305d': "\u305e", '\u306f': "\u3070", '\u3072': "\u3073", '\u3075': "\u3076", '\u3078': "\u3079", '\u307b': "\u307c", + '\u30ab': "\u30ac", '\u30ad': "\u30ae", '\u30af': "\u30b0", '\u30b1': "\u30b2", '\u30b3': "\u30b4", '\u30bf': "\u30c0", '\u30c1': "\u30c2", '\u30c4': "\u30c5", '\u30c6': "\u30c7", '\u30c8': "\u30c9", + '\u30b5': "\u30b6", '\u30b7': "\u30b8", '\u30b9': "\u30ba", '\u30bb': "\u30bc", '\u30bd': "\u30be", '\u30cf': "\u30d0", '\u30d2': "\u30d3", '\u30d5': "\u30d6", '\u30d8': "\u30d9", '\u30db': "\u30dc" + }; + this.VKI_deadkey['~'] = { // Tilde / Stroke + 'a': "\u00e3", 'l': "\u0142", 'n': "\u00f1", 'o': "\u00f5", + 'A': "\u00c3", 'L': "\u0141", 'N': "\u00d1", 'O': "\u00d5" + }; + this.VKI_deadkey['^'] = { // Circumflex + 'a': "\u00e2", 'e': "\u00ea", 'i': "\u00ee", 'o': "\u00f4", 'u': "\u00fb", 'w': "\u0175", 'y': "\u0177", + 'A': "\u00c2", 'E': "\u00ca", 'I': "\u00ce", 'O': "\u00d4", 'U': "\u00db", 'W': "\u0174", 'Y': "\u0176" + }; + this.VKI_deadkey['\u02c7'] = { // Baltic caron + 'c': "\u010D", 'd': "\u010f", 'e': "\u011b", 's': "\u0161", 'l': "\u013e", 'n': "\u0148", 'r': "\u0159", 't': "\u0165", 'u': "\u01d4", 'z': "\u017E", '\u00fc': "\u01da", + 'C': "\u010C", 'D': "\u010e", 'E': "\u011a", 'S': "\u0160", 'L': "\u013d", 'N': "\u0147", 'R': "\u0158", 'T': "\u0164", 'U': "\u01d3", 'Z': "\u017D", '\u00dc': "\u01d9" + }; + this.VKI_deadkey['\u02d8'] = { // Romanian and Turkish breve + 'a': "\u0103", 'g': "\u011f", + 'A': "\u0102", 'G': "\u011e" + }; + this.VKI_deadkey['-'] = this.VKI_deadkey['\u00af'] = { // Macron + 'a': "\u0101", 'e': "\u0113", 'i': "\u012b", 'o': "\u014d", 'u': "\u016B", 'y': "\u0233", '\u00fc': "\u01d6", + 'A': "\u0100", 'E': "\u0112", 'I': "\u012a", 'O': "\u014c", 'U': "\u016A", 'Y': "\u0232", '\u00dc': "\u01d5" + }; + this.VKI_deadkey['`'] = { // Grave + 'a': "\u00e0", 'e': "\u00e8", 'i': "\u00ec", 'o': "\u00f2", 'u': "\u00f9", '\u00fc': "\u01dc", + 'A': "\u00c0", 'E': "\u00c8", 'I': "\u00cc", 'O': "\u00d2", 'U': "\u00d9", '\u00dc': "\u01db" + }; + this.VKI_deadkey["'"] = this.VKI_deadkey['\u00b4'] = this.VKI_deadkey['\u0384'] = { // Acute / Greek Tonos + 'a': "\u00e1", 'e': "\u00e9", 'i': "\u00ed", 'o': "\u00f3", 'u': "\u00fa", 'y': "\u00fd", '\u03b1': "\u03ac", '\u03b5': "\u03ad", '\u03b7': "\u03ae", '\u03b9': "\u03af", '\u03bf': "\u03cc", '\u03c5': "\u03cd", '\u03c9': "\u03ce", '\u00fc': "\u01d8", + 'A': "\u00c1", 'E': "\u00c9", 'I': "\u00cd", 'O': "\u00d3", 'U': "\u00da", 'Y': "\u00dd", '\u0391': "\u0386", '\u0395': "\u0388", '\u0397': "\u0389", '\u0399': "\u038a", '\u039f': "\u038c", '\u03a5': "\u038e", '\u03a9': "\u038f", '\u00dc': "\u01d7" + }; + this.VKI_deadkey['\u02dd'] = { // Hungarian Double Acute Accent + 'o': "\u0151", 'u': "\u0171", + 'O': "\u0150", 'U': "\u0170" + }; + this.VKI_deadkey['\u0385'] = { // Greek Dialytika + Tonos + '\u03b9': "\u0390", '\u03c5': "\u03b0" + }; + this.VKI_deadkey['\u00b0'] = this.VKI_deadkey['\u00ba'] = { // Ring + 'a': "\u00e5", 'u': "\u016f", + 'A': "\u00c5", 'U': "\u016e" + }; + this.VKI_deadkey['\u02DB'] = { // Ogonek + 'a': "\u0106", 'e': "\u0119", 'i': "\u012f", 'o': "\u01eb", 'u': "\u0173", 'y': "\u0177", + 'A': "\u0105", 'E': "\u0118", 'I': "\u012e", 'O': "\u01ea", 'U': "\u0172", 'Y': "\u0176" + }; + this.VKI_deadkey['\u02D9'] = { // Dot-above + 'c': "\u010B", 'e': "\u0117", 'g': "\u0121", 'z': "\u017C", + 'C': "\u010A", 'E': "\u0116", 'G': "\u0120", 'Z': "\u017B" + }; + this.VKI_deadkey['\u00B8'] = this.VKI_deadkey['\u201a'] = { // Cedilla + 'c': "\u00e7", 's': "\u015F", + 'C': "\u00c7", 'S': "\u015E" + }; + this.VKI_deadkey[','] = { // Comma + 's': (this.VKI_isIElt8) ? "\u015F" : "\u0219", 't': (this.VKI_isIElt8) ? "\u0163" : "\u021B", + 'S': (this.VKI_isIElt8) ? "\u015E" : "\u0218", 'T': (this.VKI_isIElt8) ? "\u0162" : "\u021A" + }; + this.VKI_deadkey['\u3002'] = { // Hiragana/Katakana Point + '\u306f': "\u3071", '\u3072': "\u3074", '\u3075': "\u3077", '\u3078': "\u307a", '\u307b': "\u307d", + '\u30cf': "\u30d1", '\u30d2': "\u30d4", '\u30d5': "\u30d7", '\u30d8': "\u30da", '\u30db': "\u30dd" + }; + + + /* ***** Define Symbols **************************************** */ + this.VKI_symbol = { + '\u00a0': "NB\nSP", '\u200b': "ZW\nSP", '\u200c': "ZW\nNJ", '\u200d': "ZW\nJ" + }; + + + /* ***** Layout Number Pad ************************************* */ + this.VKI_numpad = [ + [["$"], ["\u00a3"], ["\u20ac"], ["\u00a5"]], + [["7"], ["8"], ["9"], ["/"]], + [["4"], ["5"], ["6"], ["*"]], + [["1"], ["2"], ["3"], ["-"]], + [["0"], ["."], ["="], ["+"]] + ]; + + + /* **************************************************************** + * Attach the keyboard to an element + * + */ + VKI_attach = function(elem) { + if (elem.getAttribute("VKI_attached")) return false; + if (self.VKI_imageURI) { + var keybut = document.createElement('img'); + keybut.src = self.VKI_imageURI; + keybut.alt = self.VKI_i18n['01']; + keybut.className = "keyboardInputInitiator"; + keybut.title = self.VKI_i18n['01']; + keybut.elem = elem; + keybut.onclick = function(e) { + e = e || event; + if (e.stopPropagation) { e.stopPropagation(); } else e.cancelBubble = true; + self.VKI_show(this.elem); + }; + elem.parentNode.insertBefore(keybut, (elem.dir == "rtl") ? elem : elem.nextSibling); + } else { + elem.onfocus = function() { + if (self.VKI_target != this) { + if (self.VKI_target) self.VKI_close(); + self.VKI_show(this); + } + }; + elem.onclick = function() { + if (!self.VKI_target) self.VKI_show(this); + } + } + elem.setAttribute("VKI_attached", 'true'); + if (self.VKI_isIE) { + elem.onclick = elem.onselect = elem.onkeyup = function(e) { + if ((e || event).type != "keyup" || !this.readOnly) + this.range = document.selection.createRange(); + }; + } + VKI_addListener(elem, 'click', function(e) { + if (self.VKI_target == this) { + e = e || event; + if (e.stopPropagation) { e.stopPropagation(); } else e.cancelBubble = true; + } return false; + }, false); + if (self.VKI_isMoz) + elem.addEventListener('blur', function() { this.setAttribute('_scrollTop', this.scrollTop); }, false); + }; + + + /* ***** Find tagged input & textarea elements ***************** */ + function VKI_buildKeyboardInputs() { + var inputElems = [ + document.getElementsByTagName('input'), + document.getElementsByTagName('textarea') + ]; + for (var x = 0, elem; elem = inputElems[x++];) + for (var y = 0, ex; ex = elem[y++];) + if (ex.nodeName == "TEXTAREA" || ex.type == "text" || ex.type == "password") + if (ex.className.indexOf("keyboardInput") > -1) VKI_attach(ex); + + VKI_addListener(document.documentElement, 'click', function(e) { self.VKI_close(); }, false); + } + + + /* **************************************************************** + * Common mouse event actions + * + */ + function VKI_mouseEvents(elem) { + if (elem.nodeName == "TD") { + if (!elem.click) elem.click = function() { + var evt = this.ownerDocument.createEvent('MouseEvents'); + evt.initMouseEvent('click', true, true, this.ownerDocument.defaultView, 1, 0, 0, 0, 0, false, false, false, false, 0, null); + this.dispatchEvent(evt); + }; + elem.VKI_clickless = 0; + VKI_addListener(elem, 'dblclick', function() { return false; }, false); + } + VKI_addListener(elem, 'mouseover', function() { + if (this.nodeName == "TD" && self.VKI_clickless) { + var _self = this; + clearTimeout(this.VKI_clickless); + this.VKI_clickless = setTimeout(function() { _self.click(); }, self.VKI_clickless); + } + if (self.VKI_isIE) this.className += " hover"; + }, false); + VKI_addListener(elem, 'mouseout', function() { + if (this.nodeName == "TD") clearTimeout(this.VKI_clickless); + if (self.VKI_isIE) this.className = this.className.replace(/ ?(hover|pressed) ?/g, ""); + }, false); + VKI_addListener(elem, 'mousedown', function() { + if (this.nodeName == "TD") clearTimeout(this.VKI_clickless); + if (self.VKI_isIE) this.className += " pressed"; + }, false); + VKI_addListener(elem, 'mouseup', function() { + if (this.nodeName == "TD") clearTimeout(this.VKI_clickless); + if (self.VKI_isIE) this.className = this.className.replace(/ ?pressed ?/g, ""); + }, false); + } + + + /* ***** Build the keyboard interface ************************** */ + this.VKI_keyboard = document.createElement('table'); + this.VKI_keyboard.id = "keyboardInputMaster"; + this.VKI_keyboard.dir = "ltr"; + this.VKI_keyboard.cellSpacing = "0"; + this.VKI_keyboard.reflow = function() { + this.style.width = "50px"; + var foo = this.offsetWidth; + this.style.width = ""; + }; + VKI_addListener(this.VKI_keyboard, 'click', function(e) { + e = e || event; + if (e.stopPropagation) { e.stopPropagation(); } else e.cancelBubble = true; + return false; + }, false); + + if (!this.VKI_layout[this.VKI_kt]) + return alert('No keyboard named "' + this.VKI_kt + '"'); + + this.VKI_langCode = {}; + var thead = document.createElement('thead'); + var tr = document.createElement('tr'); + var th = document.createElement('th'); + th.colSpan = "2"; + + var kbSelect = document.createElement('div'); + kbSelect.title = this.VKI_i18n['02']; + VKI_addListener(kbSelect, 'click', function() { + var ol = this.getElementsByTagName('ol')[0]; + if (!ol.style.display) { + ol.style.display = "block"; + var li = ol.getElementsByTagName('li'); + for (var x = 0, scr = 0; x < li.length; x++) { + if (VKI_kt == li[x].firstChild.nodeValue) { + li[x].className = "selected"; + scr = li[x].offsetTop - li[x].offsetHeight * 2; + } else li[x].className = ""; + } setTimeout(function() { ol.scrollTop = scr; }, 0); + } else ol.style.display = ""; + }, false); + kbSelect.appendChild(document.createTextNode(this.VKI_kt)); + kbSelect.appendChild(document.createTextNode(this.VKI_isIElt8 ? " \u2193" : " \u25be")); + kbSelect.langCount = 0; + var ol = document.createElement('ol'); + for (ktype in this.VKI_layout) { + if (typeof this.VKI_layout[ktype] == "object") { + if (!this.VKI_layout[ktype].lang) this.VKI_layout[ktype].lang = []; + for (var x = 0; x < this.VKI_layout[ktype].lang.length; x++) + this.VKI_langCode[this.VKI_layout[ktype].lang[x].toLowerCase().replace(/-/g, "_")] = ktype; + var li = document.createElement('li'); + li.title = this.VKI_layout[ktype].name; + VKI_addListener(li, 'click', function(e) { + e = e || event; + if (e.stopPropagation) { e.stopPropagation(); } else e.cancelBubble = true; + this.parentNode.style.display = ""; + self.VKI_kts = self.VKI_kt = kbSelect.firstChild.nodeValue = this.firstChild.nodeValue; + self.VKI_buildKeys(); + self.VKI_position(true); + }, false); + VKI_mouseEvents(li); + li.appendChild(document.createTextNode(ktype)); + ol.appendChild(li); + kbSelect.langCount++; + } + } kbSelect.appendChild(ol); + if (kbSelect.langCount > 1) th.appendChild(kbSelect); + this.VKI_langCode.index = []; + for (prop in this.VKI_langCode) + if (prop != "index" && typeof this.VKI_langCode[prop] == "string") + this.VKI_langCode.index.push(prop); + this.VKI_langCode.index.sort(); + this.VKI_langCode.index.reverse(); + + if (this.VKI_numberPad) { + var span = document.createElement('span'); + span.appendChild(document.createTextNode("#")); + span.title = this.VKI_i18n['00']; + VKI_addListener(span, 'click', function() { + kbNumpad.style.display = (!kbNumpad.style.display) ? "none" : ""; + self.VKI_position(true); + }, false); + VKI_mouseEvents(span); + th.appendChild(span); + } + + this.VKI_kbsize = function(e) { + self.VKI_size = Math.min(5, Math.max(1, self.VKI_size)); + self.VKI_keyboard.className = self.VKI_keyboard.className.replace(/ ?keyboardInputSize\d ?/, ""); + if (self.VKI_size != 2) self.VKI_keyboard.className += " keyboardInputSize" + self.VKI_size; + self.VKI_position(true); + if (self.VKI_isOpera) self.VKI_keyboard.reflow(); + }; + if (this.VKI_sizeAdj) { + var small = document.createElement('small'); + small.title = this.VKI_i18n['10']; + VKI_addListener(small, 'click', function() { + --self.VKI_size; + self.VKI_kbsize(); + }, false); + VKI_mouseEvents(small); + small.appendChild(document.createTextNode(this.VKI_isIElt8 ? "\u2193" : "\u21d3")); + th.appendChild(small); + var big = document.createElement('big'); + big.title = this.VKI_i18n['11']; + VKI_addListener(big, 'click', function() { + ++self.VKI_size; + self.VKI_kbsize(); + }, false); + VKI_mouseEvents(big); + big.appendChild(document.createTextNode(this.VKI_isIElt8 ? "\u2191" : "\u21d1")); + th.appendChild(big); + } + + var span = document.createElement('span'); + span.appendChild(document.createTextNode(this.VKI_i18n['07'])); + span.title = this.VKI_i18n['08']; + VKI_addListener(span, 'click', function() { + self.VKI_target.value = ""; + self.VKI_target.focus(); + return false; + }, false); + VKI_mouseEvents(span); + th.appendChild(span); + + var strong = document.createElement('strong'); + strong.appendChild(document.createTextNode('X')); + strong.title = this.VKI_i18n['06']; + VKI_addListener(strong, 'click', function() { self.VKI_close(); }, false); + VKI_mouseEvents(strong); + th.appendChild(strong); + + tr.appendChild(th); + thead.appendChild(tr); + this.VKI_keyboard.appendChild(thead); + + var tbody = document.createElement('tbody'); + var tr = document.createElement('tr'); + var td = document.createElement('td'); + var div = document.createElement('div'); + + if (this.VKI_deadBox) { + var label = document.createElement('label'); + var checkbox = document.createElement('input'); + checkbox.type = "checkbox"; + checkbox.title = this.VKI_i18n['03'] + ": " + ((this.VKI_deadkeysOn) ? this.VKI_i18n['04'] : this.VKI_i18n['05']); + checkbox.defaultChecked = this.VKI_deadkeysOn; + VKI_addListener(checkbox, 'click', function() { + this.title = self.VKI_i18n['03'] + ": " + ((this.checked) ? self.VKI_i18n['04'] : self.VKI_i18n['05']); + self.VKI_modify(""); + return true; + }, false); + label.appendChild(checkbox); + checkbox.checked = this.VKI_deadkeysOn; + div.appendChild(label); + this.VKI_deadkeysOn = checkbox; + } else this.VKI_deadkeysOn.checked = this.VKI_deadkeysOn; + + if (this.VKI_showVersion) { + var vr = document.createElement('var'); + vr.title = this.VKI_i18n['09'] + " " + this.VKI_version; + vr.appendChild(document.createTextNode("v" + this.VKI_version)); + div.appendChild(vr); + } td.appendChild(div); + tr.appendChild(td); + + var kbNumpad = document.createElement('td'); + kbNumpad.id = "keyboardInputNumpad"; + if (!this.VKI_numberPadOn) kbNumpad.style.display = "none"; + var ntable = document.createElement('table'); + ntable.cellSpacing = "0"; + var ntbody = document.createElement('tbody'); + for (var x = 0; x < this.VKI_numpad.length; x++) { + var ntr = document.createElement('tr'); + for (var y = 0; y < this.VKI_numpad[x].length; y++) { + var ntd = document.createElement('td'); + VKI_addListener(ntd, 'click', VKI_keyClick, false); + VKI_mouseEvents(ntd); + ntd.appendChild(document.createTextNode(this.VKI_numpad[x][y])); + ntr.appendChild(ntd); + } ntbody.appendChild(ntr); + } ntable.appendChild(ntbody); + kbNumpad.appendChild(ntable); + tr.appendChild(kbNumpad); + tbody.appendChild(tr); + this.VKI_keyboard.appendChild(tbody); + + if (this.VKI_isIE6) { + this.VKI_iframe = document.createElement('iframe'); + this.VKI_iframe.style.position = "absolute"; + this.VKI_iframe.style.border = "0px none"; + this.VKI_iframe.style.filter = "mask()"; + this.VKI_iframe.style.zIndex = "999999"; + this.VKI_iframe.src = this.VKI_imageURI; + } + + + /* **************************************************************** + * Private table cell attachment function for generic characters + * + */ + function VKI_keyClick() { + var done = false, character = "\xa0"; + if (this.firstChild.nodeName.toLowerCase() != "small") { + if ((character = this.firstChild.nodeValue) == "\xa0") return false; + } else character = this.firstChild.getAttribute('char'); + if (self.VKI_deadkeysOn.checked && self.VKI_dead) { + if (self.VKI_dead != character) { + if (character != " ") { + if (self.VKI_deadkey[self.VKI_dead][character]) { + self.VKI_insert(self.VKI_deadkey[self.VKI_dead][character]); + done = true; + } + } else { + self.VKI_insert(self.VKI_dead); + done = true; + } + } else done = true; + } self.VKI_dead = false; + + if (!done) { + if (self.VKI_deadkeysOn.checked && self.VKI_deadkey[character]) { + self.VKI_dead = character; + this.className += " dead"; + if (self.VKI_shift) self.VKI_modify("Shift"); + if (self.VKI_altgr) self.VKI_modify("AltGr"); + } else self.VKI_insert(character); + } self.VKI_modify(""); + return false; + } + + + /* **************************************************************** + * Build or rebuild the keyboard keys + * + */ + this.VKI_buildKeys = function() { + this.VKI_shift = this.VKI_shiftlock = this.VKI_altgr = this.VKI_altgrlock = this.VKI_dead = false; + var container = this.VKI_keyboard.tBodies[0].getElementsByTagName('div')[0]; + var tables = container.getElementsByTagName('table'); + for (var x = tables.length - 1; x >= 0; x--) container.removeChild(tables[x]); + + for (var x = 0, hasDeadKey = false, lyt; lyt = this.VKI_layout[this.VKI_kt].keys[x++];) { + var table = document.createElement('table'); + table.cellSpacing = "0"; + if (lyt.length <= this.VKI_keyCenter) table.className = "keyboardInputCenter"; + var tbody = document.createElement('tbody'); + var tr = document.createElement('tr'); + for (var y = 0, lkey; lkey = lyt[y++];) { + var td = document.createElement('td'); + if (this.VKI_symbol[lkey[0]]) { + var text = this.VKI_symbol[lkey[0]].split("\n"); + var small = document.createElement('small'); + small.setAttribute('char', lkey[0]); + for (var z = 0; z < text.length; z++) { + if (z) small.appendChild(document.createElement("br")); + small.appendChild(document.createTextNode(text[z])); + } td.appendChild(small); + } else td.appendChild(document.createTextNode(lkey[0] || "\xa0")); + + var className = []; + if (this.VKI_deadkeysOn.checked) + for (key in this.VKI_deadkey) + if (key === lkey[0]) { className.push("deadkey"); break; } + if (lyt.length > this.VKI_keyCenter && y == lyt.length) className.push("last"); + if (lkey[0] == " " || lkey[1] == " ") className.push("space"); + td.className = className.join(" "); + + switch (lkey[1]) { + case "Caps": case "Shift": + case "Alt": case "AltGr": case "AltLk": + VKI_addListener(td, 'click', (function(type) { return function() { self.VKI_modify(type); return false; }})(lkey[1]), false); + break; + case "Tab": + VKI_addListener(td, 'click', function() { + if (self.VKI_activeTab) { + if (self.VKI_target.form) { + var target = self.VKI_target, elems = target.form.elements; + self.VKI_close(); + for (var z = 0, me = false, j = -1; z < elems.length; z++) { + if (j == -1 && elems[z].getAttribute("VKI_attached")) j = z; + if (me) { + if (self.VKI_activeTab == 1 && elems[z]) break; + if (elems[z].getAttribute("VKI_attached")) break; + } else if (elems[z] == target) me = true; + } if (z == elems.length) z = Math.max(j, 0); + if (elems[z].getAttribute("VKI_attached")) { + self.VKI_show(elems[z]); + } else elems[z].focus(); + } else self.VKI_target.focus(); + } else self.VKI_insert("\t"); + return false; + }, false); + break; + case "Bksp": + VKI_addListener(td, 'click', function() { + self.VKI_target.focus(); + if (self.VKI_target.setSelectionRange && !self.VKI_target.readOnly) { + var rng = [self.VKI_target.selectionStart, self.VKI_target.selectionEnd]; + if (rng[0] < rng[1]) rng[0]++; + self.VKI_target.value = self.VKI_target.value.substr(0, rng[0] - 1) + self.VKI_target.value.substr(rng[1]); + self.VKI_target.setSelectionRange(rng[0] - 1, rng[0] - 1); + } else if (self.VKI_target.createTextRange && !self.VKI_target.readOnly) { + try { + self.VKI_target.range.select(); + } catch(e) { self.VKI_target.range = document.selection.createRange(); } + if (!self.VKI_target.range.text.length) self.VKI_target.range.moveStart('character', -1); + self.VKI_target.range.text = ""; + } else self.VKI_target.value = self.VKI_target.value.substr(0, self.VKI_target.value.length - 1); + if (self.VKI_shift) self.VKI_modify("Shift"); + if (self.VKI_altgr) self.VKI_modify("AltGr"); + self.VKI_target.focus(); + return true; + }, false); + break; + case "Enter": + VKI_addListener(td, 'click', function() { + if (self.VKI_target.nodeName != "TEXTAREA") { + if (self.VKI_enterSubmit && self.VKI_target.form) { + for (var z = 0, subm = false; z < self.VKI_target.form.elements.length; z++) + if (self.VKI_target.form.elements[z].type == "submit") subm = true; + if (!subm) self.VKI_target.form.submit(); + } + self.VKI_close(); + } else self.VKI_insert("\n"); + return true; + }, false); + break; + default: + VKI_addListener(td, 'click', VKI_keyClick, false); + + } VKI_mouseEvents(td); + tr.appendChild(td); + for (var z = 0; z < 4; z++) + if (this.VKI_deadkey[lkey[z] = lkey[z] || ""]) hasDeadKey = true; + } tbody.appendChild(tr); + table.appendChild(tbody); + container.appendChild(table); + } + if (this.VKI_deadBox) + this.VKI_deadkeysOn.style.display = (hasDeadKey) ? "inline" : "none"; + if (this.VKI_isIE6) { + this.VKI_iframe.style.width = this.VKI_keyboard.offsetWidth + "px"; + this.VKI_iframe.style.height = this.VKI_keyboard.offsetHeight + "px"; + } + }; + + this.VKI_buildKeys(); + VKI_addListener(this.VKI_keyboard, 'selectstart', function() { return false; }, false); + this.VKI_keyboard.unselectable = "on"; + if (this.VKI_isOpera) + VKI_addListener(this.VKI_keyboard, 'mousedown', function() { return false; }, false); + + + /* **************************************************************** + * Controls modifier keys + * + */ + this.VKI_modify = function(type) { + switch (type) { + case "Alt": + case "AltGr": this.VKI_altgr = !this.VKI_altgr; break; + case "AltLk": this.VKI_altgr = 0; this.VKI_altgrlock = !this.VKI_altgrlock; break; + case "Caps": this.VKI_shift = 0; this.VKI_shiftlock = !this.VKI_shiftlock; break; + case "Shift": this.VKI_shift = !this.VKI_shift; break; + } var vchar = 0; + if (!this.VKI_shift != !this.VKI_shiftlock) vchar += 1; + if (!this.VKI_altgr != !this.VKI_altgrlock) vchar += 2; + + var tables = this.VKI_keyboard.tBodies[0].getElementsByTagName('div')[0].getElementsByTagName('table'); + for (var x = 0; x < tables.length; x++) { + var tds = tables[x].getElementsByTagName('td'); + for (var y = 0; y < tds.length; y++) { + var className = [], lkey = this.VKI_layout[this.VKI_kt].keys[x][y]; + + switch (lkey[1]) { + case "Alt": + case "AltGr": + if (this.VKI_altgr) className.push("pressed"); + break; + case "AltLk": + if (this.VKI_altgrlock) className.push("pressed"); + break; + case "Shift": + if (this.VKI_shift) className.push("pressed"); + break; + case "Caps": + if (this.VKI_shiftlock) className.push("pressed"); + break; + case "Tab": case "Enter": case "Bksp": break; + default: + if (type) { + tds[y].removeChild(tds[y].firstChild); + if (this.VKI_symbol[lkey[vchar]]) { + var text = this.VKI_symbol[lkey[vchar]].split("\n"); + var small = document.createElement('small'); + small.setAttribute('char', lkey[vchar]); + for (var z = 0; z < text.length; z++) { + if (z) small.appendChild(document.createElement("br")); + small.appendChild(document.createTextNode(text[z])); + } tds[y].appendChild(small); + } else tds[y].appendChild(document.createTextNode(lkey[vchar] || "\xa0")); + } + if (this.VKI_deadkeysOn.checked) { + var character = tds[y].firstChild.nodeValue || tds[y].firstChild.className; + if (this.VKI_dead) { + if (character == this.VKI_dead) className.push("pressed"); + if (this.VKI_deadkey[this.VKI_dead][character]) className.push("target"); + } + if (this.VKI_deadkey[character]) className.push("deadkey"); + } + } + + if (y == tds.length - 1 && tds.length > this.VKI_keyCenter) className.push("last"); + if (lkey[0] == " " || lkey[1] == " ") className.push("space"); + tds[y].className = className.join(" "); + } + } + }; + + + /* **************************************************************** + * Insert text at the cursor + * + */ + this.VKI_insert = function(text) { + /*this.VKI_target.focus(); + if (this.VKI_target.maxLength) this.VKI_target.maxlength = this.VKI_target.maxLength; + if (typeof this.VKI_target.maxlength == "undefined" || + this.VKI_target.maxlength < 0 || + this.VKI_target.value.length < this.VKI_target.maxlength) { + if (this.VKI_target.setSelectionRange && !this.VKI_target.readOnly && !this.VKI_isIE) { + var rng = [this.VKI_target.selectionStart, this.VKI_target.selectionEnd]; + this.VKI_target.value = this.VKI_target.value.substr(0, rng[0]) + text + this.VKI_target.value.substr(rng[1]); + if (text == "\n" && this.VKI_isOpera) rng[0]++; + this.VKI_target.setSelectionRange(rng[0] + text.length, rng[0] + text.length); + } else if (this.VKI_target.createTextRange && !this.VKI_target.readOnly) { + try { + this.VKI_target.range.select(); + } catch(e) { this.VKI_target.range = document.selection.createRange(); } + this.VKI_target.range.text = text; + this.VKI_target.range.collapse(true); + this.VKI_target.range.select(); + } else this.VKI_target.value += text; + if (this.VKI_shift) this.VKI_modify("Shift"); + if (this.VKI_altgr) this.VKI_modify("AltGr"); + this.VKI_target.focus(); + } else if (this.VKI_target.createTextRange && this.VKI_target.range) + this.VKI_target.range.select(); + */ + //alert(text); + document.getElementById("keyboardText").value =text; + document.getElementById("keyboardText").focus(); + }; + + + /* **************************************************************** + * Show the keyboard interface + * + */ + this.VKI_show = function(elem) { + if (!this.VKI_target) { + this.VKI_target = elem; + if (this.VKI_langAdapt && this.VKI_target.lang) { + var chg = false, sub = [], lang = this.VKI_target.lang.toLowerCase().replace(/-/g, "_"); + for (var x = 0, chg = false; !chg && x < this.VKI_langCode.index.length; x++) + if (lang.indexOf(this.VKI_langCode.index[x]) == 0) + chg = kbSelect.firstChild.nodeValue = this.VKI_kt = this.VKI_langCode[this.VKI_langCode.index[x]]; + if (chg) this.VKI_buildKeys(); + } + if (this.VKI_isIE) { + if (!this.VKI_target.range) { + this.VKI_target.range = this.VKI_target.createTextRange(); + this.VKI_target.range.moveStart('character', this.VKI_target.value.length); + } this.VKI_target.range.select(); + } + try { this.VKI_keyboard.parentNode.removeChild(this.VKI_keyboard); } catch (e) {} + if (this.VKI_clearPasswords && this.VKI_target.type == "password") this.VKI_target.value = ""; + + var elem = this.VKI_target; + this.VKI_target.keyboardPosition = "absolute"; + do { + if (VKI_getStyle(elem, "position") == "fixed") { + this.VKI_target.keyboardPosition = "fixed"; + break; + } + } while (elem = elem.offsetParent); + + if (this.VKI_isIE6) document.body.appendChild(this.VKI_iframe); + document.body.appendChild(this.VKI_keyboard); + this.VKI_keyboard.style.position = this.VKI_target.keyboardPosition; + if (this.VKI_isOpera) this.VKI_keyboard.reflow(); + + this.VKI_position(true); + if (self.VKI_isMoz || self.VKI_isWebKit) this.VKI_position(true); + this.VKI_target.blur(); + this.VKI_target.focus(); + } else this.VKI_close(); + }; + + + /* **************************************************************** + * Position the keyboard + * + */ + this.VKI_position = function(force) { + if (self.VKI_target) { + var kPos = VKI_findPos(self.VKI_keyboard), wDim = VKI_innerDimensions(), sDis = VKI_scrollDist(); + var place = false, fudge = self.VKI_target.offsetHeight + 3; + if (force !== true) { + if (kPos[1] + self.VKI_keyboard.offsetHeight - sDis[1] - wDim[1] > 0) { + place = true; + fudge = -self.VKI_keyboard.offsetHeight - 3; + } else if (kPos[1] - sDis[1] < 0) place = true; + } + if (place || force === true) { + var iPos = VKI_findPos(self.VKI_target), scr = self.VKI_target; + while (scr = scr.parentNode) { + if (scr == document.body) break; + if (scr.scrollHeight > scr.offsetHeight || scr.scrollWidth > scr.offsetWidth) { + if (!scr.getAttribute("VKI_scrollListener")) { + scr.setAttribute("VKI_scrollListener", true); + VKI_addListener(scr, 'scroll', function() { self.VKI_position(true); }, false); + } // Check if the input is in view + var pPos = VKI_findPos(scr), oTop = iPos[1] - pPos[1], oLeft = iPos[0] - pPos[0]; + var top = oTop + self.VKI_target.offsetHeight; + var left = oLeft + self.VKI_target.offsetWidth; + var bottom = scr.offsetHeight - oTop - self.VKI_target.offsetHeight; + var right = scr.offsetWidth - oLeft - self.VKI_target.offsetWidth; + self.VKI_keyboard.style.display = (top < 0 || left < 0 || bottom < 0 || right < 0) ? "none" : ""; + if (self.VKI_isIE6) self.VKI_iframe.style.display = (top < 0 || left < 0 || bottom < 0 || right < 0) ? "none" : ""; + } + } + self.VKI_keyboard.style.top = iPos[1] - ((self.VKI_target.keyboardPosition == "fixed" && !self.VKI_isIE && !self.VKI_isMoz) ? sDis[1] : 0) + fudge + "px"; + self.VKI_keyboard.style.left = Math.max(10, Math.min(wDim[0] - self.VKI_keyboard.offsetWidth - 25, iPos[0])) + "px"; + if (self.VKI_isIE6) { + self.VKI_iframe.style.width = self.VKI_keyboard.offsetWidth + "px"; + self.VKI_iframe.style.height = self.VKI_keyboard.offsetHeight + "px"; + self.VKI_iframe.style.top = self.VKI_keyboard.style.top; + self.VKI_iframe.style.left = self.VKI_keyboard.style.left; + } + } + if (force === true) self.VKI_position(); + } + }; + + + /* **************************************************************** + * Close the keyboard interface + * + */ + this.VKI_close = VKI_close = function() { + if (this.VKI_target) { + try { + this.VKI_keyboard.parentNode.removeChild(this.VKI_keyboard); + if (this.VKI_isIE6) this.VKI_iframe.parentNode.removeChild(this.VKI_iframe); + } catch (e) {} + if (this.VKI_kt != this.VKI_kts) { + kbSelect.firstChild.nodeValue = this.VKI_kt = this.VKI_kts; + this.VKI_buildKeys(); + } kbSelect.getElementsByTagName('ol')[0].style.display = "";; + this.VKI_target.focus(); + if (this.VKI_isIE) { + setTimeout(function() { self.VKI_target = false; }, 0); + } else this.VKI_target = false; + } + }; + + + /* ***** Private functions *************************************** */ + function VKI_addListener(elem, type, func, cap) { + if (elem.addEventListener) { + elem.addEventListener(type, function(e) { func.call(elem, e); }, cap); + } else if (elem.attachEvent) + elem.attachEvent('on' + type, function() { func.call(elem); }); + } + + function VKI_findPos(obj) { + var curleft = curtop = 0, scr = obj; + while ((scr = scr.parentNode) && scr != document.body) { + curleft -= scr.scrollLeft || 0; + curtop -= scr.scrollTop || 0; + } + do { + curleft += obj.offsetLeft; + curtop += obj.offsetTop; + } while (obj = obj.offsetParent); + return [curleft, curtop]; + } + + function VKI_innerDimensions() { + if (self.innerHeight) { + return [self.innerWidth, self.innerHeight]; + } else if (document.documentElement && document.documentElement.clientHeight) { + return [document.documentElement.clientWidth, document.documentElement.clientHeight]; + } else if (document.body) + return [document.body.clientWidth, document.body.clientHeight]; + return [0, 0]; + } + + function VKI_scrollDist() { + var html = document.getElementsByTagName('html')[0]; + if (html.scrollTop && document.documentElement.scrollTop) { + return [html.scrollLeft, html.scrollTop]; + } else if (html.scrollTop || document.documentElement.scrollTop) { + return [html.scrollLeft + document.documentElement.scrollLeft, html.scrollTop + document.documentElement.scrollTop]; + } else if (document.body.scrollTop) + return [document.body.scrollLeft, document.body.scrollTop]; + return [0, 0]; + } + + function VKI_getStyle(obj, styleProp) { + if (obj.currentStyle) { + var y = obj.currentStyle[styleProp]; + } else if (window.getComputedStyle) + var y = window.getComputedStyle(obj, null)[styleProp]; + return y; + } + + + VKI_addListener(window, 'resize', this.VKI_position, false); + VKI_addListener(window, 'scroll', this.VKI_position, false); + this.VKI_kbsize(); + VKI_addListener(window, 'load', VKI_buildKeyboardInputs, false); + // VKI_addListener(window, 'load', function() { + // setTimeout(VKI_buildKeyboardInputs, 5); + // }, false); +})(); diff --git a/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/more/keyboard.png b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/more/keyboard.png new file mode 100644 index 0000000000000000000000000000000000000000..dba1c8caf30e6de6fcf2ec26626b6bf812938120 Binary files /dev/null and b/snf-cyclades-app/synnefo/ui/static/snf/extra/noVNC/more/keyboard.png differ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/images/icons/machines/small/snapshot-off.png b/snf-cyclades-app/synnefo/ui/static/snf/images/icons/machines/small/snapshot-off.png new file mode 100644 index 0000000000000000000000000000000000000000..ef58e7706446d909debb3c7e3eae3fe0bbb302b6 Binary files /dev/null and b/snf-cyclades-app/synnefo/ui/static/snf/images/icons/machines/small/snapshot-off.png differ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/images/icons/machines/small/snapshot-on.png b/snf-cyclades-app/synnefo/ui/static/snf/images/icons/machines/small/snapshot-on.png new file mode 100644 index 0000000000000000000000000000000000000000..ef58e7706446d909debb3c7e3eae3fe0bbb302b6 Binary files /dev/null and b/snf-cyclades-app/synnefo/ui/static/snf/images/icons/machines/small/snapshot-on.png differ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/images/icons/machines/small/snapshot.png b/snf-cyclades-app/synnefo/ui/static/snf/images/icons/machines/small/snapshot.png new file mode 100644 index 0000000000000000000000000000000000000000..ef58e7706446d909debb3c7e3eae3fe0bbb302b6 Binary files /dev/null and b/snf-cyclades-app/synnefo/ui/static/snf/images/icons/machines/small/snapshot.png differ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/images/volume-icon-detached.png b/snf-cyclades-app/synnefo/ui/static/snf/images/volume-icon-detached.png new file mode 100644 index 0000000000000000000000000000000000000000..c7654914a58c4a64bc4d01fd8fdecee10f78611d Binary files /dev/null and b/snf-cyclades-app/synnefo/ui/static/snf/images/volume-icon-detached.png differ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/images/volume-icon-small.png b/snf-cyclades-app/synnefo/ui/static/snf/images/volume-icon-small.png new file mode 100644 index 0000000000000000000000000000000000000000..585274ddc726cfaa9336ad59d48f10f9127782e2 Binary files /dev/null and b/snf-cyclades-app/synnefo/ui/static/snf/images/volume-icon-small.png differ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/images/volume-icon.png b/snf-cyclades-app/synnefo/ui/static/snf/images/volume-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c7654914a58c4a64bc4d01fd8fdecee10f78611d Binary files /dev/null and b/snf-cyclades-app/synnefo/ui/static/snf/images/volume-icon.png differ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/auth.js b/snf-cyclades-app/synnefo/ui/static/snf/js/auth.js index 894f3bae0e582329c1f8daed45ff808a7d8270fb..72861fb4e88c6acf182384466c5486b5c6274987 100644 --- a/snf-cyclades-app/synnefo/ui/static/snf/js/auth.js +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/auth.js @@ -1,35 +1,17 @@ -// Copyright 2012 GRNET S.A. All rights reserved. -// -// Redistribution and use in source and binary forms, with or -// without modification, are permitted provided that the following -// conditions are met: -// -// 1. Redistributions of source code must retain the above -// copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following -// disclaimer in the documentation and/or other materials -// provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// -// The views and conclusions contained in the software and -// documentation are those of the authors and should not be -// interpreted as representing official policies, either expressed -// or implied, of GRNET S.A. +// Copyright (C) 2010-2014 GRNET S.A. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. // ;(function(root){ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/glance_models.js b/snf-cyclades-app/synnefo/ui/static/snf/js/glance_models.js index d7d856f7badf2ff309203ec892b50477a4dae001..a94995cfc1073d0711bfde2bd6380f82c8b548b4 100644 --- a/snf-cyclades-app/synnefo/ui/static/snf/js/glance_models.js +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/glance_models.js @@ -1,5 +1,5 @@ ;(function(root){ - + var set = true; // root var root = root; @@ -17,8 +17,13 @@ models.GlanceImage = snf.models.Image.extend({ api_type: 'glance', - get_size: function() { - return this.get('size') / 1024 / 1024; + get_size: function(metric) { + if (metric == undefined) { metric = 'mb' } + var map = { + 'mb': Math.pow(1024, 2), + 'gb': Math.pow(1024, 3) + } + return this.get('size') / map[metric]; }, get_readable_size: function() { @@ -42,6 +47,15 @@ return this.get('owner') || 'Unknown'; }, + is_snapshot: function() { + return this.get('is_snapshot'); + }, + + + is_available: function() { + if (!this.is_snapshot()) { return true } + return this.get("status") === "AVAILABLE"; + }, display_size: function() { return this.get_readable_size(); @@ -49,8 +63,12 @@ display_users: function() { try { - return this.get_meta('users').split(' ').join(", "); - } catch(err) { console.log(err); return ''} + if (this.get_meta('users')) { + return this.get_meta('users').split(' ').join(", "); + } else { + return ""; + } + } catch(err) { console.error(err); return ''} } }) @@ -58,7 +76,6 @@ models.GlanceImages = snf.models.Images.extend({ model: models.GlanceImage, api_type: 'glance', - type_selections: {'personal':'My images', 'shared': 'Shared with me', 'public': 'Public'}, @@ -115,9 +132,30 @@ } img = models.GlanceImages.__super__.parse_meta.call(this, img); + if (img.is_snapshot) { + if (!img.OS) { + img.OS = 'snapshot'; + } + if (!img.metadata) { img.metadata = {}; } + if (!img.metadata || !img.metadata.OS) { + img.metadata.OS = 'snapshot'; + } + } return img; }, + active: function() { + return this.filter(function(img) { + return img.get('status') != "DELETED" && !img.is_snapshot() + }); + }, + + active_snapshots: function() { + return this.filter(function(img) { + return img.get('status') != "DELETED" && img.is_snapshot() + }); + }, + get_system_images: function() { return _.filter(this.active(), function(i) { return _.include(_.keys(snf.config.system_images_owners), @@ -142,7 +180,33 @@ i.get_owner_uuid() != snf.user.get_username() && !i.is_public(); }); - } + }, + + get_snapshot_system_images: function() { + return _.filter(this.active_snapshots(), function(i) { + return _.include(_.keys(snf.config.system_images_owners), + i.get_owner()); + }) + }, + + get_snapshot_personal_images: function() { + return _.filter(this.active_snapshots(), function(i) { + return i.get_owner_uuid() == snf.user.get_username(); + }); + }, + + get_snapshot_public_images: function() { + return _.filter(this.active_snapshots(), function(i){ return i.is_public() }) + }, + + get_snapshot_shared_images: function() { + return _.filter(this.active_snapshots(), function(i){ + return !_.include(_.keys(snf.config.system_images_owners), + i.get_owner()) && + i.get_owner_uuid() != snf.user.get_username() && + !i.is_public(); + }); + }, }) diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/invitations.js b/snf-cyclades-app/synnefo/ui/static/snf/js/invitations.js index d9ccf9b8469685588853443cf2f81c6e7bf2d3eb..b824533e5f830986f3fbdab29826792150d3e53f 100755 --- a/snf-cyclades-app/synnefo/ui/static/snf/js/invitations.js +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/invitations.js @@ -1,35 +1,17 @@ -// Copyright 2011 GRNET S.A. All rights reserved. -// -// Redistribution and use in source and binary forms, with or -// without modification, are permitted provided that the following -// conditions are met: -// -// 1. Redistributions of source code must retain the above -// copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following -// disclaimer in the documentation and/or other materials -// provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// -// The views and conclusions contained in the software and -// documentation are those of the authors and should not be -// interpreted as representing official policies, either expressed -// or implied, of GRNET S.A. +// Copyright (C) 2010-2014 GRNET S.A. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. // /* diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/lib/rivets.conf.js b/snf-cyclades-app/synnefo/ui/static/snf/js/lib/rivets.conf.js index 3dd9159055ae645fedab81384ab19fb6873d5567..d5687e710d86805e016776dc4cb45b18f0a57e31 100644 --- a/snf-cyclades-app/synnefo/ui/static/snf/js/lib/rivets.conf.js +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/lib/rivets.conf.js @@ -27,7 +27,12 @@ COLLECTION_EVENTS = ['add', 'remove', 'update', 'reset'] _.extend(rivets.formatters, { - prefix: function(value, prefix) { + in_brackets: function(value) { + return "[" + value + "]"; + }, + + prefix: function(value) { + var prefix = _.rest(_.toArray(arguments), 1).join(" "); return prefix + value.toString(); }, @@ -43,7 +48,7 @@ _.extend(rivets.formatters, { }, list_truncate: function(value, size) { - size = size === undefined ? 38 : size; + size = size === undefined ? 34 : size; return synnefo.util.truncate(value, size); }, @@ -71,6 +76,22 @@ _.extend(rivets.formatters, { intEq: function(value, cmp) { return parseInt(value) == parseInt(cmp); + }, + + bytes_display: function(value) { + return synnefo.util.readablizeBytes(value); + }, + + disk_size_display: function(value) { + return '{0}GB'.format(value || 0); + }, + + msg_if_empty: function(value) { + var msg = [].slice.call(arguments, 1).join(" "); + if (!value) { + value = msg; + } + return value; } }); @@ -97,7 +118,7 @@ _.extend(rivets.binders, { value = this.view.models.model; } } catch (err) { - console.log("value error"); + console.error("value error", err); } } @@ -115,11 +136,16 @@ _.extend(rivets.binders, { var specs = this.options.formatters[0].split(","); var cls_name = specs[0]; var params = specs[1]; + if (params && this.view.models && + this.view.models.view && + this.view.models.view[params]) { + params = this.view.models.view[params](this, specs); + } else { + if (params) { params = JSON.parse(params) } + } var view_cls = synnefo.views[cls_name]; var view_params = {collection: value}; - if (params) { - _.extend(view_params, JSON.parse(params)); - } + if (params) { _.extend(view_params, params); } var view = this.view.models.view.create_view(view_cls, view_params); this.view.models.view.add_subview(view); view.show(true); @@ -296,7 +322,7 @@ rivets.configure({ }, publish: function(obj, keypath, value) { - throw "Publish not available" + console.error("Publish not available"); }, } diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/lib/select2.min.js b/snf-cyclades-app/synnefo/ui/static/snf/js/lib/select2.min.js new file mode 100644 index 0000000000000000000000000000000000000000..a26a3593fe303457461de4a65356a32a09cd0a7f --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/lib/select2.min.js @@ -0,0 +1,23 @@ +/* +Copyright 2014 Igor Vaynberg + +Version: 3.5.0 Timestamp: Mon Jun 16 19:29:44 EDT 2014 + +This software is licensed under the Apache License, Version 2.0 (the "Apache License") or the GNU +General Public License version 2 (the "GPL License"). You may choose either license to govern your +use of this software only upon the condition that you accept all of the terms of either the Apache +License or the GPL License. + +You may obtain a copy of the Apache License and the GPL License at: + +http://www.apache.org/licenses/LICENSE-2.0 +http://www.gnu.org/licenses/gpl-2.0.html + +Unless required by applicable law or agreed to in writing, software distributed under the Apache License +or the GPL Licesnse is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +either express or implied. See the Apache License and the GPL License for the specific language governing +permissions and limitations under the Apache License and the GPL License. +*/ +!function(a){"undefined"==typeof a.fn.each2&&a.extend(a.fn,{each2:function(b){for(var c=a([0]),d=-1,e=this.length;++d<e&&(c.context=c[0]=this[d])&&b.call(c[0],d,c)!==!1;);return this}})}(jQuery),function(a,b){"use strict";function n(b){var c=a(document.createTextNode(""));b.before(c),c.before(b),c.remove()}function o(a){function b(a){return m[a]||a}return a.replace(/[^\u0000-\u007E]/g,b)}function p(a,b){for(var c=0,d=b.length;d>c;c+=1)if(r(a,b[c]))return c;return-1}function q(){var b=a(l);b.appendTo("body");var c={width:b.width()-b[0].clientWidth,height:b.height()-b[0].clientHeight};return b.remove(),c}function r(a,c){return a===c?!0:a===b||c===b?!1:null===a||null===c?!1:a.constructor===String?a+""==c+"":c.constructor===String?c+""==a+"":!1}function s(b,c){var d,e,f;if(null===b||b.length<1)return[];for(d=b.split(c),e=0,f=d.length;f>e;e+=1)d[e]=a.trim(d[e]);return d}function t(a){return a.outerWidth(!1)-a.width()}function u(c){var d="keyup-change-value";c.on("keydown",function(){a.data(c,d)===b&&a.data(c,d,c.val())}),c.on("keyup",function(){var e=a.data(c,d);e!==b&&c.val()!==e&&(a.removeData(c,d),c.trigger("keyup-change"))})}function v(c){c.on("mousemove",function(c){var d=i;(d===b||d.x!==c.pageX||d.y!==c.pageY)&&a(c.target).trigger("mousemove-filtered",c)})}function w(a,c,d){d=d||b;var e;return function(){var b=arguments;window.clearTimeout(e),e=window.setTimeout(function(){c.apply(d,b)},a)}}function x(a,b){var c=w(a,function(a){b.trigger("scroll-debounced",a)});b.on("scroll",function(a){p(a.target,b.get())>=0&&c(a)})}function y(a){a[0]!==document.activeElement&&window.setTimeout(function(){var d,b=a[0],c=a.val().length;a.focus();var e=b.offsetWidth>0||b.offsetHeight>0;e&&b===document.activeElement&&(b.setSelectionRange?b.setSelectionRange(c,c):b.createTextRange&&(d=b.createTextRange(),d.collapse(!1),d.select()))},0)}function z(b){b=a(b)[0];var c=0,d=0;if("selectionStart"in b)c=b.selectionStart,d=b.selectionEnd-c;else if("selection"in document){b.focus();var e=document.selection.createRange();d=document.selection.createRange().text.length,e.moveStart("character",-b.value.length),c=e.text.length-d}return{offset:c,length:d}}function A(a){a.preventDefault(),a.stopPropagation()}function B(a){a.preventDefault(),a.stopImmediatePropagation()}function C(b){if(!h){var c=b[0].currentStyle||window.getComputedStyle(b[0],null);h=a(document.createElement("div")).css({position:"absolute",left:"-10000px",top:"-10000px",display:"none",fontSize:c.fontSize,fontFamily:c.fontFamily,fontStyle:c.fontStyle,fontWeight:c.fontWeight,letterSpacing:c.letterSpacing,textTransform:c.textTransform,whiteSpace:"nowrap"}),h.attr("class","select2-sizer"),a("body").append(h)}return h.text(b.val()),h.width()}function D(b,c,d){var e,g,f=[];e=a.trim(b.attr("class")),e&&(e=""+e,a(e.split(/\s+/)).each2(function(){0===this.indexOf("select2-")&&f.push(this)})),e=a.trim(c.attr("class")),e&&(e=""+e,a(e.split(/\s+/)).each2(function(){0!==this.indexOf("select2-")&&(g=d(this),g&&f.push(g))})),b.attr("class",f.join(" "))}function E(a,b,c,d){var e=o(a.toUpperCase()).indexOf(o(b.toUpperCase())),f=b.length;return 0>e?(c.push(d(a)),void 0):(c.push(d(a.substring(0,e))),c.push("<span class='select2-match'>"),c.push(d(a.substring(e,e+f))),c.push("</span>"),c.push(d(a.substring(e+f,a.length))),void 0)}function F(a){var b={"\\":"\","&":"&","<":"<",">":">",'"':""","'":"'","/":"/"};return String(a).replace(/[&<>"'\/\\]/g,function(a){return b[a]})}function G(c){var d,e=null,f=c.quietMillis||100,g=c.url,h=this;return function(i){window.clearTimeout(d),d=window.setTimeout(function(){var d=c.data,f=g,j=c.transport||a.fn.select2.ajaxDefaults.transport,k={type:c.type||"GET",cache:c.cache||!1,jsonpCallback:c.jsonpCallback||b,dataType:c.dataType||"json"},l=a.extend({},a.fn.select2.ajaxDefaults.params,k);d=d?d.call(h,i.term,i.page,i.context):null,f="function"==typeof f?f.call(h,i.term,i.page,i.context):f,e&&"function"==typeof e.abort&&e.abort(),c.params&&(a.isFunction(c.params)?a.extend(l,c.params.call(h)):a.extend(l,c.params)),a.extend(l,{url:f,dataType:c.dataType,data:d,success:function(a){var b=c.results(a,i.page,i);i.callback(b)}}),e=j.call(h,l)},f)}}function H(b){var d,e,c=b,f=function(a){return""+a.text};a.isArray(c)&&(e=c,c={results:e}),a.isFunction(c)===!1&&(e=c,c=function(){return e});var g=c();return g.text&&(f=g.text,a.isFunction(f)||(d=g.text,f=function(a){return a[d]})),function(b){var g,d=b.term,e={results:[]};return""===d?(b.callback(c()),void 0):(g=function(c,e){var h,i;if(c=c[0],c.children){h={};for(i in c)c.hasOwnProperty(i)&&(h[i]=c[i]);h.children=[],a(c.children).each2(function(a,b){g(b,h.children)}),(h.children.length||b.matcher(d,f(h),c))&&e.push(h)}else b.matcher(d,f(c),c)&&e.push(c)},a(c().results).each2(function(a,b){g(b,e.results)}),b.callback(e),void 0)}}function I(c){var d=a.isFunction(c);return function(e){var f=e.term,g={results:[]},h=d?c(e):c;a.isArray(h)&&(a(h).each(function(){var a=this.text!==b,c=a?this.text:this;(""===f||e.matcher(f,c))&&g.results.push(a?this:{id:this,text:this})}),e.callback(g))}}function J(b,c){if(a.isFunction(b))return!0;if(!b)return!1;if("string"==typeof b)return!0;throw new Error(c+" must be a string, function, or falsy value")}function K(b,c){if(a.isFunction(b)){var d=Array.prototype.slice.call(arguments,2);return b.apply(c,d)}return b}function L(b){var c=0;return a.each(b,function(a,b){b.children?c+=L(b.children):c++}),c}function M(a,c,d,e){var h,i,j,k,l,f=a,g=!1;if(!e.createSearchChoice||!e.tokenSeparators||e.tokenSeparators.length<1)return b;for(;;){for(i=-1,j=0,k=e.tokenSeparators.length;k>j&&(l=e.tokenSeparators[j],i=a.indexOf(l),!(i>=0));j++);if(0>i)break;if(h=a.substring(0,i),a=a.substring(i+l.length),h.length>0&&(h=e.createSearchChoice.call(this,h,c),h!==b&&null!==h&&e.id(h)!==b&&null!==e.id(h))){for(g=!1,j=0,k=c.length;k>j;j++)if(r(e.id(h),e.id(c[j]))){g=!0;break}g||d(h)}}return f!==a?a:void 0}function N(){var b=this;a.each(arguments,function(a,c){b[c].remove(),b[c]=null})}function O(b,c){var d=function(){};return d.prototype=new b,d.prototype.constructor=d,d.prototype.parent=b.prototype,d.prototype=a.extend(d.prototype,c),d}if(window.Select2===b){var c,d,e,f,g,h,j,k,i={x:0,y:0},c={TAB:9,ENTER:13,ESC:27,SPACE:32,LEFT:37,UP:38,RIGHT:39,DOWN:40,SHIFT:16,CTRL:17,ALT:18,PAGE_UP:33,PAGE_DOWN:34,HOME:36,END:35,BACKSPACE:8,DELETE:46,isArrow:function(a){switch(a=a.which?a.which:a){case c.LEFT:case c.RIGHT:case c.UP:case c.DOWN:return!0}return!1},isControl:function(a){var b=a.which;switch(b){case c.SHIFT:case c.CTRL:case c.ALT:return!0}return a.metaKey?!0:!1},isFunctionKey:function(a){return a=a.which?a.which:a,a>=112&&123>=a}},l="<div class='select2-measure-scrollbar'></div>",m={"\u24b6":"A","\uff21":"A","\xc0":"A","\xc1":"A","\xc2":"A","\u1ea6":"A","\u1ea4":"A","\u1eaa":"A","\u1ea8":"A","\xc3":"A","\u0100":"A","\u0102":"A","\u1eb0":"A","\u1eae":"A","\u1eb4":"A","\u1eb2":"A","\u0226":"A","\u01e0":"A","\xc4":"A","\u01de":"A","\u1ea2":"A","\xc5":"A","\u01fa":"A","\u01cd":"A","\u0200":"A","\u0202":"A","\u1ea0":"A","\u1eac":"A","\u1eb6":"A","\u1e00":"A","\u0104":"A","\u023a":"A","\u2c6f":"A","\ua732":"AA","\xc6":"AE","\u01fc":"AE","\u01e2":"AE","\ua734":"AO","\ua736":"AU","\ua738":"AV","\ua73a":"AV","\ua73c":"AY","\u24b7":"B","\uff22":"B","\u1e02":"B","\u1e04":"B","\u1e06":"B","\u0243":"B","\u0182":"B","\u0181":"B","\u24b8":"C","\uff23":"C","\u0106":"C","\u0108":"C","\u010a":"C","\u010c":"C","\xc7":"C","\u1e08":"C","\u0187":"C","\u023b":"C","\ua73e":"C","\u24b9":"D","\uff24":"D","\u1e0a":"D","\u010e":"D","\u1e0c":"D","\u1e10":"D","\u1e12":"D","\u1e0e":"D","\u0110":"D","\u018b":"D","\u018a":"D","\u0189":"D","\ua779":"D","\u01f1":"DZ","\u01c4":"DZ","\u01f2":"Dz","\u01c5":"Dz","\u24ba":"E","\uff25":"E","\xc8":"E","\xc9":"E","\xca":"E","\u1ec0":"E","\u1ebe":"E","\u1ec4":"E","\u1ec2":"E","\u1ebc":"E","\u0112":"E","\u1e14":"E","\u1e16":"E","\u0114":"E","\u0116":"E","\xcb":"E","\u1eba":"E","\u011a":"E","\u0204":"E","\u0206":"E","\u1eb8":"E","\u1ec6":"E","\u0228":"E","\u1e1c":"E","\u0118":"E","\u1e18":"E","\u1e1a":"E","\u0190":"E","\u018e":"E","\u24bb":"F","\uff26":"F","\u1e1e":"F","\u0191":"F","\ua77b":"F","\u24bc":"G","\uff27":"G","\u01f4":"G","\u011c":"G","\u1e20":"G","\u011e":"G","\u0120":"G","\u01e6":"G","\u0122":"G","\u01e4":"G","\u0193":"G","\ua7a0":"G","\ua77d":"G","\ua77e":"G","\u24bd":"H","\uff28":"H","\u0124":"H","\u1e22":"H","\u1e26":"H","\u021e":"H","\u1e24":"H","\u1e28":"H","\u1e2a":"H","\u0126":"H","\u2c67":"H","\u2c75":"H","\ua78d":"H","\u24be":"I","\uff29":"I","\xcc":"I","\xcd":"I","\xce":"I","\u0128":"I","\u012a":"I","\u012c":"I","\u0130":"I","\xcf":"I","\u1e2e":"I","\u1ec8":"I","\u01cf":"I","\u0208":"I","\u020a":"I","\u1eca":"I","\u012e":"I","\u1e2c":"I","\u0197":"I","\u24bf":"J","\uff2a":"J","\u0134":"J","\u0248":"J","\u24c0":"K","\uff2b":"K","\u1e30":"K","\u01e8":"K","\u1e32":"K","\u0136":"K","\u1e34":"K","\u0198":"K","\u2c69":"K","\ua740":"K","\ua742":"K","\ua744":"K","\ua7a2":"K","\u24c1":"L","\uff2c":"L","\u013f":"L","\u0139":"L","\u013d":"L","\u1e36":"L","\u1e38":"L","\u013b":"L","\u1e3c":"L","\u1e3a":"L","\u0141":"L","\u023d":"L","\u2c62":"L","\u2c60":"L","\ua748":"L","\ua746":"L","\ua780":"L","\u01c7":"LJ","\u01c8":"Lj","\u24c2":"M","\uff2d":"M","\u1e3e":"M","\u1e40":"M","\u1e42":"M","\u2c6e":"M","\u019c":"M","\u24c3":"N","\uff2e":"N","\u01f8":"N","\u0143":"N","\xd1":"N","\u1e44":"N","\u0147":"N","\u1e46":"N","\u0145":"N","\u1e4a":"N","\u1e48":"N","\u0220":"N","\u019d":"N","\ua790":"N","\ua7a4":"N","\u01ca":"NJ","\u01cb":"Nj","\u24c4":"O","\uff2f":"O","\xd2":"O","\xd3":"O","\xd4":"O","\u1ed2":"O","\u1ed0":"O","\u1ed6":"O","\u1ed4":"O","\xd5":"O","\u1e4c":"O","\u022c":"O","\u1e4e":"O","\u014c":"O","\u1e50":"O","\u1e52":"O","\u014e":"O","\u022e":"O","\u0230":"O","\xd6":"O","\u022a":"O","\u1ece":"O","\u0150":"O","\u01d1":"O","\u020c":"O","\u020e":"O","\u01a0":"O","\u1edc":"O","\u1eda":"O","\u1ee0":"O","\u1ede":"O","\u1ee2":"O","\u1ecc":"O","\u1ed8":"O","\u01ea":"O","\u01ec":"O","\xd8":"O","\u01fe":"O","\u0186":"O","\u019f":"O","\ua74a":"O","\ua74c":"O","\u01a2":"OI","\ua74e":"OO","\u0222":"OU","\u24c5":"P","\uff30":"P","\u1e54":"P","\u1e56":"P","\u01a4":"P","\u2c63":"P","\ua750":"P","\ua752":"P","\ua754":"P","\u24c6":"Q","\uff31":"Q","\ua756":"Q","\ua758":"Q","\u024a":"Q","\u24c7":"R","\uff32":"R","\u0154":"R","\u1e58":"R","\u0158":"R","\u0210":"R","\u0212":"R","\u1e5a":"R","\u1e5c":"R","\u0156":"R","\u1e5e":"R","\u024c":"R","\u2c64":"R","\ua75a":"R","\ua7a6":"R","\ua782":"R","\u24c8":"S","\uff33":"S","\u1e9e":"S","\u015a":"S","\u1e64":"S","\u015c":"S","\u1e60":"S","\u0160":"S","\u1e66":"S","\u1e62":"S","\u1e68":"S","\u0218":"S","\u015e":"S","\u2c7e":"S","\ua7a8":"S","\ua784":"S","\u24c9":"T","\uff34":"T","\u1e6a":"T","\u0164":"T","\u1e6c":"T","\u021a":"T","\u0162":"T","\u1e70":"T","\u1e6e":"T","\u0166":"T","\u01ac":"T","\u01ae":"T","\u023e":"T","\ua786":"T","\ua728":"TZ","\u24ca":"U","\uff35":"U","\xd9":"U","\xda":"U","\xdb":"U","\u0168":"U","\u1e78":"U","\u016a":"U","\u1e7a":"U","\u016c":"U","\xdc":"U","\u01db":"U","\u01d7":"U","\u01d5":"U","\u01d9":"U","\u1ee6":"U","\u016e":"U","\u0170":"U","\u01d3":"U","\u0214":"U","\u0216":"U","\u01af":"U","\u1eea":"U","\u1ee8":"U","\u1eee":"U","\u1eec":"U","\u1ef0":"U","\u1ee4":"U","\u1e72":"U","\u0172":"U","\u1e76":"U","\u1e74":"U","\u0244":"U","\u24cb":"V","\uff36":"V","\u1e7c":"V","\u1e7e":"V","\u01b2":"V","\ua75e":"V","\u0245":"V","\ua760":"VY","\u24cc":"W","\uff37":"W","\u1e80":"W","\u1e82":"W","\u0174":"W","\u1e86":"W","\u1e84":"W","\u1e88":"W","\u2c72":"W","\u24cd":"X","\uff38":"X","\u1e8a":"X","\u1e8c":"X","\u24ce":"Y","\uff39":"Y","\u1ef2":"Y","\xdd":"Y","\u0176":"Y","\u1ef8":"Y","\u0232":"Y","\u1e8e":"Y","\u0178":"Y","\u1ef6":"Y","\u1ef4":"Y","\u01b3":"Y","\u024e":"Y","\u1efe":"Y","\u24cf":"Z","\uff3a":"Z","\u0179":"Z","\u1e90":"Z","\u017b":"Z","\u017d":"Z","\u1e92":"Z","\u1e94":"Z","\u01b5":"Z","\u0224":"Z","\u2c7f":"Z","\u2c6b":"Z","\ua762":"Z","\u24d0":"a","\uff41":"a","\u1e9a":"a","\xe0":"a","\xe1":"a","\xe2":"a","\u1ea7":"a","\u1ea5":"a","\u1eab":"a","\u1ea9":"a","\xe3":"a","\u0101":"a","\u0103":"a","\u1eb1":"a","\u1eaf":"a","\u1eb5":"a","\u1eb3":"a","\u0227":"a","\u01e1":"a","\xe4":"a","\u01df":"a","\u1ea3":"a","\xe5":"a","\u01fb":"a","\u01ce":"a","\u0201":"a","\u0203":"a","\u1ea1":"a","\u1ead":"a","\u1eb7":"a","\u1e01":"a","\u0105":"a","\u2c65":"a","\u0250":"a","\ua733":"aa","\xe6":"ae","\u01fd":"ae","\u01e3":"ae","\ua735":"ao","\ua737":"au","\ua739":"av","\ua73b":"av","\ua73d":"ay","\u24d1":"b","\uff42":"b","\u1e03":"b","\u1e05":"b","\u1e07":"b","\u0180":"b","\u0183":"b","\u0253":"b","\u24d2":"c","\uff43":"c","\u0107":"c","\u0109":"c","\u010b":"c","\u010d":"c","\xe7":"c","\u1e09":"c","\u0188":"c","\u023c":"c","\ua73f":"c","\u2184":"c","\u24d3":"d","\uff44":"d","\u1e0b":"d","\u010f":"d","\u1e0d":"d","\u1e11":"d","\u1e13":"d","\u1e0f":"d","\u0111":"d","\u018c":"d","\u0256":"d","\u0257":"d","\ua77a":"d","\u01f3":"dz","\u01c6":"dz","\u24d4":"e","\uff45":"e","\xe8":"e","\xe9":"e","\xea":"e","\u1ec1":"e","\u1ebf":"e","\u1ec5":"e","\u1ec3":"e","\u1ebd":"e","\u0113":"e","\u1e15":"e","\u1e17":"e","\u0115":"e","\u0117":"e","\xeb":"e","\u1ebb":"e","\u011b":"e","\u0205":"e","\u0207":"e","\u1eb9":"e","\u1ec7":"e","\u0229":"e","\u1e1d":"e","\u0119":"e","\u1e19":"e","\u1e1b":"e","\u0247":"e","\u025b":"e","\u01dd":"e","\u24d5":"f","\uff46":"f","\u1e1f":"f","\u0192":"f","\ua77c":"f","\u24d6":"g","\uff47":"g","\u01f5":"g","\u011d":"g","\u1e21":"g","\u011f":"g","\u0121":"g","\u01e7":"g","\u0123":"g","\u01e5":"g","\u0260":"g","\ua7a1":"g","\u1d79":"g","\ua77f":"g","\u24d7":"h","\uff48":"h","\u0125":"h","\u1e23":"h","\u1e27":"h","\u021f":"h","\u1e25":"h","\u1e29":"h","\u1e2b":"h","\u1e96":"h","\u0127":"h","\u2c68":"h","\u2c76":"h","\u0265":"h","\u0195":"hv","\u24d8":"i","\uff49":"i","\xec":"i","\xed":"i","\xee":"i","\u0129":"i","\u012b":"i","\u012d":"i","\xef":"i","\u1e2f":"i","\u1ec9":"i","\u01d0":"i","\u0209":"i","\u020b":"i","\u1ecb":"i","\u012f":"i","\u1e2d":"i","\u0268":"i","\u0131":"i","\u24d9":"j","\uff4a":"j","\u0135":"j","\u01f0":"j","\u0249":"j","\u24da":"k","\uff4b":"k","\u1e31":"k","\u01e9":"k","\u1e33":"k","\u0137":"k","\u1e35":"k","\u0199":"k","\u2c6a":"k","\ua741":"k","\ua743":"k","\ua745":"k","\ua7a3":"k","\u24db":"l","\uff4c":"l","\u0140":"l","\u013a":"l","\u013e":"l","\u1e37":"l","\u1e39":"l","\u013c":"l","\u1e3d":"l","\u1e3b":"l","\u017f":"l","\u0142":"l","\u019a":"l","\u026b":"l","\u2c61":"l","\ua749":"l","\ua781":"l","\ua747":"l","\u01c9":"lj","\u24dc":"m","\uff4d":"m","\u1e3f":"m","\u1e41":"m","\u1e43":"m","\u0271":"m","\u026f":"m","\u24dd":"n","\uff4e":"n","\u01f9":"n","\u0144":"n","\xf1":"n","\u1e45":"n","\u0148":"n","\u1e47":"n","\u0146":"n","\u1e4b":"n","\u1e49":"n","\u019e":"n","\u0272":"n","\u0149":"n","\ua791":"n","\ua7a5":"n","\u01cc":"nj","\u24de":"o","\uff4f":"o","\xf2":"o","\xf3":"o","\xf4":"o","\u1ed3":"o","\u1ed1":"o","\u1ed7":"o","\u1ed5":"o","\xf5":"o","\u1e4d":"o","\u022d":"o","\u1e4f":"o","\u014d":"o","\u1e51":"o","\u1e53":"o","\u014f":"o","\u022f":"o","\u0231":"o","\xf6":"o","\u022b":"o","\u1ecf":"o","\u0151":"o","\u01d2":"o","\u020d":"o","\u020f":"o","\u01a1":"o","\u1edd":"o","\u1edb":"o","\u1ee1":"o","\u1edf":"o","\u1ee3":"o","\u1ecd":"o","\u1ed9":"o","\u01eb":"o","\u01ed":"o","\xf8":"o","\u01ff":"o","\u0254":"o","\ua74b":"o","\ua74d":"o","\u0275":"o","\u01a3":"oi","\u0223":"ou","\ua74f":"oo","\u24df":"p","\uff50":"p","\u1e55":"p","\u1e57":"p","\u01a5":"p","\u1d7d":"p","\ua751":"p","\ua753":"p","\ua755":"p","\u24e0":"q","\uff51":"q","\u024b":"q","\ua757":"q","\ua759":"q","\u24e1":"r","\uff52":"r","\u0155":"r","\u1e59":"r","\u0159":"r","\u0211":"r","\u0213":"r","\u1e5b":"r","\u1e5d":"r","\u0157":"r","\u1e5f":"r","\u024d":"r","\u027d":"r","\ua75b":"r","\ua7a7":"r","\ua783":"r","\u24e2":"s","\uff53":"s","\xdf":"s","\u015b":"s","\u1e65":"s","\u015d":"s","\u1e61":"s","\u0161":"s","\u1e67":"s","\u1e63":"s","\u1e69":"s","\u0219":"s","\u015f":"s","\u023f":"s","\ua7a9":"s","\ua785":"s","\u1e9b":"s","\u24e3":"t","\uff54":"t","\u1e6b":"t","\u1e97":"t","\u0165":"t","\u1e6d":"t","\u021b":"t","\u0163":"t","\u1e71":"t","\u1e6f":"t","\u0167":"t","\u01ad":"t","\u0288":"t","\u2c66":"t","\ua787":"t","\ua729":"tz","\u24e4":"u","\uff55":"u","\xf9":"u","\xfa":"u","\xfb":"u","\u0169":"u","\u1e79":"u","\u016b":"u","\u1e7b":"u","\u016d":"u","\xfc":"u","\u01dc":"u","\u01d8":"u","\u01d6":"u","\u01da":"u","\u1ee7":"u","\u016f":"u","\u0171":"u","\u01d4":"u","\u0215":"u","\u0217":"u","\u01b0":"u","\u1eeb":"u","\u1ee9":"u","\u1eef":"u","\u1eed":"u","\u1ef1":"u","\u1ee5":"u","\u1e73":"u","\u0173":"u","\u1e77":"u","\u1e75":"u","\u0289":"u","\u24e5":"v","\uff56":"v","\u1e7d":"v","\u1e7f":"v","\u028b":"v","\ua75f":"v","\u028c":"v","\ua761":"vy","\u24e6":"w","\uff57":"w","\u1e81":"w","\u1e83":"w","\u0175":"w","\u1e87":"w","\u1e85":"w","\u1e98":"w","\u1e89":"w","\u2c73":"w","\u24e7":"x","\uff58":"x","\u1e8b":"x","\u1e8d":"x","\u24e8":"y","\uff59":"y","\u1ef3":"y","\xfd":"y","\u0177":"y","\u1ef9":"y","\u0233":"y","\u1e8f":"y","\xff":"y","\u1ef7":"y","\u1e99":"y","\u1ef5":"y","\u01b4":"y","\u024f":"y","\u1eff":"y","\u24e9":"z","\uff5a":"z","\u017a":"z","\u1e91":"z","\u017c":"z","\u017e":"z","\u1e93":"z","\u1e95":"z","\u01b6":"z","\u0225":"z","\u0240":"z","\u2c6c":"z","\ua763":"z","\u0386":"\u0391","\u0388":"\u0395","\u0389":"\u0397","\u038a":"\u0399","\u03aa":"\u0399","\u038c":"\u039f","\u038e":"\u03a5","\u03ab":"\u03a5","\u038f":"\u03a9","\u03ac":"\u03b1","\u03ad":"\u03b5","\u03ae":"\u03b7","\u03af":"\u03b9","\u03ca":"\u03b9","\u0390":"\u03b9","\u03cc":"\u03bf","\u03cd":"\u03c5","\u03cb":"\u03c5","\u03b0":"\u03c5","\u03c9":"\u03c9","\u03c2":"\u03c3"};j=a(document),g=function(){var a=1;return function(){return a++}}(),d=O(Object,{bind:function(a){var b=this;return function(){a.apply(b,arguments)}},init:function(c){var d,e,f=".select2-results";this.opts=c=this.prepareOpts(c),this.id=c.id,c.element.data("select2")!==b&&null!==c.element.data("select2")&&c.element.data("select2").destroy(),this.container=this.createContainer(),this.liveRegion=a("<span>",{role:"status","aria-live":"polite"}).addClass("select2-hidden-accessible").appendTo(document.body),this.containerId="s2id_"+(c.element.attr("id")||"autogen"+g()),this.containerEventName=this.containerId.replace(/([.])/g,"_").replace(/([;&,\-\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g,"\\$1"),this.container.attr("id",this.containerId),this.container.attr("title",c.element.attr("title")),this.body=a("body"),D(this.container,this.opts.element,this.opts.adaptContainerCssClass),this.container.attr("style",c.element.attr("style")),this.container.css(K(c.containerCss,this.opts.element)),this.container.addClass(K(c.containerCssClass,this.opts.element)),this.elementTabIndex=this.opts.element.attr("tabindex"),this.opts.element.data("select2",this).attr("tabindex","-1").before(this.container).on("click.select2",A),this.container.data("select2",this),this.dropdown=this.container.find(".select2-drop"),D(this.dropdown,this.opts.element,this.opts.adaptDropdownCssClass),this.dropdown.addClass(K(c.dropdownCssClass,this.opts.element)),this.dropdown.data("select2",this),this.dropdown.on("click",A),this.results=d=this.container.find(f),this.search=e=this.container.find("input.select2-input"),this.queryCount=0,this.resultsPage=0,this.context=null,this.initContainer(),this.container.on("click",A),v(this.results),this.dropdown.on("mousemove-filtered",f,this.bind(this.highlightUnderEvent)),this.dropdown.on("touchstart touchmove touchend",f,this.bind(function(a){this._touchEvent=!0,this.highlightUnderEvent(a)})),this.dropdown.on("touchmove",f,this.bind(this.touchMoved)),this.dropdown.on("touchstart touchend",f,this.bind(this.clearTouchMoved)),this.dropdown.on("click",this.bind(function(){this._touchEvent&&(this._touchEvent=!1,this.selectHighlighted())})),x(80,this.results),this.dropdown.on("scroll-debounced",f,this.bind(this.loadMoreIfNeeded)),a(this.container).on("change",".select2-input",function(a){a.stopPropagation()}),a(this.dropdown).on("change",".select2-input",function(a){a.stopPropagation()}),a.fn.mousewheel&&d.mousewheel(function(a,b,c,e){var f=d.scrollTop();e>0&&0>=f-e?(d.scrollTop(0),A(a)):0>e&&d.get(0).scrollHeight-d.scrollTop()+e<=d.height()&&(d.scrollTop(d.get(0).scrollHeight-d.height()),A(a))}),u(e),e.on("keyup-change input paste",this.bind(this.updateResults)),e.on("focus",function(){e.addClass("select2-focused")}),e.on("blur",function(){e.removeClass("select2-focused")}),this.dropdown.on("mouseup",f,this.bind(function(b){a(b.target).closest(".select2-result-selectable").length>0&&(this.highlightUnderEvent(b),this.selectHighlighted(b))})),this.dropdown.on("click mouseup mousedown touchstart touchend focusin",function(a){a.stopPropagation()}),this.nextSearchTerm=b,a.isFunction(this.opts.initSelection)&&(this.initSelection(),this.monitorSource()),null!==c.maximumInputLength&&this.search.attr("maxlength",c.maximumInputLength);var h=c.element.prop("disabled");h===b&&(h=!1),this.enable(!h);var i=c.element.prop("readonly");i===b&&(i=!1),this.readonly(i),k=k||q(),this.autofocus=c.element.prop("autofocus"),c.element.prop("autofocus",!1),this.autofocus&&this.focus(),this.search.attr("placeholder",c.searchInputPlaceholder)},destroy:function(){var a=this.opts.element,c=a.data("select2");this.close(),a.length&&a[0].detachEvent&&a.each(function(){this.detachEvent("onpropertychange",this._sync)}),this.propertyObserver&&(this.propertyObserver.disconnect(),this.propertyObserver=null),this._sync=null,c!==b&&(c.container.remove(),c.liveRegion.remove(),c.dropdown.remove(),a.removeClass("select2-offscreen").removeData("select2").off(".select2").prop("autofocus",this.autofocus||!1),this.elementTabIndex?a.attr({tabindex:this.elementTabIndex}):a.removeAttr("tabindex"),a.show()),N.call(this,"container","liveRegion","dropdown","results","search")},optionToData:function(a){return a.is("option")?{id:a.prop("value"),text:a.text(),element:a.get(),css:a.attr("class"),disabled:a.prop("disabled"),locked:r(a.attr("locked"),"locked")||r(a.data("locked"),!0)}:a.is("optgroup")?{text:a.attr("label"),children:[],element:a.get(),css:a.attr("class")}:void 0},prepareOpts:function(c){var d,e,f,h,i=this;if(d=c.element,"select"===d.get(0).tagName.toLowerCase()&&(this.select=e=c.element),e&&a.each(["id","multiple","ajax","query","createSearchChoice","initSelection","data","tags"],function(){if(this in c)throw new Error("Option '"+this+"' is not allowed for Select2 when attached to a <select> element.")}),c=a.extend({},{populateResults:function(d,e,f){var h,j=this.opts.id,k=this.liveRegion;h=function(d,e,l){var m,n,o,p,q,r,s,t,u,v;d=c.sortResults(d,e,f);var w=[];for(m=0,n=d.length;n>m;m+=1)o=d[m],q=o.disabled===!0,p=!q&&j(o)!==b,r=o.children&&o.children.length>0,s=a("<li></li>"),s.addClass("select2-results-dept-"+l),s.addClass("select2-result"),s.addClass(p?"select2-result-selectable":"select2-result-unselectable"),q&&s.addClass("select2-disabled"),r&&s.addClass("select2-result-with-children"),s.addClass(i.opts.formatResultCssClass(o)),s.attr("role","presentation"),t=a(document.createElement("div")),t.addClass("select2-result-label"),t.attr("id","select2-result-label-"+g()),t.attr("role","option"),v=c.formatResult(o,t,f,i.opts.escapeMarkup),v!==b&&(t.html(v),s.append(t)),r&&(u=a("<ul></ul>"),u.addClass("select2-result-sub"),h(o.children,u,l+1),s.append(u)),s.data("select2-data",o),w.push(s[0]);e.append(w),k.text(c.formatMatches(d.length))},h(e,d,0)}},a.fn.select2.defaults,c),"function"!=typeof c.id&&(f=c.id,c.id=function(a){return a[f]}),a.isArray(c.element.data("select2Tags"))){if("tags"in c)throw"tags specified as both an attribute 'data-select2-tags' and in options of Select2 "+c.element.attr("id");c.tags=c.element.data("select2Tags")}if(e?(c.query=this.bind(function(a){var f,g,h,c={results:[],more:!1},e=a.term;h=function(b,c){var d;b.is("option")?a.matcher(e,b.text(),b)&&c.push(i.optionToData(b)):b.is("optgroup")&&(d=i.optionToData(b),b.children().each2(function(a,b){h(b,d.children)}),d.children.length>0&&c.push(d))},f=d.children(),this.getPlaceholder()!==b&&f.length>0&&(g=this.getPlaceholderOption(),g&&(f=f.not(g))),f.each2(function(a,b){h(b,c.results)}),a.callback(c)}),c.id=function(a){return a.id}):"query"in c||("ajax"in c?(h=c.element.data("ajax-url"),h&&h.length>0&&(c.ajax.url=h),c.query=G.call(c.element,c.ajax)):"data"in c?c.query=H(c.data):"tags"in c&&(c.query=I(c.tags),c.createSearchChoice===b&&(c.createSearchChoice=function(b){return{id:a.trim(b),text:a.trim(b)}}),c.initSelection===b&&(c.initSelection=function(b,d){var e=[];a(s(b.val(),c.separator)).each(function(){var b={id:this,text:this},d=c.tags;a.isFunction(d)&&(d=d()),a(d).each(function(){return r(this.id,b.id)?(b=this,!1):void 0}),e.push(b)}),d(e)}))),"function"!=typeof c.query)throw"query function not defined for Select2 "+c.element.attr("id");if("top"===c.createSearchChoicePosition)c.createSearchChoicePosition=function(a,b){a.unshift(b)};else if("bottom"===c.createSearchChoicePosition)c.createSearchChoicePosition=function(a,b){a.push(b)};else if("function"!=typeof c.createSearchChoicePosition)throw"invalid createSearchChoicePosition option must be 'top', 'bottom' or a custom function";return c},monitorSource:function(){var d,c=this.opts.element,e=this;c.on("change.select2",this.bind(function(){this.opts.element.data("select2-change-triggered")!==!0&&this.initSelection()})),this._sync=this.bind(function(){var a=c.prop("disabled");a===b&&(a=!1),this.enable(!a);var d=c.prop("readonly");d===b&&(d=!1),this.readonly(d),D(this.container,this.opts.element,this.opts.adaptContainerCssClass),this.container.addClass(K(this.opts.containerCssClass,this.opts.element)),D(this.dropdown,this.opts.element,this.opts.adaptDropdownCssClass),this.dropdown.addClass(K(this.opts.dropdownCssClass,this.opts.element))}),c.length&&c[0].attachEvent&&c.each(function(){this.attachEvent("onpropertychange",e._sync)}),d=window.MutationObserver||window.WebKitMutationObserver||window.MozMutationObserver,d!==b&&(this.propertyObserver&&(delete this.propertyObserver,this.propertyObserver=null),this.propertyObserver=new d(function(b){a.each(b,e._sync)}),this.propertyObserver.observe(c.get(0),{attributes:!0,subtree:!1}))},triggerSelect:function(b){var c=a.Event("select2-selecting",{val:this.id(b),object:b,choice:b});return this.opts.element.trigger(c),!c.isDefaultPrevented()},triggerChange:function(b){b=b||{},b=a.extend({},b,{type:"change",val:this.val()}),this.opts.element.data("select2-change-triggered",!0),this.opts.element.trigger(b),this.opts.element.data("select2-change-triggered",!1),this.opts.element.click(),this.opts.blurOnChange&&this.opts.element.blur()},isInterfaceEnabled:function(){return this.enabledInterface===!0},enableInterface:function(){var a=this._enabled&&!this._readonly,b=!a;return a===this.enabledInterface?!1:(this.container.toggleClass("select2-container-disabled",b),this.close(),this.enabledInterface=a,!0)},enable:function(a){a===b&&(a=!0),this._enabled!==a&&(this._enabled=a,this.opts.element.prop("disabled",!a),this.enableInterface())},disable:function(){this.enable(!1)},readonly:function(a){a===b&&(a=!1),this._readonly!==a&&(this._readonly=a,this.opts.element.prop("readonly",a),this.enableInterface())},opened:function(){return this.container?this.container.hasClass("select2-dropdown-open"):!1},positionDropdown:function(){var t,u,v,w,x,b=this.dropdown,c=this.container.offset(),d=this.container.outerHeight(!1),e=this.container.outerWidth(!1),f=b.outerHeight(!1),g=a(window),h=g.width(),i=g.height(),j=g.scrollLeft()+h,l=g.scrollTop()+i,m=c.top+d,n=c.left,o=l>=m+f,p=c.top-f>=g.scrollTop(),q=b.outerWidth(!1),r=j>=n+q,s=b.hasClass("select2-drop-above");s?(u=!0,!p&&o&&(v=!0,u=!1)):(u=!1,!o&&p&&(v=!0,u=!0)),v&&(b.hide(),c=this.container.offset(),d=this.container.outerHeight(!1),e=this.container.outerWidth(!1),f=b.outerHeight(!1),j=g.scrollLeft()+h,l=g.scrollTop()+i,m=c.top+d,n=c.left,q=b.outerWidth(!1),r=j>=n+q,b.show(),this.focusSearch()),this.opts.dropdownAutoWidth?(x=a(".select2-results",b)[0],b.addClass("select2-drop-auto-width"),b.css("width",""),q=b.outerWidth(!1)+(x.scrollHeight===x.clientHeight?0:k.width),q>e?e=q:q=e,f=b.outerHeight(!1),r=j>=n+q):this.container.removeClass("select2-drop-auto-width"),"static"!==this.body.css("position")&&(t=this.body.offset(),m-=t.top,n-=t.left),r||(n=c.left+this.container.outerWidth(!1)-q),w={left:n,width:e},u?(w.top=c.top-f,w.bottom="auto",this.container.addClass("select2-drop-above"),b.addClass("select2-drop-above")):(w.top=m,w.bottom="auto",this.container.removeClass("select2-drop-above"),b.removeClass("select2-drop-above")),w=a.extend(w,K(this.opts.dropdownCss,this.opts.element)),b.css(w)},shouldOpen:function(){var b;return this.opened()?!1:this._enabled===!1||this._readonly===!0?!1:(b=a.Event("select2-opening"),this.opts.element.trigger(b),!b.isDefaultPrevented())},clearDropdownAlignmentPreference:function(){this.container.removeClass("select2-drop-above"),this.dropdown.removeClass("select2-drop-above")},open:function(){return this.shouldOpen()?(this.opening(),j.on("mousemove.select2Event",function(a){i.x=a.pageX,i.y=a.pageY}),!0):!1},opening:function(){var f,b=this.containerEventName,c="scroll."+b,d="resize."+b,e="orientationchange."+b;this.container.addClass("select2-dropdown-open").addClass("select2-container-active"),this.clearDropdownAlignmentPreference(),this.dropdown[0]!==this.body.children().last()[0]&&this.dropdown.detach().appendTo(this.body),f=a("#select2-drop-mask"),0==f.length&&(f=a(document.createElement("div")),f.attr("id","select2-drop-mask").attr("class","select2-drop-mask"),f.hide(),f.appendTo(this.body),f.on("mousedown touchstart click",function(b){n(f);var d,c=a("#select2-drop");c.length>0&&(d=c.data("select2"),d.opts.selectOnBlur&&d.selectHighlighted({noFocus:!0}),d.close(),b.preventDefault(),b.stopPropagation())})),this.dropdown.prev()[0]!==f[0]&&this.dropdown.before(f),a("#select2-drop").removeAttr("id"),this.dropdown.attr("id","select2-drop"),f.show(),this.positionDropdown(),this.dropdown.show(),this.positionDropdown(),this.dropdown.addClass("select2-drop-active");var g=this;this.container.parents().add(window).each(function(){a(this).on(d+" "+c+" "+e,function(){g.opened()&&g.positionDropdown()})})},close:function(){if(this.opened()){var b=this.containerEventName,c="scroll."+b,d="resize."+b,e="orientationchange."+b;this.container.parents().add(window).each(function(){a(this).off(c).off(d).off(e)}),this.clearDropdownAlignmentPreference(),a("#select2-drop-mask").hide(),this.dropdown.removeAttr("id"),this.dropdown.hide(),this.container.removeClass("select2-dropdown-open").removeClass("select2-container-active"),this.results.empty(),j.off("mousemove.select2Event"),this.clearSearch(),this.search.removeClass("select2-active"),this.opts.element.trigger(a.Event("select2-close"))}},externalSearch:function(a){this.open(),this.search.val(a),this.updateResults(!1)},clearSearch:function(){},getMaximumSelectionSize:function(){return K(this.opts.maximumSelectionSize,this.opts.element)},ensureHighlightVisible:function(){var c,d,e,f,g,h,i,j,b=this.results;if(d=this.highlight(),!(0>d)){if(0==d)return b.scrollTop(0),void 0;c=this.findHighlightableChoices().find(".select2-result-label"),e=a(c[d]),j=(e.offset()||{}).top||0,f=j+e.outerHeight(!0),d===c.length-1&&(i=b.find("li.select2-more-results"),i.length>0&&(f=i.offset().top+i.outerHeight(!0))),g=b.offset().top+b.outerHeight(!0),f>g&&b.scrollTop(b.scrollTop()+(f-g)),h=j-b.offset().top,0>h&&"none"!=e.css("display")&&b.scrollTop(b.scrollTop()+h)}},findHighlightableChoices:function(){return this.results.find(".select2-result-selectable:not(.select2-disabled):not(.select2-selected)")},moveHighlight:function(b){for(var c=this.findHighlightableChoices(),d=this.highlight();d>-1&&d<c.length;){d+=b;var e=a(c[d]);if(e.hasClass("select2-result-selectable")&&!e.hasClass("select2-disabled")&&!e.hasClass("select2-selected")){this.highlight(d);break}}},highlight:function(b){var d,e,c=this.findHighlightableChoices(); +return 0===arguments.length?p(c.filter(".select2-highlighted")[0],c.get()):(b>=c.length&&(b=c.length-1),0>b&&(b=0),this.removeHighlight(),d=a(c[b]),d.addClass("select2-highlighted"),this.search.attr("aria-activedescendant",d.find(".select2-result-label").attr("id")),this.ensureHighlightVisible(),this.liveRegion.text(d.text()),e=d.data("select2-data"),e&&this.opts.element.trigger({type:"select2-highlight",val:this.id(e),choice:e}),void 0)},removeHighlight:function(){this.results.find(".select2-highlighted").removeClass("select2-highlighted")},touchMoved:function(){this._touchMoved=!0},clearTouchMoved:function(){this._touchMoved=!1},countSelectableResults:function(){return this.findHighlightableChoices().length},highlightUnderEvent:function(b){var c=a(b.target).closest(".select2-result-selectable");if(c.length>0&&!c.is(".select2-highlighted")){var d=this.findHighlightableChoices();this.highlight(d.index(c))}else 0==c.length&&this.removeHighlight()},loadMoreIfNeeded:function(){var c,a=this.results,b=a.find("li.select2-more-results"),d=this.resultsPage+1,e=this,f=this.search.val(),g=this.context;0!==b.length&&(c=b.offset().top-a.offset().top-a.height(),c<=this.opts.loadMorePadding&&(b.addClass("select2-active"),this.opts.query({element:this.opts.element,term:f,page:d,context:g,matcher:this.opts.matcher,callback:this.bind(function(c){e.opened()&&(e.opts.populateResults.call(this,a,c.results,{term:f,page:d,context:g}),e.postprocessResults(c,!1,!1),c.more===!0?(b.detach().appendTo(a).text(K(e.opts.formatLoadMore,e.opts.element,d+1)),window.setTimeout(function(){e.loadMoreIfNeeded()},10)):b.remove(),e.positionDropdown(),e.resultsPage=d,e.context=c.context,this.opts.element.trigger({type:"select2-loaded",items:c}))})})))},tokenize:function(){},updateResults:function(c){function m(){d.removeClass("select2-active"),h.positionDropdown(),e.find(".select2-no-results,.select2-selection-limit,.select2-searching").length?h.liveRegion.text(e.text()):h.liveRegion.text(h.opts.formatMatches(e.find(".select2-result-selectable").length))}function n(a){e.html(a),m()}var g,i,l,d=this.search,e=this.results,f=this.opts,h=this,j=d.val(),k=a.data(this.container,"select2-last-term");if((c===!0||!k||!r(j,k))&&(a.data(this.container,"select2-last-term",j),c===!0||this.showSearchInput!==!1&&this.opened())){l=++this.queryCount;var o=this.getMaximumSelectionSize();if(o>=1&&(g=this.data(),a.isArray(g)&&g.length>=o&&J(f.formatSelectionTooBig,"formatSelectionTooBig")))return n("<li class='select2-selection-limit'>"+K(f.formatSelectionTooBig,f.element,o)+"</li>"),void 0;if(d.val().length<f.minimumInputLength)return J(f.formatInputTooShort,"formatInputTooShort")?n("<li class='select2-no-results'>"+K(f.formatInputTooShort,f.element,d.val(),f.minimumInputLength)+"</li>"):n(""),c&&this.showSearch&&this.showSearch(!0),void 0;if(f.maximumInputLength&&d.val().length>f.maximumInputLength)return J(f.formatInputTooLong,"formatInputTooLong")?n("<li class='select2-no-results'>"+K(f.formatInputTooLong,f.element,d.val(),f.maximumInputLength)+"</li>"):n(""),void 0;f.formatSearching&&0===this.findHighlightableChoices().length&&n("<li class='select2-searching'>"+K(f.formatSearching,f.element)+"</li>"),d.addClass("select2-active"),this.removeHighlight(),i=this.tokenize(),i!=b&&null!=i&&d.val(i),this.resultsPage=1,f.query({element:f.element,term:d.val(),page:this.resultsPage,context:null,matcher:f.matcher,callback:this.bind(function(g){var i;if(l==this.queryCount){if(!this.opened())return this.search.removeClass("select2-active"),void 0;if(this.context=g.context===b?null:g.context,this.opts.createSearchChoice&&""!==d.val()&&(i=this.opts.createSearchChoice.call(h,d.val(),g.results),i!==b&&null!==i&&h.id(i)!==b&&null!==h.id(i)&&0===a(g.results).filter(function(){return r(h.id(this),h.id(i))}).length&&this.opts.createSearchChoicePosition(g.results,i)),0===g.results.length&&J(f.formatNoMatches,"formatNoMatches"))return n("<li class='select2-no-results'>"+K(f.formatNoMatches,f.element,d.val())+"</li>"),void 0;e.empty(),h.opts.populateResults.call(this,e,g.results,{term:d.val(),page:this.resultsPage,context:null}),g.more===!0&&J(f.formatLoadMore,"formatLoadMore")&&(e.append("<li class='select2-more-results'>"+f.escapeMarkup(K(f.formatLoadMore,f.element,this.resultsPage))+"</li>"),window.setTimeout(function(){h.loadMoreIfNeeded()},10)),this.postprocessResults(g,c),m(),this.opts.element.trigger({type:"select2-loaded",items:g})}})})}},cancel:function(){this.close()},blur:function(){this.opts.selectOnBlur&&this.selectHighlighted({noFocus:!0}),this.close(),this.container.removeClass("select2-container-active"),this.search[0]===document.activeElement&&this.search.blur(),this.clearSearch(),this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus")},focusSearch:function(){y(this.search)},selectHighlighted:function(a){if(this._touchMoved)return this.clearTouchMoved(),void 0;var b=this.highlight(),c=this.results.find(".select2-highlighted"),d=c.closest(".select2-result").data("select2-data");d?(this.highlight(b),this.onSelect(d,a)):a&&a.noFocus&&this.close()},getPlaceholder:function(){var a;return this.opts.element.attr("placeholder")||this.opts.element.attr("data-placeholder")||this.opts.element.data("placeholder")||this.opts.placeholder||((a=this.getPlaceholderOption())!==b?a.text():b)},getPlaceholderOption:function(){if(this.select){var c=this.select.children("option").first();if(this.opts.placeholderOption!==b)return"first"===this.opts.placeholderOption&&c||"function"==typeof this.opts.placeholderOption&&this.opts.placeholderOption(this.select);if(""===a.trim(c.text())&&""===c.val())return c}},initContainerWidth:function(){function c(){var c,d,e,f,g,h;if("off"===this.opts.width)return null;if("element"===this.opts.width)return 0===this.opts.element.outerWidth(!1)?"auto":this.opts.element.outerWidth(!1)+"px";if("copy"===this.opts.width||"resolve"===this.opts.width){if(c=this.opts.element.attr("style"),c!==b)for(d=c.split(";"),f=0,g=d.length;g>f;f+=1)if(h=d[f].replace(/\s/g,""),e=h.match(/^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i),null!==e&&e.length>=1)return e[1];return"resolve"===this.opts.width?(c=this.opts.element.css("width"),c.indexOf("%")>0?c:0===this.opts.element.outerWidth(!1)?"auto":this.opts.element.outerWidth(!1)+"px"):null}return a.isFunction(this.opts.width)?this.opts.width():this.opts.width}var d=c.call(this);null!==d&&this.container.css("width",d)}}),e=O(d,{createContainer:function(){var b=a(document.createElement("div")).attr({"class":"select2-container"}).html(["<a href='javascript:void(0)' class='select2-choice' tabindex='-1'>"," <span class='select2-chosen'> </span><abbr class='select2-search-choice-close'></abbr>"," <span class='select2-arrow' role='presentation'><b role='presentation'></b></span>","</a>","<label for='' class='select2-offscreen'></label>","<input class='select2-focusser select2-offscreen' type='text' aria-haspopup='true' role='button' />","<div class='select2-drop select2-display-none'>"," <div class='select2-search'>"," <label for='' class='select2-offscreen'></label>"," <input type='text' autocomplete='off' autocorrect='off' autocapitalize='off' spellcheck='false' class='select2-input' role='combobox' aria-expanded='true'"," aria-autocomplete='list' />"," </div>"," <ul class='select2-results' role='listbox'>"," </ul>","</div>"].join(""));return b},enableInterface:function(){this.parent.enableInterface.apply(this,arguments)&&this.focusser.prop("disabled",!this.isInterfaceEnabled())},opening:function(){var c,d,e;this.opts.minimumResultsForSearch>=0&&this.showSearch(!0),this.parent.opening.apply(this,arguments),this.showSearchInput!==!1&&this.search.val(this.focusser.val()),this.opts.shouldFocusInput(this)&&(this.search.focus(),c=this.search.get(0),c.createTextRange?(d=c.createTextRange(),d.collapse(!1),d.select()):c.setSelectionRange&&(e=this.search.val().length,c.setSelectionRange(e,e))),""===this.search.val()&&this.nextSearchTerm!=b&&(this.search.val(this.nextSearchTerm),this.search.select()),this.focusser.prop("disabled",!0).val(""),this.updateResults(!0),this.opts.element.trigger(a.Event("select2-open"))},close:function(){this.opened()&&(this.parent.close.apply(this,arguments),this.focusser.prop("disabled",!1),this.opts.shouldFocusInput(this)&&this.focusser.focus())},focus:function(){this.opened()?this.close():(this.focusser.prop("disabled",!1),this.opts.shouldFocusInput(this)&&this.focusser.focus())},isFocused:function(){return this.container.hasClass("select2-container-active")},cancel:function(){this.parent.cancel.apply(this,arguments),this.focusser.prop("disabled",!1),this.opts.shouldFocusInput(this)&&this.focusser.focus()},destroy:function(){a("label[for='"+this.focusser.attr("id")+"']").attr("for",this.opts.element.attr("id")),this.parent.destroy.apply(this,arguments),N.call(this,"selection","focusser")},initContainer:function(){var b,h,d=this.container,e=this.dropdown,f=g();this.opts.minimumResultsForSearch<0?this.showSearch(!1):this.showSearch(!0),this.selection=b=d.find(".select2-choice"),this.focusser=d.find(".select2-focusser"),b.find(".select2-chosen").attr("id","select2-chosen-"+f),this.focusser.attr("aria-labelledby","select2-chosen-"+f),this.results.attr("id","select2-results-"+f),this.search.attr("aria-owns","select2-results-"+f),this.focusser.attr("id","s2id_autogen"+f),h=a("label[for='"+this.opts.element.attr("id")+"']"),this.focusser.prev().text(h.text()).attr("for",this.focusser.attr("id"));var i=this.opts.element.attr("title");this.opts.element.attr("title",i||h.text()),this.focusser.attr("tabindex",this.elementTabIndex),this.search.attr("id",this.focusser.attr("id")+"_search"),this.search.prev().text(a("label[for='"+this.focusser.attr("id")+"']").text()).attr("for",this.search.attr("id")),this.search.on("keydown",this.bind(function(a){if(this.isInterfaceEnabled()){if(a.which===c.PAGE_UP||a.which===c.PAGE_DOWN)return A(a),void 0;switch(a.which){case c.UP:case c.DOWN:return this.moveHighlight(a.which===c.UP?-1:1),A(a),void 0;case c.ENTER:return this.selectHighlighted(),A(a),void 0;case c.TAB:return this.selectHighlighted({noFocus:!0}),void 0;case c.ESC:return this.cancel(a),A(a),void 0}}})),this.search.on("blur",this.bind(function(){document.activeElement===this.body.get(0)&&window.setTimeout(this.bind(function(){this.opened()&&this.search.focus()}),0)})),this.focusser.on("keydown",this.bind(function(a){if(this.isInterfaceEnabled()&&a.which!==c.TAB&&!c.isControl(a)&&!c.isFunctionKey(a)&&a.which!==c.ESC){if(this.opts.openOnEnter===!1&&a.which===c.ENTER)return A(a),void 0;if(a.which==c.DOWN||a.which==c.UP||a.which==c.ENTER&&this.opts.openOnEnter){if(a.altKey||a.ctrlKey||a.shiftKey||a.metaKey)return;return this.open(),A(a),void 0}return a.which==c.DELETE||a.which==c.BACKSPACE?(this.opts.allowClear&&this.clear(),A(a),void 0):void 0}})),u(this.focusser),this.focusser.on("keyup-change input",this.bind(function(a){if(this.opts.minimumResultsForSearch>=0){if(a.stopPropagation(),this.opened())return;this.open()}})),b.on("mousedown touchstart","abbr",this.bind(function(a){this.isInterfaceEnabled()&&(this.clear(),B(a),this.close(),this.selection.focus())})),b.on("mousedown touchstart",this.bind(function(c){n(b),this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.opened()?this.close():this.isInterfaceEnabled()&&this.open(),A(c)})),e.on("mousedown touchstart",this.bind(function(){this.opts.shouldFocusInput(this)&&this.search.focus()})),b.on("focus",this.bind(function(a){A(a)})),this.focusser.on("focus",this.bind(function(){this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.container.addClass("select2-container-active")})).on("blur",this.bind(function(){this.opened()||(this.container.removeClass("select2-container-active"),this.opts.element.trigger(a.Event("select2-blur")))})),this.search.on("focus",this.bind(function(){this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.container.addClass("select2-container-active")})),this.initContainerWidth(),this.opts.element.addClass("select2-offscreen"),this.setPlaceholder()},clear:function(b){var c=this.selection.data("select2-data");if(c){var d=a.Event("select2-clearing");if(this.opts.element.trigger(d),d.isDefaultPrevented())return;var e=this.getPlaceholderOption();this.opts.element.val(e?e.val():""),this.selection.find(".select2-chosen").empty(),this.selection.removeData("select2-data"),this.setPlaceholder(),b!==!1&&(this.opts.element.trigger({type:"select2-removed",val:this.id(c),choice:c}),this.triggerChange({removed:c}))}},initSelection:function(){if(this.isPlaceholderOptionSelected())this.updateSelection(null),this.close(),this.setPlaceholder();else{var c=this;this.opts.initSelection.call(null,this.opts.element,function(a){a!==b&&null!==a&&(c.updateSelection(a),c.close(),c.setPlaceholder(),c.nextSearchTerm=c.opts.nextSearchTerm(a,c.search.val()))})}},isPlaceholderOptionSelected:function(){var a;return this.getPlaceholder()===b?!1:(a=this.getPlaceholderOption())!==b&&a.prop("selected")||""===this.opts.element.val()||this.opts.element.val()===b||null===this.opts.element.val()},prepareOpts:function(){var b=this.parent.prepareOpts.apply(this,arguments),c=this;return"select"===b.element.get(0).tagName.toLowerCase()?b.initSelection=function(a,b){var d=a.find("option").filter(function(){return this.selected&&!this.disabled});b(c.optionToData(d))}:"data"in b&&(b.initSelection=b.initSelection||function(c,d){var e=c.val(),f=null;b.query({matcher:function(a,c,d){var g=r(e,b.id(d));return g&&(f=d),g},callback:a.isFunction(d)?function(){d(f)}:a.noop})}),b},getPlaceholder:function(){return this.select&&this.getPlaceholderOption()===b?b:this.parent.getPlaceholder.apply(this,arguments)},setPlaceholder:function(){var a=this.getPlaceholder();if(this.isPlaceholderOptionSelected()&&a!==b){if(this.select&&this.getPlaceholderOption()===b)return;this.selection.find(".select2-chosen").html(this.opts.escapeMarkup(a)),this.selection.addClass("select2-default"),this.container.removeClass("select2-allowclear")}},postprocessResults:function(a,b,c){var d=0,e=this;if(this.findHighlightableChoices().each2(function(a,b){return r(e.id(b.data("select2-data")),e.opts.element.val())?(d=a,!1):void 0}),c!==!1&&(b===!0&&d>=0?this.highlight(d):this.highlight(0)),b===!0){var g=this.opts.minimumResultsForSearch;g>=0&&this.showSearch(L(a.results)>=g)}},showSearch:function(b){this.showSearchInput!==b&&(this.showSearchInput=b,this.dropdown.find(".select2-search").toggleClass("select2-search-hidden",!b),this.dropdown.find(".select2-search").toggleClass("select2-offscreen",!b),a(this.dropdown,this.container).toggleClass("select2-with-searchbox",b))},onSelect:function(a,b){if(this.triggerSelect(a)){var c=this.opts.element.val(),d=this.data();this.opts.element.val(this.id(a)),this.updateSelection(a),this.opts.element.trigger({type:"select2-selected",val:this.id(a),choice:a}),this.nextSearchTerm=this.opts.nextSearchTerm(a,this.search.val()),this.close(),b&&b.noFocus||!this.opts.shouldFocusInput(this)||this.focusser.focus(),r(c,this.id(a))||this.triggerChange({added:a,removed:d})}},updateSelection:function(a){var d,e,c=this.selection.find(".select2-chosen");this.selection.data("select2-data",a),c.empty(),null!==a&&(d=this.opts.formatSelection(a,c,this.opts.escapeMarkup)),d!==b&&c.append(d),e=this.opts.formatSelectionCssClass(a,c),e!==b&&c.addClass(e),this.selection.removeClass("select2-default"),this.opts.allowClear&&this.getPlaceholder()!==b&&this.container.addClass("select2-allowclear")},val:function(){var a,c=!1,d=null,e=this,f=this.data();if(0===arguments.length)return this.opts.element.val();if(a=arguments[0],arguments.length>1&&(c=arguments[1]),this.select)this.select.val(a).find("option").filter(function(){return this.selected}).each2(function(a,b){return d=e.optionToData(b),!1}),this.updateSelection(d),this.setPlaceholder(),c&&this.triggerChange({added:d,removed:f});else{if(!a&&0!==a)return this.clear(c),void 0;if(this.opts.initSelection===b)throw new Error("cannot call val() if initSelection() is not defined");this.opts.element.val(a),this.opts.initSelection(this.opts.element,function(a){e.opts.element.val(a?e.id(a):""),e.updateSelection(a),e.setPlaceholder(),c&&e.triggerChange({added:a,removed:f})})}},clearSearch:function(){this.search.val(""),this.focusser.val("")},data:function(a){var c,d=!1;return 0===arguments.length?(c=this.selection.data("select2-data"),c==b&&(c=null),c):(arguments.length>1&&(d=arguments[1]),a?(c=this.data(),this.opts.element.val(a?this.id(a):""),this.updateSelection(a),d&&this.triggerChange({added:a,removed:c})):this.clear(d),void 0)}}),f=O(d,{createContainer:function(){var b=a(document.createElement("div")).attr({"class":"select2-container select2-container-multi"}).html(["<ul class='select2-choices'>"," <li class='select2-search-field'>"," <label for='' class='select2-offscreen'></label>"," <input type='text' autocomplete='off' autocorrect='off' autocapitalize='off' spellcheck='false' class='select2-input'>"," </li>","</ul>","<div class='select2-drop select2-drop-multi select2-display-none'>"," <ul class='select2-results'>"," </ul>","</div>"].join(""));return b},prepareOpts:function(){var b=this.parent.prepareOpts.apply(this,arguments),c=this;return"select"===b.element.get(0).tagName.toLowerCase()?b.initSelection=function(a,b){var d=[];a.find("option").filter(function(){return this.selected&&!this.disabled}).each2(function(a,b){d.push(c.optionToData(b))}),b(d)}:"data"in b&&(b.initSelection=b.initSelection||function(c,d){var e=s(c.val(),b.separator),f=[];b.query({matcher:function(c,d,g){var h=a.grep(e,function(a){return r(a,b.id(g))}).length;return h&&f.push(g),h},callback:a.isFunction(d)?function(){for(var a=[],c=0;c<e.length;c++)for(var g=e[c],h=0;h<f.length;h++){var i=f[h];if(r(g,b.id(i))){a.push(i),f.splice(h,1);break}}d(a)}:a.noop})}),b},selectChoice:function(a){var b=this.container.find(".select2-search-choice-focus");b.length&&a&&a[0]==b[0]||(b.length&&this.opts.element.trigger("choice-deselected",b),b.removeClass("select2-search-choice-focus"),a&&a.length&&(this.close(),a.addClass("select2-search-choice-focus"),this.opts.element.trigger("choice-selected",a)))},destroy:function(){a("label[for='"+this.search.attr("id")+"']").attr("for",this.opts.element.attr("id")),this.parent.destroy.apply(this,arguments),N.call(this,"searchContainer","selection")},initContainer:function(){var d,b=".select2-choices";this.searchContainer=this.container.find(".select2-search-field"),this.selection=d=this.container.find(b);var e=this;this.selection.on("click",".select2-search-choice:not(.select2-locked)",function(){e.search[0].focus(),e.selectChoice(a(this))}),this.search.attr("id","s2id_autogen"+g()),this.search.prev().text(a("label[for='"+this.opts.element.attr("id")+"']").text()).attr("for",this.search.attr("id")),this.search.on("input paste",this.bind(function(){this.search.attr("placeholder")&&0==this.search.val().length||this.isInterfaceEnabled()&&(this.opened()||this.open())})),this.search.attr("tabindex",this.elementTabIndex),this.keydowns=0,this.search.on("keydown",this.bind(function(a){if(this.isInterfaceEnabled()){++this.keydowns;var b=d.find(".select2-search-choice-focus"),e=b.prev(".select2-search-choice:not(.select2-locked)"),f=b.next(".select2-search-choice:not(.select2-locked)"),g=z(this.search);if(b.length&&(a.which==c.LEFT||a.which==c.RIGHT||a.which==c.BACKSPACE||a.which==c.DELETE||a.which==c.ENTER)){var h=b;return a.which==c.LEFT&&e.length?h=e:a.which==c.RIGHT?h=f.length?f:null:a.which===c.BACKSPACE?this.unselect(b.first())&&(this.search.width(10),h=e.length?e:f):a.which==c.DELETE?this.unselect(b.first())&&(this.search.width(10),h=f.length?f:null):a.which==c.ENTER&&(h=null),this.selectChoice(h),A(a),h&&h.length||this.open(),void 0}if((a.which===c.BACKSPACE&&1==this.keydowns||a.which==c.LEFT)&&0==g.offset&&!g.length)return this.selectChoice(d.find(".select2-search-choice:not(.select2-locked)").last()),A(a),void 0;if(this.selectChoice(null),this.opened())switch(a.which){case c.UP:case c.DOWN:return this.moveHighlight(a.which===c.UP?-1:1),A(a),void 0;case c.ENTER:return this.selectHighlighted(),A(a),void 0;case c.TAB:return this.selectHighlighted({noFocus:!0}),this.close(),void 0;case c.ESC:return this.cancel(a),A(a),void 0}if(a.which!==c.TAB&&!c.isControl(a)&&!c.isFunctionKey(a)&&a.which!==c.BACKSPACE&&a.which!==c.ESC){if(a.which===c.ENTER){if(this.opts.openOnEnter===!1)return;if(a.altKey||a.ctrlKey||a.shiftKey||a.metaKey)return}this.open(),(a.which===c.PAGE_UP||a.which===c.PAGE_DOWN)&&A(a),a.which===c.ENTER&&A(a)}}})),this.search.on("keyup",this.bind(function(){this.keydowns=0,this.resizeSearch()})),this.search.on("blur",this.bind(function(b){this.container.removeClass("select2-container-active"),this.search.removeClass("select2-focused"),this.selectChoice(null),this.opened()||this.clearSearch(),b.stopImmediatePropagation(),this.opts.element.trigger(a.Event("select2-blur"))})),this.container.on("click",b,this.bind(function(b){this.isInterfaceEnabled()&&(a(b.target).closest(".select2-search-choice").length>0||(this.selectChoice(null),this.clearPlaceholder(),this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.open(),this.focusSearch(),b.preventDefault()))})),this.container.on("focus",b,this.bind(function(){this.isInterfaceEnabled()&&(this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.container.addClass("select2-container-active"),this.dropdown.addClass("select2-drop-active"),this.clearPlaceholder())})),this.initContainerWidth(),this.opts.element.addClass("select2-offscreen"),this.clearSearch()},enableInterface:function(){this.parent.enableInterface.apply(this,arguments)&&this.search.prop("disabled",!this.isInterfaceEnabled())},initSelection:function(){if(""===this.opts.element.val()&&""===this.opts.element.text()&&(this.updateSelection([]),this.close(),this.clearSearch()),this.select||""!==this.opts.element.val()){var c=this;this.opts.initSelection.call(null,this.opts.element,function(a){a!==b&&null!==a&&(c.updateSelection(a),c.close(),c.clearSearch())})}},clearSearch:function(){var a=this.getPlaceholder(),c=this.getMaxSearchWidth();a!==b&&0===this.getVal().length&&this.search.hasClass("select2-focused")===!1?(this.search.val(a).addClass("select2-default"),this.search.width(c>0?c:this.container.css("width"))):this.search.val("").width(10)},clearPlaceholder:function(){this.search.hasClass("select2-default")&&this.search.val("").removeClass("select2-default")},opening:function(){this.clearPlaceholder(),this.resizeSearch(),this.parent.opening.apply(this,arguments),this.focusSearch(),""===this.search.val()&&this.nextSearchTerm!=b&&(this.search.val(this.nextSearchTerm),this.search.select()),this.updateResults(!0),this.opts.shouldFocusInput(this)&&this.search.focus(),this.opts.element.trigger(a.Event("select2-open"))},close:function(){this.opened()&&this.parent.close.apply(this,arguments)},focus:function(){this.close(),this.search.focus()},isFocused:function(){return this.search.hasClass("select2-focused")},updateSelection:function(b){var c=[],d=[],e=this;a(b).each(function(){p(e.id(this),c)<0&&(c.push(e.id(this)),d.push(this))}),b=d,this.selection.find(".select2-search-choice").remove(),a(b).each(function(){e.addSelectedChoice(this)}),e.postprocessResults()},tokenize:function(){var a=this.search.val();a=this.opts.tokenizer.call(this,a,this.data(),this.bind(this.onSelect),this.opts),null!=a&&a!=b&&(this.search.val(a),a.length>0&&this.open())},onSelect:function(a,c){this.triggerSelect(a)&&(this.addSelectedChoice(a),this.opts.element.trigger({type:"selected",val:this.id(a),choice:a}),this.nextSearchTerm=this.opts.nextSearchTerm(a,this.search.val()),this.clearSearch(),this.updateResults(),(this.select||!this.opts.closeOnSelect)&&this.postprocessResults(a,!1,this.opts.closeOnSelect===!0),this.opts.closeOnSelect?(this.close(),this.search.width(10)):this.countSelectableResults()>0?(this.search.width(10),this.resizeSearch(),this.getMaximumSelectionSize()>0&&this.val().length>=this.getMaximumSelectionSize()?this.updateResults(!0):this.nextSearchTerm!=b&&(this.search.val(this.nextSearchTerm),this.updateResults(),this.search.select()),this.positionDropdown()):(this.close(),this.search.width(10)),this.triggerChange({added:a}),c&&c.noFocus||this.focusSearch())},cancel:function(){this.close(),this.focusSearch()},addSelectedChoice:function(c){var j,k,d=!c.locked,e=a("<li class='select2-search-choice'> <div></div> <a href='#' class='select2-search-choice-close' tabindex='-1'></a></li>"),f=a("<li class='select2-search-choice select2-locked'><div></div></li>"),g=d?e:f,h=this.id(c),i=this.getVal();j=this.opts.formatSelection(c,g.find("div"),this.opts.escapeMarkup),j!=b&&g.find("div").replaceWith("<div>"+j+"</div>"),k=this.opts.formatSelectionCssClass(c,g.find("div")),k!=b&&g.addClass(k),d&&g.find(".select2-search-choice-close").on("mousedown",A).on("click dblclick",this.bind(function(b){this.isInterfaceEnabled()&&(this.unselect(a(b.target)),this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus"),A(b),this.close(),this.focusSearch())})).on("focus",this.bind(function(){this.isInterfaceEnabled()&&(this.container.addClass("select2-container-active"),this.dropdown.addClass("select2-drop-active"))})),g.data("select2-data",c),g.insertBefore(this.searchContainer),i.push(h),this.setVal(i)},unselect:function(b){var d,e,c=this.getVal();if(b=b.closest(".select2-search-choice"),0===b.length)throw"Invalid argument: "+b+". Must be .select2-search-choice";if(d=b.data("select2-data")){var f=a.Event("select2-removing");if(f.val=this.id(d),f.choice=d,this.opts.element.trigger(f),f.isDefaultPrevented())return!1;for(;(e=p(this.id(d),c))>=0;)c.splice(e,1),this.setVal(c),this.select&&this.postprocessResults();return b.remove(),this.opts.element.trigger({type:"select2-removed",val:this.id(d),choice:d}),this.triggerChange({removed:d}),!0}},postprocessResults:function(a,b,c){var d=this.getVal(),e=this.results.find(".select2-result"),f=this.results.find(".select2-result-with-children"),g=this;e.each2(function(a,b){var c=g.id(b.data("select2-data"));p(c,d)>=0&&(b.addClass("select2-selected"),b.find(".select2-result-selectable").addClass("select2-selected"))}),f.each2(function(a,b){b.is(".select2-result-selectable")||0!==b.find(".select2-result-selectable:not(.select2-selected)").length||b.addClass("select2-selected")}),-1==this.highlight()&&c!==!1&&g.highlight(0),!this.opts.createSearchChoice&&!e.filter(".select2-result:not(.select2-selected)").length>0&&(!a||a&&!a.more&&0===this.results.find(".select2-no-results").length)&&J(g.opts.formatNoMatches,"formatNoMatches")&&this.results.append("<li class='select2-no-results'>"+K(g.opts.formatNoMatches,g.opts.element,g.search.val())+"</li>")},getMaxSearchWidth:function(){return this.selection.width()-t(this.search)},resizeSearch:function(){var a,b,c,d,e,f=t(this.search);a=C(this.search)+10,b=this.search.offset().left,c=this.selection.width(),d=this.selection.offset().left,e=c-(b-d)-f,a>e&&(e=c-f),40>e&&(e=c-f),0>=e&&(e=a),this.search.width(Math.floor(e))},getVal:function(){var a;return this.select?(a=this.select.val(),null===a?[]:a):(a=this.opts.element.val(),s(a,this.opts.separator))},setVal:function(b){var c;this.select?this.select.val(b):(c=[],a(b).each(function(){p(this,c)<0&&c.push(this)}),this.opts.element.val(0===c.length?"":c.join(this.opts.separator)))},buildChangeDetails:function(a,b){for(var b=b.slice(0),a=a.slice(0),c=0;c<b.length;c++)for(var d=0;d<a.length;d++)r(this.opts.id(b[c]),this.opts.id(a[d]))&&(b.splice(c,1),c>0&&c--,a.splice(d,1),d--);return{added:b,removed:a}},val:function(c,d){var e,f=this;if(0===arguments.length)return this.getVal();if(e=this.data(),e.length||(e=[]),!c&&0!==c)return this.opts.element.val(""),this.updateSelection([]),this.clearSearch(),d&&this.triggerChange({added:this.data(),removed:e}),void 0;if(this.setVal(c),this.select)this.opts.initSelection(this.select,this.bind(this.updateSelection)),d&&this.triggerChange(this.buildChangeDetails(e,this.data()));else{if(this.opts.initSelection===b)throw new Error("val() cannot be called if initSelection() is not defined");this.opts.initSelection(this.opts.element,function(b){var c=a.map(b,f.id);f.setVal(c),f.updateSelection(b),f.clearSearch(),d&&f.triggerChange(f.buildChangeDetails(e,f.data()))})}this.clearSearch()},onSortStart:function(){if(this.select)throw new Error("Sorting of elements is not supported when attached to <select>. Attach to <input type='hidden'/> instead.");this.search.width(0),this.searchContainer.hide()},onSortEnd:function(){var b=[],c=this;this.searchContainer.show(),this.searchContainer.appendTo(this.searchContainer.parent()),this.resizeSearch(),this.selection.find(".select2-search-choice").each(function(){b.push(c.opts.id(a(this).data("select2-data")))}),this.setVal(b),this.triggerChange()},data:function(b,c){var e,f,d=this;return 0===arguments.length?this.selection.children(".select2-search-choice").map(function(){return a(this).data("select2-data")}).get():(f=this.data(),b||(b=[]),e=a.map(b,function(a){return d.opts.id(a)}),this.setVal(e),this.updateSelection(b),this.clearSearch(),c&&this.triggerChange(this.buildChangeDetails(f,this.data())),void 0)}}),a.fn.select2=function(){var d,e,f,g,h,c=Array.prototype.slice.call(arguments,0),i=["val","destroy","opened","open","close","focus","isFocused","container","dropdown","onSortStart","onSortEnd","enable","disable","readonly","positionDropdown","data","search"],j=["opened","isFocused","container","dropdown"],k=["val","data"],l={search:"externalSearch"};return this.each(function(){if(0===c.length||"object"==typeof c[0])d=0===c.length?{}:a.extend({},c[0]),d.element=a(this),"select"===d.element.get(0).tagName.toLowerCase()?h=d.element.prop("multiple"):(h=d.multiple||!1,"tags"in d&&(d.multiple=h=!0)),e=h?new window.Select2["class"].multi:new window.Select2["class"].single,e.init(d);else{if("string"!=typeof c[0])throw"Invalid arguments to select2 plugin: "+c;if(p(c[0],i)<0)throw"Unknown method: "+c[0];if(g=b,e=a(this).data("select2"),e===b)return;if(f=c[0],"container"===f?g=e.container:"dropdown"===f?g=e.dropdown:(l[f]&&(f=l[f]),g=e[f].apply(e,c.slice(1))),p(c[0],j)>=0||p(c[0],k)>=0&&1==c.length)return!1}}),g===b?this:g},a.fn.select2.defaults={width:"copy",loadMorePadding:0,closeOnSelect:!0,openOnEnter:!0,containerCss:{},dropdownCss:{},containerCssClass:"",dropdownCssClass:"",formatResult:function(a,b,c,d){var e=[];return E(a.text,c.term,e,d),e.join("")},formatSelection:function(a,c,d){return a?d(a.text):b},sortResults:function(a){return a},formatResultCssClass:function(a){return a.css},formatSelectionCssClass:function(){return b},formatMatches:function(a){return 1===a?"One result is available, press enter to select it.":a+" results are available, use up and down arrow keys to navigate."},formatNoMatches:function(){return"No matches found"},formatInputTooShort:function(a,b){var c=b-a.length;return"Please enter "+c+" or more character"+(1==c?"":"s")},formatInputTooLong:function(a,b){var c=a.length-b;return"Please delete "+c+" character"+(1==c?"":"s")},formatSelectionTooBig:function(a){return"You can only select "+a+" item"+(1==a?"":"s")},formatLoadMore:function(){return"Loading more results\u2026"},formatSearching:function(){return"Searching\u2026"},minimumResultsForSearch:0,minimumInputLength:0,maximumInputLength:null,maximumSelectionSize:0,id:function(a){return a==b?null:a.id},matcher:function(a,b){return o(""+b).toUpperCase().indexOf(o(""+a).toUpperCase())>=0},separator:",",tokenSeparators:[],tokenizer:M,escapeMarkup:F,blurOnChange:!1,selectOnBlur:!1,adaptContainerCssClass:function(a){return a},adaptDropdownCssClass:function(){return null +},nextSearchTerm:function(){return b},searchInputPlaceholder:"",createSearchChoicePosition:"top",shouldFocusInput:function(a){var b="ontouchstart"in window||navigator.msMaxTouchPoints>0;return b?a.opts.minimumResultsForSearch<0?!1:!0:!0}},a.fn.select2.ajaxDefaults={transport:a.ajax,params:{type:"GET",cache:!1,dataType:"json"}},window.Select2={query:{ajax:G,local:H,tags:I},util:{debounce:w,markMatch:E,escapeMarkup:F,stripDiacritics:o},"class":{"abstract":d,single:e,multi:f}}}}(jQuery); \ No newline at end of file diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/lib/simple-slider.min.js b/snf-cyclades-app/synnefo/ui/static/snf/js/lib/simple-slider.min.js new file mode 100644 index 0000000000000000000000000000000000000000..6d6d1e469f4b4c5905768dd230354453fd02d225 --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/lib/simple-slider.min.js @@ -0,0 +1,376 @@ +/* + jQuery Simple Slider + + Copyright (c) 2012 James Smith (http://loopj.com) + + Licensed under the MIT license (http://mit-license.org/) +*/ + +var __slice = [].slice, + __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; + +(function($, window) { + var SimpleSlider; + SimpleSlider = (function() { + + function SimpleSlider(input, options) { + var ratio, + _this = this; + this.input = input; + this.defaultOptions = { + animate: true, + snapMid: false, + classPrefix: null, + classSuffix: null, + theme: null, + highlight: false + }; + this.settings = $.extend({}, this.defaultOptions, options); + if (this.settings.theme) { + this.settings.classSuffix = "-" + this.settings.theme; + } + this.input.hide(); + this.slider = $("<div>").addClass("slider" + (this.settings.classSuffix || "")).css({ + position: "relative", + userSelect: "none", + boxSizing: "border-box" + }).insertBefore(this.input).addClass("slider"); + if (this.input.attr("id")) { + this.slider.attr("id", this.input.attr("id") + "-slider"); + } + this.track = this.createDivElement("track").css({ + width: "100%" + }); + if (this.settings.highlight) { + this.highlightTrack = this.createDivElement("highlight-track").css({ + width: "0" + }); + } + this.dragger = this.createDivElement("dragger"); + this.slider.css({ + minHeight: this.dragger.outerHeight(), + marginLeft: this.dragger.outerWidth() / 2, + marginRight: this.dragger.outerWidth() / 2 + }); + this.track.css({ + marginTop: this.track.outerHeight() / -2 + }); + if (this.settings.highlight) { + this.highlightTrack.css({ + marginTop: this.track.outerHeight() / -2 + }); + } + this.dragger.css({ + marginTop: this.dragger.outerHeight() / -2, + marginLeft: this.dragger.outerWidth() / -2 + }); + this.track.mousedown(function(e) { + return _this.trackEvent(e); + }); + if (this.settings.highlight) { + this.highlightTrack.mousedown(function(e) { + return _this.trackEvent(e); + }); + } + this.dragger.mousedown(function(e) { + if (e.which !== 1) { + return; + } + _this.dragging = true; + _this.dragger.addClass("dragging"); + _this.domDrag(e.pageX, e.pageY); + return false; + }); + $("body").mousemove(function(e) { + if (_this.dragging) { + _this.domDrag(e.pageX, e.pageY); + return $("body").css({ + cursor: "pointer" + }); + } + }).mouseup(function(e) { + if (_this.dragging) { + _this.dragging = false; + _this.dragger.removeClass("dragging"); + return $("body").css({ + cursor: "auto" + }); + } + }); + this.pagePos = 0; + if (this.input.val() === "") { + this.value = this.getRange().min; + this.input.val(this.value); + } else { + this.value = this.nearestValidValue(this.input.val()); + } + this.setSliderPositionFromValue(this.value); + ratio = this.valueToRatio(this.value); + this.input.trigger("slider:ready", { + value: this.value, + ratio: ratio, + position: ratio * this.slider.outerWidth(), + el: this.slider + }); + } + + SimpleSlider.prototype.createDivElement = function(classname) { + var item; + item = $("<div>").addClass(classname).css({ + position: "absolute", + top: "50%", + userSelect: "none", + cursor: "pointer" + }).appendTo(this.slider); + return item; + }; + + SimpleSlider.prototype.setRatio = function(ratio) { + var value; + ratio = Math.min(1, ratio); + ratio = Math.max(0, ratio); + value = this.ratioToValue(ratio); + this.setSliderPositionFromValue(value); + return this.valueChanged(value, ratio, "setRatio"); + }; + + SimpleSlider.prototype.setMax = function(value) { + var curr = this.input.val(); + this.settings.range[1] = parseInt(value); + this.setValue(curr); + } + + SimpleSlider.prototype.setMin = function(value) { + var curr = this.input.val(); + this.settings.range[0] = parseInt(value); + this.setValue(curr); + } + + SimpleSlider.prototype.setValue = function(value) { + var ratio; + value = this.nearestValidValue(value); + ratio = this.valueToRatio(value); + this.setSliderPositionFromValue(value); + return this.valueChanged(value, ratio, "setValue"); + }; + + SimpleSlider.prototype.trackEvent = function(e) { + if (e.which !== 1) { + return; + } + this.domDrag(e.pageX, e.pageY, true); + this.dragging = true; + return false; + }; + + SimpleSlider.prototype.domDrag = function(pageX, pageY, animate) { + var pagePos, ratio, value; + if (animate == null) { + animate = false; + } + pagePos = pageX - this.slider.offset().left; + pagePos = Math.min(this.slider.outerWidth(), pagePos); + pagePos = Math.max(0, pagePos); + if (this.pagePos !== pagePos) { + this.pagePos = pagePos; + ratio = pagePos / this.slider.outerWidth(); + value = this.ratioToValue(ratio); + this.valueChanged(value, ratio, "domDrag"); + if (this.settings.snap) { + return this.setSliderPositionFromValue(value, animate); + } else { + return this.setSliderPosition(pagePos, animate); + } + } + }; + + SimpleSlider.prototype.setSliderPosition = function(position, animate) { + if (animate == null) { + animate = false; + } + if (animate && this.settings.animate) { + this.dragger.animate({ + left: position + }, 200); + if (this.settings.highlight) { + return this.highlightTrack.animate({ + width: position + }, 200); + } + } else { + this.dragger.css({ + left: position + }); + if (this.settings.highlight) { + return this.highlightTrack.css({ + width: position + }); + } + } + }; + + SimpleSlider.prototype.setSliderPositionFromValue = function(value, animate) { + var ratio; + if (animate == null) { + animate = false; + } + ratio = this.valueToRatio(value); + return this.setSliderPosition(ratio * this.slider.outerWidth(), animate); + }; + + SimpleSlider.prototype.getRange = function() { + if (this.settings.allowedValues) { + return { + min: Math.min.apply(Math, this.settings.allowedValues), + max: Math.max.apply(Math, this.settings.allowedValues) + }; + } else if (this.settings.range) { + return { + min: parseFloat(this.settings.range[0]), + max: parseFloat(this.settings.range[1]) + }; + } else { + return { + min: 0, + max: 1 + }; + } + }; + + SimpleSlider.prototype.nearestValidValue = function(rawValue) { + var closest, maxSteps, range, steps; + range = this.getRange(); + rawValue = Math.min(range.max, rawValue); + rawValue = Math.max(range.min, rawValue); + if (this.settings.allowedValues) { + closest = null; + $.each(this.settings.allowedValues, function() { + if (closest === null || Math.abs(this - rawValue) < Math.abs(closest - rawValue)) { + return closest = this; + } + }); + return closest; + } else if (this.settings.step) { + maxSteps = (range.max - range.min) / this.settings.step; + steps = Math.floor((rawValue - range.min) / this.settings.step); + if ((rawValue - range.min) % this.settings.step > this.settings.step / 2 && steps < maxSteps) { + steps += 1; + } + return steps * this.settings.step + range.min; + } else { + return rawValue; + } + }; + + SimpleSlider.prototype.valueToRatio = function(value) { + var allowedVal, closest, closestIdx, idx, range, _i, _len, _ref; + if (this.settings.equalSteps) { + _ref = this.settings.allowedValues; + for (idx = _i = 0, _len = _ref.length; _i < _len; idx = ++_i) { + allowedVal = _ref[idx]; + if (!(typeof closest !== "undefined" && closest !== null) || Math.abs(allowedVal - value) < Math.abs(closest - value)) { + closest = allowedVal; + closestIdx = idx; + } + } + if (this.settings.snapMid) { + return (closestIdx + 0.5) / this.settings.allowedValues.length; + } else { + return closestIdx / (this.settings.allowedValues.length - 1); + } + } else { + range = this.getRange(); + return (value - range.min) / (range.max - range.min); + } + }; + + SimpleSlider.prototype.ratioToValue = function(ratio) { + var idx, range, rawValue, step, steps; + if (this.settings.equalSteps) { + steps = this.settings.allowedValues.length; + step = Math.round(ratio * steps - 0.5); + idx = Math.min(step, this.settings.allowedValues.length - 1); + return this.settings.allowedValues[idx]; + } else { + range = this.getRange(); + rawValue = ratio * (range.max - range.min) + range.min; + return this.nearestValidValue(rawValue); + } + }; + + SimpleSlider.prototype.valueChanged = function(value, ratio, trigger) { + var eventData; + if (value.toString() === this.value.toString()) { + this.input.val(value); + return; + } + this.value = value; + eventData = { + value: value, + ratio: ratio, + position: ratio * this.slider.outerWidth(), + trigger: trigger, + el: this.slider + }; + return this.input.val(value).trigger($.Event("change", eventData)).trigger("slider:changed", eventData); + }; + + return SimpleSlider; + + })(); + $.extend($.fn, { + simpleSlider: function() { + var params, publicMethods, settingsOrMethod; + settingsOrMethod = arguments[0], params = 2 <= arguments.length ? __slice.call(arguments, 1) : []; + publicMethods = ["setRatio", "setValue", "setMax", "setMin"]; + return $(this).each(function() { + var obj, settings; + if (settingsOrMethod && __indexOf.call(publicMethods, settingsOrMethod) >= 0) { + obj = $(this).data("slider-object"); + return obj[settingsOrMethod].apply(obj, params); + } else { + settings = settingsOrMethod; + return $(this).data("slider-object", new SimpleSlider($(this), settings)); + } + }); + } + }); + return $(function() { + return $("[data-slider]").each(function() { + var $el, allowedValues, settings, x; + $el = $(this); + settings = {}; + allowedValues = $el.data("slider-values"); + if (allowedValues) { + settings.allowedValues = (function() { + var _i, _len, _ref, _results; + _ref = allowedValues.split(","); + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + x = _ref[_i]; + _results.push(parseFloat(x)); + } + return _results; + })(); + } + if ($el.data("slider-range")) { + settings.range = $el.data("slider-range").split(","); + } + if ($el.data("slider-step")) { + settings.step = $el.data("slider-step"); + } + settings.snap = $el.data("slider-snap"); + settings.equalSteps = $el.data("slider-equal-steps"); + if ($el.data("slider-theme")) { + settings.theme = $el.data("slider-theme"); + } + if ($el.attr("data-slider-highlight")) { + settings.highlight = $el.data("slider-highlight"); + } + if ($el.data("slider-animate") != null) { + settings.animate = $el.data("slider-animate"); + } + return $el.simpleSlider(settings); + }); + }); +})(this.jQuery || this.Zepto, this); diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/models.js b/snf-cyclades-app/synnefo/ui/static/snf/js/models.js index c296a73adada49373d6efd4b7e4405a280d8086c..bf0c7dcf7c3bff85f6b73ccf8b89e08b2d0196ef 100644 --- a/snf-cyclades-app/synnefo/ui/static/snf/js/models.js +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/models.js @@ -1,35 +1,17 @@ -// Copyright 2011 GRNET S.A. All rights reserved. -// -// Redistribution and use in source and binary forms, with or -// without modification, are permitted provided that the following -// conditions are met: -// -// 1. Redistributions of source code must retain the above -// copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following -// disclaimer in the documentation and/or other materials -// provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// -// The views and conclusions contained in the software and -// documentation are those of the authors and should not be -// interpreted as representing official policies, either expressed -// or implied, of GRNET S.A. +// Copyright (C) 2010-2014 GRNET S.A. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. // ;(function(root){ @@ -334,6 +316,23 @@ api: snf.api, api_type: 'compute', supportIncUpdates: true, + + mapAttrs: function() { + var params = _.toArray(arguments); + return this.map(function(i) { + return _.map(params, function(attr) { + return i.get(attr); + }); + }) + }, + + mapAttr: function(attr) { + return this.map(function(i) { return i.get(attr)}) + }, + + filterAttr: function(attr, eq) { + return this.filter(function(i) { return i.get(attr) === eq}) + }, initialize: function() { models.Collection.__super__.initialize.apply(this, arguments); @@ -353,6 +352,12 @@ return getUrl.call(this, this.base_url) + ( options.details || this.details && method != 'create' ? '/detail' : ''); }, + + delay_fetch: function(delay, options) { + window.setTimeout(_.bind(function() { + this.fetch(options) + }, this), delay); + }, fetch: function(options) { if (!options) { options = {} }; @@ -392,6 +397,7 @@ if (coll.add_on_create) { coll.add(nextModel, options); } + synnefo.api.trigger("quota:update"); if (success) success(nextModel, resp, xhr); }; model.save(null, options); @@ -618,6 +624,34 @@ return parseInt(this.get("ram")) * 1024 * 1024; }, + get_readable: function(key) { + var parser = function(v) { return v } + var getter = this.get; + if (key == 'ram') { + parser = synnefo.util.readablizeBytes + getter = this.ram_to_bytes + } + if (key == 'disk') { + parser = synnefo.util.readablizeBytes + getter = this.disk_to_bytes + } + if (key == 'cpu') { + parser = function(v) { return v + 'x' } + } + + var val = getter.call(this, key); + return parser(val); + }, + + quotas: function() { + return { + 'cyclades.vm': 1, + 'cyclades.disk': this.disk_to_bytes(), + 'cyclades.ram': this.ram_to_bytes(), + 'cyclades.cpu': this.get('cpu') + } + } + }); models.ParamsList = function(){this.initialize.apply(this, arguments)}; @@ -737,14 +771,26 @@ ] }, + storage_attrs: { + 'tenant_id': ['projects', 'project'] + }, + initialize: function(params) { var self = this; + this.ports = new Backbone.FilteredCollection(undefined, { collection: synnefo.storage.ports, collectionFilter: function(m) { return self.id == m.get('device_id') }}); + this.volumes = new Backbone.FilteredCollection(undefined, { + collection: synnefo.storage.volumes, + collectionFilter: function(m) { + var volumes = _.map(self.get('volumes'), function(id) { return ''+id }); + return _.contains(volumes, m.id+''); + }}); + this.pending_firewalls = {}; models.VM.__super__.initialize.apply(this, arguments); @@ -832,6 +878,11 @@ }, this) return found; }, + + is_ext: function() { + var tpl = this.get_flavor().get('disk_template'); + return tpl.indexOf('ext_') == 0; + }, status: function(st) { if (!st) { return this.get("status")} @@ -1104,8 +1155,12 @@ }, can_start: function(flv, count_current) { + if (!this.can_resize()) { return false; } + var self = this; var get_quota = function(key) { - return synnefo.storage.quotas.get(key).get('available'); + if (!self.get('project')) { return false; } + var quota = self.get('project').quotas.get(key); + return quota && quota.get('available'); } var flavor = flv || this.get_flavor(); var vm_ram_current = 0, vm_cpu_current = 0; @@ -1122,6 +1177,11 @@ return true }, + can_attach_volume: function() { + return _.contains(["ACTIVE", "STOPPED"], this.get("status")) && + !this.get('suspended') + }, + can_connect: function() { if (!synnefo.config.hotplug_enabled && this.is_active()) { return false } return _.contains(["ACTIVE", "STOPPED"], this.get("status")) && @@ -1133,7 +1193,12 @@ }, can_resize: function() { - return this.get('status') == 'STOPPED'; + return this.get('status') == 'STOPPED' && + !this.get('project').get('missing'); + }, + + can_reassign: function() { + return true; }, handle_stats_error: function() { @@ -1234,6 +1299,14 @@ return this.get("pending_action") ? this.get("pending_action") : false; }, + active_resources: function() { + if (this.get("status") == "STOPPED") { + return ["cyclades.vm", "cyclades.disk"] + } + return ["cyclades.vm", "cyclades.disk", "cyclades.cpu", + "cyclades.ram"] + }, + // machine is active is_active: function() { return models.VM.ACTIVE_STATES.indexOf(this.state()) > -1; @@ -1399,6 +1472,7 @@ // call rename api rename: function(new_name) { //this.set({'name': new_name}); + var self = this; this.sync("update", this, { critical: true, data: { @@ -1408,6 +1482,7 @@ }, success: _.bind(function(){ snf.api.trigger("call"); + this.set({'name': new_name}); }, this) }); }, @@ -1415,8 +1490,7 @@ get_console_url: function(data) { var url_params = { machine: this.get("name"), - host_ip: this.get_hostname(), - host_ip_v6: this.get_hostname(), + machine_hostname: this.get_hostname(), host: data.host, port: data.port, password: data.password @@ -1450,6 +1524,24 @@ skip_api_error: false }); }, + + create_snapshot: function(snapshot_params, callback, error_cb) { + var volume = this.get('volumes') && this.get('volumes').length ? + this.get('volumes')[0] : undefined; + var params = _.extend({ + 'metadata': {}, + 'volume_id': volume + }, snapshot_params); + + snf.api.sync('create', undefined, { + url: synnefo.config.api_urls.volume + '/snapshots/', + data: JSON.stringify({snapshot:params}), + success: callback, + error: error_cb, + skip_api_error: false, + contentType: 'application/json' + }); + }, // action helper call: function(action_name, success, error, params) { @@ -1501,7 +1593,7 @@ break; case 'console': this.__make_api_call(this.url() + "/action", "create", - {'console': {'type':'vnc'}}, + {'console': {'type':'vnc-wss'}}, function(data) { var cons_data = data.console; success.apply(this, [cons_data]); @@ -1515,10 +1607,22 @@ // set state after successful call self.state('DESTROY'); success.apply(this, arguments); - + synnefo.api.trigger("quotas:call", 20); }, error, 'destroy', params); break; + case 'reassign': + this.__make_api_call(this.get_action_url(), // vm actions url + "create", // create so that sync later uses POST to make the call + {reassign: {project:params.project_id}}, // payload + function() { + self.state('reassign'); + self.set({'tenant_id': params.project_id}); + success.apply(this, arguments); + snf.api.trigger("call"); + }, + error, 'reassign', params); + break; case 'resize': this.__make_api_call(this.get_action_url(), // vm actions url "create", // create so that sync later uses POST to make the call @@ -1660,26 +1764,31 @@ 'console', 'destroy', 'resize', + 'reassign', 'snapshot' ] models.VM.TASK_STATE_STATUS_MAP = { - 'BULDING': 'BUILD', + 'BUILDING': 'BUILD', 'REBOOTING': 'REBOOT', 'STOPPING': 'SHUTDOWN', 'STARTING': 'START', 'RESIZING': 'RESIZE', + 'REASSIGNING': 'REASSIGN', 'CONNECTING': 'CONNECT', 'DISCONNECTING': 'DISCONNECT', + 'UNKNOWN': 'UNKNOWN', + 'ATTACHING_VOLUME': 'ATTACH_VOLUME', + 'DETACHING_VOLUME': 'DETACH_VOLUME', 'DESTROYING': 'DESTROY' } models.VM.AVAILABLE_ACTIONS = { - 'UNKNWON' : ['destroy'], + 'UNKNOWN' : ['destroy'], 'BUILD' : ['destroy'], 'REBOOT' : ['destroy'], - 'STOPPED' : ['start', 'destroy', 'resize', 'snapshot'], - 'ACTIVE' : ['shutdown', 'destroy', 'reboot', 'console', 'resize', 'snapshot'], + 'STOPPED' : ['start', 'destroy', 'reassign', 'resize', 'snapshot'], + 'ACTIVE' : ['shutdown', 'destroy', 'reboot', 'console', 'reassign', 'resize', 'snapshot'], 'ERROR' : ['destroy'], 'DELETED' : ['destroy'], 'DESTROY' : ['destroy'], @@ -1687,20 +1796,24 @@ 'START' : ['destroy'], 'CONNECT' : ['destroy'], 'DISCONNECT' : ['destroy'], - 'RESIZE' : ['destroy'] + 'DETACH_VOLUME' : ['destroy'], + 'ATTACH_VOLUME' : ['destroy'], + 'RESIZE' : ['destroy'], + 'REASSIGN' : ['destroy'] } models.VM.AVAILABLE_ACTIONS_INACTIVE = {} // api status values models.VM.STATUSES = [ - 'UNKNWON', + 'UNKNOWN', 'BUILD', 'REBOOT', 'STOPPED', 'ACTIVE', 'ERROR', 'DELETED', + 'REASSIGN', 'RESIZE' ] @@ -1719,18 +1832,22 @@ 'CONNECT', 'DISCONNECT', 'FIREWALL', + 'DETACH_VOLUME', + 'ATTACH_VOLUME', + 'REASSIGN', 'RESIZE' ]); models.VM.STATES_TRANSITIONS = { 'DESTROY' : ['DELETED'], 'SHUTDOWN': ['ERROR', 'STOPPED', 'DESTROY'], - 'STOPPED': ['ERROR', 'ACTIVE', 'DESTROY'], - 'ACTIVE': ['ERROR', 'STOPPED', 'REBOOT', 'SHUTDOWN', 'DESTROY'], + 'STOPPED': ['ERROR', 'ACTIVE', 'DESTROY', 'RESIZE', 'REASSIGN'], + 'ACTIVE': ['ERROR', 'STOPPED', 'REBOOT', 'SHUTDOWN', 'DESTROY', 'REASSIGN'], 'START': ['ERROR', 'ACTIVE', 'DESTROY'], 'REBOOT': ['ERROR', 'ACTIVE', 'STOPPED', 'DESTROY'], 'BUILD': ['ERROR', 'ACTIVE', 'DESTROY'], - 'RESIZE': ['ERROR', 'STOPPED'] + 'RESIZE': ['ERROR', 'STOPPED'], + 'REASSIGN': ['ERROR', 'STOPPED', 'ACTIVE'] } models.VM.TRANSITION_STATES = [ @@ -1740,8 +1857,11 @@ 'REBOOT', 'BUILD', 'RESIZE', + 'REASSIGN', 'DISCONNECT', - 'CONNECT' + 'CONNECT', + 'ATTACH_VOLUME', + 'DETACH_VOLUME' ] models.VM.ACTIVE_STATES = [ @@ -1865,7 +1985,8 @@ }, comparator: function(img) { - return -img.get_sort_order("sortorder") || 0; + var date = new Date(img.get('created_at')); + return -img.get_sort_order("sortorder") || -date.getTime(); }, parse_meta: function(img) { @@ -1877,7 +1998,8 @@ }, active: function() { - return this.filter(function(img){return img.get('status') != "DELETED"}); + var active_states = ['available']; + return this.filter(function(img){return img.get('status') == "available"}); }, predefined: function() { @@ -1892,10 +2014,10 @@ }, get_images_for_type: function(type) { - if (this['get_{0}_images'.format(type)]) { - return this['get_{0}_images'.format(type)](); + var method = 'get_{0}_images'.format(type.replace("-", "_")); + if (this[method]) { + return this[method](); } - return this.active(); }, @@ -1930,9 +2052,11 @@ var url = getUrl.call(this) + "/" + id; this.api_call(this.path + "/" + id, "read", {_options:{async:false, skip_api_error:true}}, undefined, _.bind(function() { - this.add({id:id, cpu:"Unknown", ram:"Unknown", disk:"Unknown", name: "Unknown", status:"DELETED"}) + this.add({id:id, cpu:"Unknown", ram:"Unknown", disk:"Unknown", disk_template: "Unknown", name: "Unknown", status:"DELETED"}) }, this), _.bind(function(flv) { if (!flv.flavor.status) { flv.flavor.status = "DELETED" }; + flv.flavor.cpu = flv.flavor['vcpus']; + flv.flavor.disk_template = flv.flavor['SNF:disk_template']; this.add(flv.flavor); }, this)); }, @@ -2073,7 +2197,7 @@ // Do not apply task_state logic when machine is in ERROR state. // In that case only update from task_state only if equals to // DESTROY - if (data['task_state']) { + if (data['task_state'] !== undefined) { if (data['status'] != 'ERROR' && data['task_state'] != 'DESTROY') { status = models.VM.TASK_STATE_STATUS_MAP[data['task_state']]; if (status) { data['status'] = status } @@ -2165,7 +2289,7 @@ return vm_data.metadata && vm_data.metadata }, - create: function (name, image, flavor, meta, extra, callback) { + create: function (name, image, flavor, meta, project, extra, callback) { if (this.copy_image_meta) { if (synnefo.config.vm_image_common_metadata) { @@ -2183,11 +2307,11 @@ } opts = {name: name, imageRef: image.id, flavorRef: flavor.id, - metadata:meta} + metadata:meta, project: project.id} opts = _.extend(opts, extra); var cb = function(data) { - synnefo.storage.quotas.get('cyclades.vm').increase(); + synnefo.api.trigger("quota:update"); callback(data); } @@ -2260,11 +2384,13 @@ rename: function(new_name) { //this.set({'name': new_name}); + var self = this; this.sync("update", this, { critical: true, data: {'name': new_name}, success: _.bind(function(){ snf.api.trigger("call"); + this.set({'name': new_name}); }, this) }); }, @@ -2400,6 +2526,9 @@ models.Quota = models.Model.extend({ + storage_attrs: { + 'project_id': ['projects', 'project'] + }, initialize: function() { models.Quota.__super__.initialize.apply(this, arguments); @@ -2443,8 +2572,14 @@ return this.get('resource').get('unit') == 'bytes'; }, + infinite: function(active) { + var suffix = ''; + if (active) { suffix = '_active' } + return this.get("limit" + suffix) >= snf.util.PRACTICALLY_INFINITE; + }, + get_available: function(active) { - suffix = ''; + var suffix = ''; if (active) { suffix = '_active'} var value = this.get('limit'+suffix) - this.get('usage'+suffix); if (active) { @@ -2456,63 +2591,129 @@ return value }, - get_readable: function(key, active) { - var value; + get_readable: function(key, active, over_value) { if (key == 'available') { value = this.get_available(active); } else { value = this.get(key) } if (value <= 0) { value = 0 } - // greater than max js int (assume infinite quota) - if (value > Math.pow(2, 53)) { - return "Infinite" + + value = parseInt(value); + if (this.infinite()) { + return "Unlimited"; } if (!this.is_bytes()) { + if (over_value !== undefined) { return over_value + "" } return value + ""; } + value = over_value !== undefined ? over_value : value; return snf.util.readablizeBytes(value); } }); - + models.Quotas = models.Collection.extend({ model: models.Quota, api_type: 'accounts', path: 'quotas', supportIncUpdates: false, + + required_quota: { + 'vm': { + 'cyclades.vm': 1, + 'cyclades.ram': 1, + 'cyclades.cpu': 1, + 'cyclades.disk': 1 + }, + 'network': { + 'cyclades.network.private': 1 + }, + 'ip': { + 'cyclades.floating_ip': 1 + }, + 'volume': { + 'cyclades.disk': 1 + } + }, + parse: function(resp) { - filtered = _.map(resp.system, function(value, key) { - var available = (value.limit - value.usage) || 0; + var parsed = []; + _.each(resp, function(resources, uuid) { + parsed = _.union(parsed, _.map(resources, function(value, key) { + var quota_available = value.limit - value.usage || 0; + var total_available = quota_available; + var project_available = value.project_limit - value.project_usage || 0; + var limit = value.limit; + var usage = value.usage; + + var available = quota_available; + var total_available = available; + + // priority to project limits + if (project_available < available ) { + available = project_available; + limit = value.project_limit; + usage = value.project_usage; + total_available = available; + } + var available_active = available; + + // corresponding total quota var keysplit = key.split("."); - var limit_active = value.limit; - var usage_active = value.usage; - keysplit[keysplit.length-1] = "total_" + keysplit[keysplit.length-1]; - var activekey = keysplit.join("."); - var exists = resp.system[activekey]; - if (exists) { - available_active = exists.limit - exists.usage; - limit_active = exists.limit; - usage_active = exists.usage; + var last_part = keysplit.pop(); + var activekey = keysplit.join(".") + "." + "total_" + last_part; + var total = resp[uuid][activekey]; + if (total) { + total_available = total.limit - total.usage; + var total_project_available = total.project_limit - total.project_usage; + var total_limit = total.limit; + var total_usage = total.usage; + if (total_project_available < total_available) { + total_available = total_project_available; + total_limit = total.project_limit; + total_usage = total.project_usage; + } + if (total_available < available) { + available = total_available; + limit = total_limit; + usage = total_usage; + } } - return _.extend(value, {'name': key, 'id': key, + + var limit_active = limit; + var usage_active = usage; + + var id = uuid + ":" + key; + if (available < 0) { available = 0 } + return _.extend(value, { + 'name': key, + 'id': id, 'available': available, 'available_active': available_active, + 'total_available': total_available, 'limit_active': limit_active, + 'project_id': uuid, 'usage_active': usage_active, - 'resource': snf.storage.resources.get(key)}); + 'resource': snf.storage.resources.get(key) + }); + })); }); - return filtered; + return parsed; }, + project_key: function(project, key) { + return project + ":" + key; + }, + get_by_id: function(k) { return this.filter(function(q) { return q.get('name') == k})[0] }, get_available_for_vm: function(options) { - var quotas = synnefo.storage.quotas; + var quotas = this; var key = 'available'; var available_quota = {}; _.each(['cyclades.ram', 'cyclades.cpu', 'cyclades.disk'], @@ -2521,6 +2722,34 @@ available_quota[key.replace('cyclades.', '')] = value; }); return available_quota; + }, + + can_create: function(type) { + return this.get_available_projects(this.required_quota[type]).length > 0; + }, + + get_available_projects: function(quotas) { + return synnefo.storage.projects.filter(function(project) { + return project.quotas.can_fit(quotas); + }); + }, + + can_fit: function(quotas, total, _issues) { + var issues = []; + if (total === undefined) { total = false } + _.each(quotas, function(value, key) { + var q = this.get(key); + if (!q) { issues.push(key); return } + var quota = q.get('available_active'); + if (total) { + quota = q.get('total_available'); + } + if (quota < value) { + issues.push(key); + } + }, this); + if (_issues) { return issues } + return issues.length === 0; } }) @@ -2533,11 +2762,123 @@ api_type: 'accounts', path: 'resources', model: models.Network, + display_name_map: { + 'cyclades.vm': 'Machines', + 'cyclades.ram': 'Memory size', + 'cyclades.total_ram': 'Memory size (total)', + 'cyclades.cpu': 'CPUs', + 'cyclades.total_cpu': 'CPUs (total)', + 'cyclades.floating_ip': 'IP Addresses', + 'pithos.diskpace': 'Storage space', + 'cyclades.disk': 'Disk size', + 'cyclades.network.private': 'Private networks' + }, parse: function(resp) { return _.map(resp, function(value, key) { - return _.extend(value, {'name': key, 'id': key}); - }) + var display_name = this.display_name_map[key] || key; + return _.extend(value, { + 'name': key, + 'id': key, + 'display_name': display_name + }); + }, this); + } + }); + + models.ProjectQuotas = models.Quotas.extend({}); + _.extend(models.ProjectQuotas.prototype, + Backbone.FilteredCollection.prototype); + models.ProjectQuotas.prototype.get = function(key) { + key = this.project_id + ":" + key; + return models.ProjectQuotas.__super__.get.call(this, key); + } + + models.Project = models.Model.extend({ + api_type: 'accounts', + path: 'projects', + + initialize: function() { + var self = this; + this.quotas = new models.ProjectQuotas(undefined, { + collection: synnefo.storage.quotas, + collectionFilter: function(m) { + return self.id == m.get('project_id') + }}); + this.quotas.bind('change', function() { + self.trigger('change:_quotas'); + }); + this.quotas.project_id = this.id; + models.Project.__super__.initialize.apply(this, arguments); + } + }); + + models.Projects = models.Collection.extend({ + api_type: 'accounts', + path: 'projects', + model: models.Project, + supportIncUpdates: false, + user_project_uuid: null, + _resolving_missing: [], + + get: function(id) { + var project = models.Projects.__super__.get.call(this, id); + if (!project && id) { + if (_.contains(this._resolving_missing, id)) { return } + this._resolving_missing.push(id); + var missing_project = { + id: id, + name: '[missing project]', + missing: true + }; + this.add(missing_project); + this.update_unknown_id(id); + return this.get(id); + } + return project; + }, + + update_unknown_id: function(id) { + this.api_call([this.path, id].join("/") , this.read_method, { + _options:{ + async:false, + skip_api_error:true + }}, undefined, + _.bind(function() {}, this), + _.bind(function(project, msg, xhr) { + if (!project) { return } + var existing = this.get(id); + existing.set(project); + existing.set({'missing': true}); + existing.set({'resolved': true}); + }, this)); + }, + + url: function() { + var args = Array.prototype.splice.call(arguments, 0); + var url = models.Projects.__super__.url.apply(this, args); + return url + "?mode=member"; + }, + + parse: function(resp) { + _.each(resp, function(project){ + if (project.system_project) { + this.user_project_uuid = project.id; + } + if (project.id == synnefo.user.get_username()) { + project.name = "System project" + } + }, this); + return resp; + }, + + get_user_project: function() { + return this.get(synnefo.user.current_username); + }, + + comparator: function(project) { + if (project.get('system_project')) { return -100 } + return project.get('name'); } }); @@ -2548,6 +2889,13 @@ snf.storage.keys = new models.PublicKeys(); snf.storage.resources = new models.Resources(); snf.storage.quotas = new models.Quotas(); + snf.storage.projects = new models.Projects(); snf.storage.public_pools = new models.PublicPools(); - + + snf.storage.joined_projects = new Backbone.FilteredCollection(undefined, { + collection: synnefo.storage.projects, + collectionFilter: function(m) { + return m.get && !m.get("missing"); + } + }); })(this); diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/neutron.js b/snf-cyclades-app/synnefo/ui/static/snf/js/neutron.js index f0f90a54ebca289f7f876a2ad520a9722162fe87..f3978add2ca94d3ae0007db7ff778f52018d7a4f 100644 --- a/snf-cyclades-app/synnefo/ui/static/snf/js/neutron.js +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/neutron.js @@ -1,3 +1,19 @@ +// Copyright (C) 2010-2014 GRNET S.A. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. +// + ;(function(root){ // Neutron api models, collections, helpers @@ -59,6 +75,14 @@ // Network models.Network = models.NetworkModel.extend({ path: 'networks', + + url: function(options, method) { + var url = models.Network.__super__.url.call(this, method, options); + if (options.data && options.data.reassign) { + return url + '/action'; + } + return url; + }, parse: function(obj) { return obj.network; @@ -83,6 +107,7 @@ this.actions.reset_pending(); this.destroy({ success: _.bind(function() { + synnefo.api.trigger("quotas:call", 10); this.set({status: 'REMOVING'}); this.set({ext_status: 'REMOVING'}); // force status display update @@ -140,11 +165,13 @@ 'subnets': ['subnets', 'subnet', function(model, attr) { var subnets = model.get(attr); if (subnets && subnets.length) { return subnets[0] } - }] + }], + 'tenant_id': ['projects', 'project'] }, // call rename api rename: function(new_name, cb) { + var self = this; this.sync("update", this, { critical: true, data: { @@ -156,6 +183,7 @@ success: _.bind(function(){ //this.set({name: new_name}); snf.api.trigger("call"); + self.set({name: new_name}); }, this), complete: cb || function() {} }); @@ -244,7 +272,25 @@ this.pending_connections++; this.update_connecting_status(); synnefo.storage.ports.create(data, {complete: cb}); - } + }, + + reassign_to_project: function(project, success, cb) { + var project_id = project.id ? project.id : project; + var self = this; + var _success = function() { + success(); + self.set({'tenant_id': project_id}); + } + synnefo.api.sync('create', this, { + success: _success, + complete: cb, + data: { + reassign: { + project: project_id + } + } + }); + }, }); models.CombinedPublicNetwork = models.Network.extend({ @@ -304,7 +350,7 @@ }, get_floating_ips_network: function() { - return this.filter(function(n) { return n.get('is_public')})[1] + return this.filter(function(n) { return n.get('is_public') })[1] }, create_subnet: function(subnet_params, complete, error) { @@ -314,20 +360,26 @@ }); }, - create: function (name, type, cidr, dhcp, gateway, callback) { + create: function (project, name, type, cidr, dhcp, gateway, callback) { var quota = synnefo.storage.quotas; var params = {network:{name:name}}; var subnet_params = {subnet:{network_id:undefined}}; if (!type) { throw "Network type cannot be empty"; } params.network.type = type; + params.network.project = project.id; if (cidr) { subnet_params.subnet.cidr = cidr; } if (dhcp) { subnet_params.subnet.dhcp_enabled = dhcp; } if (dhcp === false) { subnet_params.subnet.dhcp_enabled = false; } - - subnet_params.subnet.gateway_ip = gateway || null; + + // api applies a gateway address automatically when gateway_ip + // parameter is missing + if (gateway !== "auto") { + subnet_params.subnet.gateway_ip = gateway || null; + } var cb = function() { + synnefo.api.trigger("quotas:call"); callback && callback(); } @@ -352,7 +404,7 @@ created_network.destroy({no_skip: true}); }); } - quota.get('cyclades.network.private').increase(); + project.quotas.get('cyclades.network.private').increase(); } return this.api_call(this.path, "create", params, complete, error, success); } @@ -504,12 +556,21 @@ models.FloatingIP = models.NetworkModel.extend({ path: 'floatingips', + + url: function(options, method) { + var url = models.FloatingIP.__super__.url.call(this, method, options); + if (options.data && options.data.reassign) { + return url + '/action'; + } + return url; + }, parse: function(obj) { return obj.floatingip; }, storage_attrs: { + 'tenant_id': ['projects', 'project'], 'port_id': ['ports', 'port'], 'floating_network_id': ['networks', 'network'], }, @@ -533,11 +594,30 @@ }] }, + reassign_to_project: function(project, success, cb) { + var project_id = project.id ? project.id : project; + var self = this; + var _success = function() { + success(); + self.set({'tenant_id': project_id}); + } + synnefo.api.sync('create', this, { + success: _success, + complete: cb, + data: { + reassign: { + project: project_id + } + } + }); + }, + do_remove: function(succ, err) { return this.do_destroy(succ, err) }, do_destroy: function(succ, err) { this.actions.reset_pending(); this.destroy({ success: _.bind(function() { + synnefo.api.trigger("quotas:call", 10); this.set({status: 'REMOVING'}); succ && succ(); }, this), @@ -552,6 +632,14 @@ }, proxy_attrs: { + '_status': [ + ['status', 'port', 'port.vm'], function() { + var status = this.get("status"); + var port = this.get("port"); + var vm = port && port.get("vm"); + return status + (vm ? vm.state() : ""); + } + ], 'ip': [ ['floating_ip_adress'], function() { return this.get('floating_ip_address'); @@ -594,6 +682,9 @@ path: 'floatingips', parse: function(resp) { return resp.floatingips; + }, + comparator: function(m) { + return parseInt(m.id); } }); diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/quota.js b/snf-cyclades-app/synnefo/ui/static/snf/js/quota.js index 4f847b9f5fd4f1bb02aa1353af6621c13316ef77..64629516a5c52907b304c18da1ebf5088af7c8a1 100644 --- a/snf-cyclades-app/synnefo/ui/static/snf/js/quota.js +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/quota.js @@ -1,35 +1,17 @@ -// Copyright 2013 GRNET S.A. All rights reserved. -// -// Redistribution and use in source and binary forms, with or -// without modification, are permitted provided that the following -// conditions are met: -// -// 1. Redistributions of source code must retain the above -// copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following -// disclaimer in the documentation and/or other materials -// provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// -// The views and conclusions contained in the software and -// documentation are those of the authors and should not be -// interpreted as representing official policies, either expressed -// or implied, of GRNET S.A. +// Copyright (C) 2010-2014 GRNET S.A. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. // ;(function(root){ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/sync.js b/snf-cyclades-app/synnefo/ui/static/snf/js/sync.js index 85574dcfaabb02518a7561b5f38093a9861ee3cf..1921999ac8bc8b0090007c6cbf3e8816c914b5b2 100644 --- a/snf-cyclades-app/synnefo/ui/static/snf/js/sync.js +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/sync.js @@ -1,35 +1,17 @@ -// Copyright 2011 GRNET S.A. All rights reserved. -// -// Redistribution and use in source and binary forms, with or -// without modification, are permitted provided that the following -// conditions are met: -// -// 1. Redistributions of source code must retain the above -// copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following -// disclaimer in the documentation and/or other materials -// provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// -// The views and conclusions contained in the software and -// documentation are those of the authors and should not be -// interpreted as representing official policies, either expressed -// or implied, of GRNET S.A. +// Copyright (C) 2010-2014 GRNET S.A. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. // ;(function(root){ @@ -536,6 +518,16 @@ api.STATES = { NORMAL:1, WARN:0, ERROR:-1 }; api.error_state = api.STATES.NORMAL; + api.bind("quota:update", function(delay) { + if (delay == undefined) { + delay = 0 + } + + window.setTimeout(function() { + synnefo.storage.quotas.fetch({refresh: true}); + }, delay); + }); + // on api error update the api error_state api.bind("error", function() { if (snf.api.error_state == snf.api.STATES.ERROR) { return }; diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/synnefo.js b/snf-cyclades-app/synnefo/ui/static/snf/js/synnefo.js index eeb3afd1c55e4b92ceb8ee2bbe8bd7e8235b6b64..860db27986793a315e2e626ab235ef81dca08d53 100644 --- a/snf-cyclades-app/synnefo/ui/static/snf/js/synnefo.js +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/synnefo.js @@ -1,36 +1,18 @@ // -// Copyright 2011 GRNET S.A. All rights reserved. +// Copyright (C) 2010-2014 GRNET S.A. // -// Redistribution and use in source and binary forms, with or -// without modification, are permitted provided that the following -// conditions are met: +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. // -// 1. Redistributions of source code must retain the above -// copyright notice, this list of conditions and the following -// disclaimer. +// 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 General Public License for more details. // -// 2. Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following -// disclaimer in the documentation and/or other materials -// provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// -// The views and conclusions contained in the software and -// documentation are those of the authors and should not be -// interpreted as representing official policies, either expressed -// or implied, of GRNET S.A. +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. // var API_URL = "/api/v1.1"; var changes_since = 0, deferred = 0, update_request = false, load_request = false, pending_actions = []; @@ -308,46 +290,6 @@ function get_short_v6(v6, parts_to_keep) { return new_parts.join(":"); } -function fix_v6_addresses() { - - // what to prepend - var match = "..."; - // long ip min length - var limit = 20; - // parts to show after the transformation - // (from the end) - var parts_to_keep_from_end = 4; - - $(".machine .ipv6-text").each(function(index, el){ - var el = $(el); - var ip = $(el).text(); - - // transformation not applyied - // FIXME: use $.data for the condition - if (ip.indexOf(match) == -1 && ip != "pending") { - - // only too long ips - if (ip.length > 20) { - $(el).data("ipstring", ip); - $(el).text(match + get_short_v6(ip, parts_to_keep_from_end)); - $(el).attr("title", ip); - $(el).tooltip({'tipClass':'tooltip ipv6-tip', 'position': 'center center'}); - } - } else { - if (ip.indexOf(match) == 0) { - } else { - // not a long ip anymore - $(el).data("ipstring", undefined); - $(el).css({'text-decoration':'none'}); - - if ($(el).data('tooltip')) { - $(el).data('tooltip').show = function () {}; - } - } - } - }); -} - // get stats function get_server_stats(serverID) { diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/tests.js b/snf-cyclades-app/synnefo/ui/static/snf/js/tests.js index 3c09b6fe486956586670e4b8bd327b77cb3ef226..b7e11f0a46973def7e673ad822010e23ae20ab70 100644 --- a/snf-cyclades-app/synnefo/ui/static/snf/js/tests.js +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/tests.js @@ -1,35 +1,17 @@ -// Copyright 2011 GRNET S.A. All rights reserved. -// -// Redistribution and use in source and binary forms, with or -// without modification, are permitted provided that the following -// conditions are met: -// -// 1. Redistributions of source code must retain the above -// copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following -// disclaimer in the documentation and/or other materials -// provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// -// The views and conclusions contained in the software and -// documentation are those of the authors and should not be -// interpreted as representing official policies, either expressed -// or implied, of GRNET S.A. +// Copyright (C) 2010-2014 GRNET S.A. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. // $(document).ready(function(){ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/tests/functional.js b/snf-cyclades-app/synnefo/ui/static/snf/js/tests/functional.js index ff540191827f2fc87f655075944da660dc5d977e..b47beecf725053f355a9b3ab75f259e7d19753ff 100644 --- a/snf-cyclades-app/synnefo/ui/static/snf/js/tests/functional.js +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/tests/functional.js @@ -1,35 +1,17 @@ -// Copyright 2011 GRNET S.A. All rights reserved. -// -// Redistribution and use in source and binary forms, with or -// without modification, are permitted provided that the following -// conditions are met: -// -// 1. Redistributions of source code must retain the above -// copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following -// disclaimer in the documentation and/or other materials -// provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// -// The views and conclusions contained in the software and -// documentation are those of the authors and should not be -// interpreted as representing official policies, either expressed -// or implied, of GRNET S.A. +// Copyright (C) 2010-2014 GRNET S.A. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. // // shortcut helpers on global context diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/ie_fixes.js b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/ie_fixes.js index 4e4a049ff2581c8d233a7dc23584402bf6f02f01..cf973d50cfa5fcd2f4fb1a97eefd783528d00d58 100644 --- a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/ie_fixes.js +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/ie_fixes.js @@ -1,35 +1,17 @@ -// Copyright 2011 GRNET S.A. All rights reserved. -// -// Redistribution and use in source and binary forms, with or -// without modification, are permitted provided that the following -// conditions are met: -// -// 1. Redistributions of source code must retain the above -// copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following -// disclaimer in the documentation and/or other materials -// provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// -// The views and conclusions contained in the software and -// documentation are those of the authors and should not be -// interpreted as representing official policies, either expressed -// or implied, of GRNET S.A. +// Copyright (C) 2010-2014 GRNET S.A. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. // (function(){ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_connect_view.js b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_connect_view.js index bfe2f9faefce460c9e6376b461a74bfe690fbf43..f36e3db6c6d80e572aa8f105978a22dba2e703fb 100644 --- a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_connect_view.js +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_connect_view.js @@ -1,35 +1,17 @@ -// Copyright 2011 GRNET S.A. All rights reserved. -// -// Redistribution and use in source and binary forms, with or -// without modification, are permitted provided that the following -// conditions are met: -// -// 1. Redistributions of source code must retain the above -// copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following -// disclaimer in the documentation and/or other materials -// provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// -// The views and conclusions contained in the software and -// documentation are those of the authors and should not be -// interpreted as representing official policies, either expressed -// or implied, of GRNET S.A. +// Copyright (C) 2010-2014 GRNET S.A. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. // ;(function(root){ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_create_view.js b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_create_view.js index 0f06ee2453c7848bb70df53c4287f573cb009cd4..aac4df1361b06713ac24fa9e18aaa1a9ea221185 100644 --- a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_create_view.js +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_create_view.js @@ -1,35 +1,17 @@ -// Copyright 2011 GRNET S.A. All rights reserved. -// -// Redistribution and use in source and binary forms, with or -// without modification, are permitted provided that the following -// conditions are met: -// -// 1. Redistributions of source code must retain the above -// copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following -// disclaimer in the documentation and/or other materials -// provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// -// The views and conclusions contained in the software and -// documentation are those of the authors and should not be -// interpreted as representing official policies, either expressed -// or implied, of GRNET S.A. +// Copyright (C) 2010-2014 GRNET S.A. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. // ;(function(root){ @@ -49,6 +31,119 @@ // shortcuts var bb = root.Backbone; + var min_vm_quota = { + 'cyclades.vm': 1, + 'cyclades.ram': 1, + 'cyclades.cpu': 1, + 'cyclades.disk': 1 + }; + + views.CreateVMSelectProjectItemView = views.ext.SelectModelView.extend({ + tpl: '#create-view-select-project-item-tpl', + can_deselect: false, + display_quota: min_vm_quota, + quotas_option_html: function() { + var data = ""; + _.each(this.display_quota, function(val, key) { + var q = this.model.quotas.get(key); + if (!q) { return } + var content = '<span class="resource">' + + '<span class="key">{0}:</span>' + + '<span class="value">{1}</span>' + + '</span>'; + data += content.format(q.get('resource').get('display_name'), + q.get_readable('available')); + }, this); + data = data.substring(0, data.length-3); + if (data) { + data += ""; + } + return data; + } + }); + + views.CreateVMSelectProjectView = views.ext.CollectionSelectView.extend({ + tpl: '#create-view-projects-select-tpl', + select2_params: {}, + model_view_cls: views.CreateVMSelectProjectItemView, + required_quota: function() { + return min_vm_quota + }, + + _select2_format_result: function(state) { + return $(state.element).html(); + }, + + _select2_format_selection: function(state) { + return $(state.element).html(); + }, + + post_init: function() { + this._select = $(this.el).find("select"); + this._select.addClass("project-select") + this._select.select2( + _.extend({}, { + width: "100%", + formatResult: this._select2_format_result, + formatSelection: this._select2_format_selection + }, this.select2_params)); + views.CreateVMSelectProjectView.__super__.post_init.apply(this, arguments); + }, + + set_current: function(model) { + if (!this._model_views[model.id]) { return } + var view = this._model_views[model.id]; + view.select(); + this._select.select2("val", view.el.attr("value")); + }, + + hide: function() { + this._select.select2("close"); + views.CreateVMSelectProjectView.__super__.hide.apply(this, arguments); + }, + + init: function() { + this.handle_quota_changed = _.bind(this.handle_quota_changed, this); + views.CreateVMSelectProjectView.__super__.init.apply(this, arguments); + }, + + handle_quota_changed: function() { + _.each(this._model_views, function(view) { + if (!view.model.quotas.can_fit(this.required_quota())) { + view.set_disabled(); + } else { + view.set_enabled(); + } + view.model.trigger("change:_quota"); + }, this); + // force select2 to update data + this._select.data().select2.search.trigger("keyup-change"); + }, + + set_handlers: function() { + var self = this; + synnefo.storage.quotas.bind("change", this.handle_quota_changed); + this.el.bind("change", function() { + var view = self._model_views[self.list_el.val()]; + self.deselect_all(); + view.delegate_checked = false; + view.select(); + view.trigger('selected', view); + }); + views.CreateVMSelectProjectView.__super__.set_handlers.apply(this, arguments); + }, + + remove_handlers: function() { + synnefo.storage.quotas.unbind("change", this.handle_quota_changed); + this.el.unbind("change"); + views.CreateVMSelectProjectView.__super__.remove_handlers.apply(this, arguments); + }, + + post_show: function() { + views.CreateVMSelectProjectView.__super__.post_show.apply(this, arguments); + this.handle_quota_changed(); + } + }); views.VMCreationPasswordView = views.Overlay.extend({ view_id: "creation_password_view", @@ -72,10 +167,11 @@ } this.hide(); }, this)); - + + var self = this; _.bindAll(this, "handle_vm_added"); storage.vms.bind("add", this.handle_vm_added); - this.password.text(""); + this.password.val(""); }, handle_vm_added: function() { @@ -84,16 +180,17 @@ show_password: function() { this.$(".show-machine").addClass("in-progress"); - this.password.text(this.pass); + this.password.val(this.pass); if (storage.vms.get(this.vm_id)) { this.$(".show-machine").removeClass("in-progress"); } this.clip = new snf.util.ClipHelper(this.copy, this.pass); + }, onClose: function() { - this.password.text(""); + this.password.val(""); this.vm_id = undefined; try { delete this.clip; } catch (err) {}; }, @@ -109,14 +206,18 @@ show: function(pass, vm_id) { this.pass = pass; this.vm_id = vm_id; - + var self = this; + this.password.unbind("click").click(function() { + self.password.selectRange(0); + }); + views.VMCreationPasswordView.__super__.show.apply(this, arguments); } }) - views.CreateVMStepView = views.View.extend({ + views.CreateWizardStepView = views.View.extend({ step: "1", title: "Image", submit: false, @@ -127,7 +228,11 @@ this.header = this.$(".step-header .step-" + this.step); this.view_id = "create_step_" + this.step; - views.CreateVMStepView.__super__.initialize.apply(this); + views.CreateWizardStepView.__super__.initialize.apply(this); + }, + + get_project: function() { + return this.parent.project; }, show: function() { @@ -140,9 +245,13 @@ reset: function() { } - }) + }); + + views.CreateVMStepView = views.CreateWizardStepView; views.CreateImageSelectView = views.CreateVMStepView.extend({ + + default_type: 'system', initialize: function() { views.CreateImageSelectView.__super__.initialize.apply(this, arguments); @@ -162,8 +271,8 @@ this.categories_list = this.$(".category-filters"); // params initialization - this.type_selections = {"system": "System"}; - this.type_selections_order = ['system']; + this.type_selections = this.type_selections || {"system": "System"}; + this.type_selections_order = this.type_selections_order || ['system']; this.images_storage = snf.storage.images; @@ -172,7 +281,6 @@ this.type_selections = _.extend( this.images_storage.type_selections, this.type_selections) - this.type_selections_order = this.images_storage.type_selections_order; } @@ -185,6 +293,11 @@ // handlers initialization this.create_types_selection_options(); + if (synnefo.config.snapshots_enabled) { + this.create_snapshot_types_selection_options(); + } else { + this.$(".snapshot-types-cont").hide(); + } this.init_handlers(); this.init_position(); }, @@ -213,21 +326,43 @@ }) $(".image-warning .confirm").bind('click', function(){ - $(".image-warning").hide(); - $(".create-controls").show(); - }) + self.parent.el.find(".image-warning").hide(); + self.parent.el.find(".create-controls").show(); + if (!self.parent.project) { + self.parent.set_no_project(); + } + }); }, update_images: function(images) { this.images = images; - this.images_ids = _.map(this.images, function(img){return img.id}); + var filtered_images = _.filter(images, function(img) { + return img.is_available() + }); + this.images_ids = _.map(filtered_images, function(img) { + return img.id + }); return this.images; }, create_types_selection_options: function() { - var list = this.$("ul.type-filter"); + var list = this.$(".image-types-cont ul.type-filter"); + list.empty(); _.each(this.type_selections_order, _.bind(function(key) { - list.append('<li id="type-select-{0}">{1}</li>'.format(key, this.type_selections[key])); + list.append('<li id="type-select-{0}">{1}</li>'.format( + key, this.type_selections[key])); + }, this)); + this.types = this.$(".type-filter li"); + }, + + create_snapshot_types_selection_options: function() { + var exclude = []; + var list = this.$(".snapshot-types-cont ul.type-filter"); + list.empty(); + _.each(this.type_selections_order, _.bind(function(key) { + if (_.includes(exclude, key)) { return } + var label = this.type_selections[key].replace("images", "snapshots"); + list.append('<li id="type-select-snapshot-{0}">{1}</li>'.format(key, label)); }, this)); this.types = this.$(".type-filter li"); }, @@ -254,12 +389,13 @@ this.categories_list.append(el); }, this)); + var empty = this.categories_list.parent().find(".empty"); if (!categories.length) { this.categories_list.parent().find(".clear").hide(); - this.categories_list.parent().find(".empty").show(); + empty.show(); } else { this.categories_list.parent().find(".clear").show(); - this.categories_list.parent().find(".empty").hide(); + empty.hide(); } }, @@ -280,7 +416,11 @@ this.reset_categories(); this.update_images(images); this.reset_images(); - this.select_image(this.selected_image); + var to_select = this.selected_image; + if (!_.contains(this.images_ids, this.selected_image && this.selected_image.get("id"))) { + to_select = this.images.length && this.images[0]; + } + this.select_image(to_select); this.hide_list_loading(); $(".custom-image-help").hide(); if (this.selected_type == 'personal' && !images.length) { @@ -292,14 +432,24 @@ select_type: function(type) { this.selected_type = type; this.types.removeClass("selected"); + var selection = "#type-select-" + this.selected_type; this.types.filter("#type-select-" + this.selected_type).addClass("selected"); + if (!type) { return } this.images_storage.update_images_for_type( this.selected_type, _.bind(this.show_loading_view, this), _.bind(this.hide_loading_view, this) ); - this.update_layout_for_type(type); + this.selected_type_el = this.types.filter(".selected").closest("ul").parent(); + this.update_type_messages(this.selected_type_el); + }, + + update_type_messages: function(el) { + var empty = this.images_list.parent().find(".empty"); + empty.text(el.data("list-empty")); + var heading = this.images_list.parent().find("h4"); + heading.text(el.data("list-title")); }, update_layout_for_type: function(type) { @@ -320,16 +470,20 @@ }, display_warning_for_image: function(image) { - if (image && !image.is_system_image() && !image.owned_by(synnefo.user)) { - $(".create-vm .image-warning").show(); - $(".create-controls").hide(); + if (image && !image.is_system_image() && + !image.owned_by(synnefo.user)) { + this.parent.el.find(".image-warning").show(); + this.parent.el.find(".create-controls").hide(); } else { - $(".create-vm .image-warning").hide(); - $(".create-controls").show(); + this.parent.el.find(".image-warning").hide(); + this.parent.el.find(".create-controls").show(); } }, select_image: function(image) { + if (image && !image.is_available()) { + image = undefined; + } if (image && image.get('id') && !_.include(this.images_ids, image.get('id'))) { image = undefined; } @@ -351,8 +505,11 @@ this.selected_image = image; if (image) { - this.images_list.find(".image-details").removeClass("selected"); - this.images_list.find(".image-details#create-vm-image-" + this.selected_image.id).addClass("selected"); + this.images_list.find( + ".image-details").removeClass("selected"); + this.images_list.find( + ".image-details#create-vm-image-" + + this.selected_image.id).addClass("selected"); this.update_image_details(image); } else { @@ -361,13 +518,17 @@ this.image_details.hide(); this.validate(); }, + + get_image_icon_tag: function(image) { + return snf.ui.helpers.os_icon_tag(image.escape("OS")); + }, update_image_details: function(image) { this.image_details_desc.hide().parent().hide(); if (image.get_description()) { this.image_details_desc.html(image.get_description(false)).show().parent().show(); } - var img = snf.ui.helpers.os_icon_tag(image.escape("OS")) + var img = this.get_image_icon_tag(image); if (image.get("name")) { this.image_details_title.html(img + image.escape("name")).show().parent().show(); } @@ -435,11 +596,12 @@ this.add_image(img); }, this)) + var empty = this.images_list.parent().find(".empty"); if (this.images.length) { - this.images_list.parent().find(".empty").hide(); + empty.hide(); this.images_list.show(); } else { - this.images_list.parent().find(".empty").show(); + empty.show(); this.images_list.hide(); } @@ -447,39 +609,51 @@ this.images_list.find(".image-details").click(function(){ self.select_image($(this).data("image")); }); - }, show: function() { this.image_details.hide(); this.parent.$(".create-controls").show(); - views.CreateImageSelectView.__super__.show.apply(this, arguments); }, add_image: function(img) { - var image = $(('<li id="create-vm-image-{1}"' + - 'class="image-details clearfix">{2}{0}'+ - '<span class="show-details">details</span>'+ - '<span class="size"><span class="prepend">by </span>{5}</span>' + - '<span class="owner">' + - '<span class="prepend"></span>' + - '{3}</span>' + - '<p>{4}</p>' + - '</li>').format(img.escape("name"), - img.id, - snf.ui.helpers.os_icon_tag(img.escape("OS")), - _.escape(img.get_readable_size()), - util.truncate(img.get_description(false), 35), - _.escape(img.display_owner()))); + var description = util.truncate(img.get_description(false), 35); + if (!img.is_available()) { + description = "Image not available." + } + var image_html = '<li id="create-vm-image-{1}"' + + 'class="image-details clearfix">{2}{0}'+ + '<span class="show-details">details</span>'+ + '<span class="size"><span class="prepend">by ' + + '</span>{5}</span>' + + '<span class="owner">' + + '<span class="prepend"></span>' + + '{3}</span>' + + '<p>{4}</p>' + + '</li>'; + + var icon_tag = this.get_image_icon_tag(img); + var image = $(image_html.format( + _.escape(util.truncate(img.get("name"), 50)), + img.id, + icon_tag, + _.escape(img.get_readable_size()), + description, + _.escape(img.display_owner()) + )); + image.data("image", img); image.data("image_id", img.id); + + if (!img.is_available()) { image.addClass("disabled"); } + this.images_list.append(image); image.find(".show-details").click(_.bind(function(e){ e.preventDefault(); e.stopPropagation(); this.show_image_details(img); - }, this)) + }, this)); }, hide_image_details: function() { @@ -495,7 +669,7 @@ reset: function() { this.selected_image = false; - this.select_type("system"); + this.select_type(this.default_type); }, get: function() { @@ -510,12 +684,13 @@ } } }); - + views.CreateFlavorSelectView = views.CreateVMStepView.extend({ step: 2, initialize: function() { views.CreateFlavorSelectView.__super__.initialize.apply(this, arguments); this.parent.bind("image:change", _.bind(this.handle_image_change, this)); + this.parent.bind("project:change", _.bind(this.handle_project_change, this)); this.cpus = this.$(".flavors-cpu-list"); this.disks = this.$(".flavors-disk-list"); @@ -530,9 +705,58 @@ }, this)); this.predefined = this.$(".predefined-list"); + this.projects_list = this.$(".project-select"); + this.project_select_view = undefined; + }, + + init_subviews: function() { + if (!this.project_select_view) { + this.project_select_view = new views.CreateVMSelectProjectView({ + container: this.projects_list, + collection: synnefo.storage.joined_projects, + parent_view: this + }); + this.project_select_view.show(true); + this.project_select_view.bind("change", + _.bind(this.handle_project_select, + this)) + } + this.project_select_view.set_current(this.parent.project); + this.handle_project_select(this.parent.project); + }, + + hide: function() { + this.hide_step(); + }, + + hide_step: function() { + this.project_select_view && this.project_select_view.hide(true); + }, + + handle_project_select: function(projects) { + if (!projects.length ) { return } + var project = projects[0]; + this.parent.set_project(project); + }, + + handle_project_change: function() { + if (!this.parent.project) { return } + this.update_valid_predefined(); + this.update_flavors_data(); + this.update_predefined_flavors(); + this.reset_flavors(); + this.update_layout(); + }, + + show: function() { + var args = _.toArray(arguments); + this.init_subviews(); + this.project_select_view.show(); + views.CreateFlavorSelectView.__super__.show.call(this, args); }, handle_image_change: function(data) { + if (!this.parent.project) { return } this.current_image = data; this.update_valid_predefined(); this.current_flavor = undefined; @@ -628,7 +852,7 @@ } // quota check - var quotas = synnefo.storage.quotas.get_available_for_vm(); + var quotas = this.get_project().quotas.get_available_for_vm(); var unavailable_check = synnefo.storage.flavors.unavailable_values_for_quotas; var unavailable = unavailable_check(quotas, [existing]); @@ -639,7 +863,7 @@ } return key; - }), function(ret) { return ret }); + }, this), function(ret) { return ret }); $("li.predefined-selection").addClass("disabled"); _.each(this.valid_predefined, function(key) { @@ -662,6 +886,7 @@ }, update_flavors_data: function() { + if (!this.parent.project) { return } this.flavors = this.get_active_flavors(); this.flavors_data = storage.flavors.get_data(this.flavors); @@ -692,8 +917,8 @@ if (this.current_image) { image_excluded = storage.flavors.unavailable_values_for_image(this.current_image); } - - var quotas = synnefo.storage.quotas.get_available_for_vm({active: true}); + + var quotas = this.get_project().quotas.get_available_for_vm({active: true}); var user_excluded = storage.flavors.unavailable_values_for_quotas(quotas); unavailable.disk = user_excluded.disk.concat(image_excluded.disk); @@ -922,7 +1147,6 @@ values.disk_template); this.disk_templates.append(disk_template); - //disk_template.tooltip({position:'top center', offset:[-5,0], delay:100, tipClass:'tooltip disktip'}); this.__added_flavors.disk_template.push(values.disk_template) } @@ -948,7 +1172,7 @@ update_quota_display: function() { - var quotas = synnefo.storage.quotas; + var quotas = this.get_project().quotas; _.each(["disk", "ram", "cpu"], function(type) { var active = true; var key = 'available'; @@ -1205,7 +1429,8 @@ var create_view = this.parent; if (!this.networks_view) { this.networks_view = new views.NetworkSelectView({ - container: this.cont + container: this.cont, + project: this.get_project() }); this.networks_view.hide(true); } @@ -1451,6 +1676,7 @@ this.name = this.$("h3.vm-name"); this.keys = this.$(".confirm-params.ssh"); this.meta = this.$(".confirm-params.meta"); + this.project = this.$(".confirm-cont.image .project-name"); this.ip_addresses = this.$(".confirm-params.ip-addresses"); this.private_networks = this.$(".confirm-params.private-networks"); this.init_handlers(); @@ -1472,11 +1698,19 @@ this.ip_addresses.empty(); if (!ips|| ips.length == 0) { this.ip_addresses.append(this.make("li", {'class':'empty'}, - 'No ip addresses selected')) + 'No IP addresses selected')) } _.each(ips, _.bind(function(ip) { + var ip_address = $('<span class="ip"></span>'); + ip_address.text(ip.get('floating_ip_address')); + var el = this.make("li", {'class':'selected-ip-address'}, - ip.get('floating_ip_address')); + ip_address); + var project_name = ip.get('project').get('name'); + project_name = util.truncate(project_name, 30); + var name = $('<span class="project"></span>'); + $(name).text(project_name); + $(el).append(name); this.ip_addresses.append(el); }, this)) @@ -1572,6 +1806,10 @@ this.update_image_details(); this.update_flavor_details(); this.update_network_details(); + + var project_name = this.get_project().get('name'); + project_name = util.truncate(project_name, 25); + this.project.text(project_name); if (!params.image.supports('ssh')) { this.keys.hide(); @@ -1597,41 +1835,92 @@ } }); - views.CreateVMView = views.Overlay.extend({ + views.VMCreateView = views.Overlay.extend({ view_id: "create_vm_view", content_selector: "#createvm-overlay-content", - css_class: 'overlay-createvm overlay-info', + css_class: 'overlay-wizard overlay-info', overlay_id: "metadata-overlay", subtitle: false, title: "Create new machine", + min_quota: min_vm_quota, initialize: function(options) { - views.CreateVMView.__super__.initialize.apply(this); + views.VMCreateView.__super__.initialize.apply(this); this.current_step = 1; - - this.password_view = new views.VMCreationPasswordView(); - + this.steps = []; - this.steps[1] = new views.CreateImageSelectView(this); - this.steps[1].bind("change", _.bind(function(data) {this.trigger("image:change", data)}, this)); - - this.steps[2] = new views.CreateFlavorSelectView(this); - this.steps[3] = new views.CreateNetworkingView(this); - this.steps[4] = new views.CreatePersonalizeView(this); - this.steps[5] = new views.CreateSubmitView(this); + this.setup_step_views(); this.cancel_btn = this.$(".create-controls .cancel"); this.next_btn = this.$(".create-controls .next"); this.prev_btn = this.$(".create-controls .prev"); + this.no_project_notice = this.$(".no-project-notice"); this.submit_btn = this.$(".create-controls .submit"); this.history = this.$(".steps-history"); this.history_steps = this.$(".steps-history .steps-history-step"); + + this.loading_view = $("<div>Loading images...</div>"); + this.$(".container").after(this.loading_view); + this.loading_view.css({ + backgroundColor: "#97C3D6", + padding: '15px', + fontSize: '0.7em', + color: '#333' + }); this.init_handlers(); }, + + setup_step_views: function() { + this.password_view = new views.VMCreationPasswordView(); + this.steps[1] = new views.CreateImageSelectView(this); + this.steps[1].bind("change", _.bind(function(data) { + this.trigger("image:change", data) + }, this)); + + this.steps[2] = new views.CreateFlavorSelectView(this); + this.steps[3] = new views.CreateNetworkingView(this); + this.steps[4] = new views.CreatePersonalizeView(this); + this.steps[5] = new views.CreateSubmitView(this); + + }, + + get_available_project: function() { + var project = undefined; + var user_project = synnefo.storage.projects.get_user_project(); + if (user_project && user_project.quotas.can_fit(this.min_quota)) { + project = user_project; + } + if (!project) { + synnefo.storage.projects.each(function(p) { + if (p.quotas.can_fit(this.min_quota)) { + project = p; + } + }, this); + } + return project; + }, + + set_project: function(project) { + var trigger = false; + if (project != this.project) { + trigger = true; + } + this.project = project; + if (trigger) { this.trigger("project:change", project)} + this.check_project_is_set(); + }, + + check_project_is_set: function() { + if (!this.project) { + this.set_no_project(); + } else { + this.unset_no_project(); + } + }, init_handlers: function() { var self = this; @@ -1699,10 +1988,11 @@ 'fixed_ip': ip.get('floating_ip_address') }); }); - + _.map(data.networks, function(n) { return n.get('id') }); storage.vms.create(data.name, data.image, data.flavor, - meta, extra, _.bind(function(data){ + meta, this.project, extra, + _.bind(function(data) { _.each(data.addresses, function(ip) { ip.set({'status': 'connecting'}); }); @@ -1722,21 +2012,18 @@ }, onClose: function() { - this.steps[3].remove(); + this.current_view && this.current_view.hide && this.current_view.hide(true); + if (this.steps && this.steps[3]) { + this.steps[3].remove(); + } }, reset: function() { this.current_step = 1; - - this.steps[1].reset(); - this.steps[2].reset(); - this.steps[3].reset(); - this.steps[4].reset(); - - //this.steps[1].show(); - //this.steps[2].show(); - //this.steps[3].show(); - //this.steps[4].show(); + + _.each(this.steps, function(s) { + s.reset(); + }); this.submit_btn.removeClass("in-progress"); }, @@ -1745,11 +2032,24 @@ }, update_layout: function() { + this.check_project_is_set(); this.show_step(this.current_step); this.current_view.update_layout(); }, + + set_no_project: function() { + this.next_btn.hide(); + this.no_project_notice.show(); + }, + + unset_no_project: function() { + this.next_btn.show(); + this.no_project_notice.hide(); + }, beforeOpen: function() { + var project = this.get_available_project(); + this.set_project(project); if (!this.skip_reset_on_next_open) { this.submiting = false; this.reset(); @@ -1757,9 +2057,18 @@ this.$(".steps-container").css({"margin-left":0 + "px"}); this.show_step(1); } - + + this.loading_view.show(); + this.$(".container").hide(); + var complete = _.bind(function() { + this.loading_view.hide(); + this.$(".container").slideDown(); + this.update_layout(); + }, this); + + synnefo.storage.images.fetch({complete: complete}); + this.skip_reset_on_next_open = false; - this.update_layout(); }, set_step: function(step) { @@ -1773,8 +2082,6 @@ }, show_step: function(step) { - // FIXME: this shouldn't be here - // but since we are not calling step.hide this should work this.steps[1].image_details.hide(); this.current_view && this.current_view.hide_step && this.current_view.hide_step(); @@ -1822,13 +2129,17 @@ this.next_btn.hide(); this.submit_btn.show(); } else { - this.next_btn.show(); + this.check_project_is_set(); this.submit_btn.hide(); } }, get_params: function() { - return _.extend({}, this.steps[1].get(), this.steps[2].get(), this.steps[3].get(), this.steps[4].get()); + var params = {}; + _.each(this.steps, function(s) { + _.extend(params, s.get()); + }); + return params; } }); diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_disks_view.js b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_disks_view.js deleted file mode 100644 index fbb8ea47c7ce41e7ff3714a02ff06bf08c471594..0000000000000000000000000000000000000000 --- a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_disks_view.js +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2011 GRNET S.A. All rights reserved. -// -// Redistribution and use in source and binary forms, with or -// without modification, are permitted provided that the following -// conditions are met: -// -// 1. Redistributions of source code must retain the above -// copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following -// disclaimer in the documentation and/or other materials -// provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// -// The views and conclusions contained in the software and -// documentation are those of the authors and should not be -// interpreted as representing official policies, either expressed -// or implied, of GRNET S.A. -// - -;(function(root){ - - // root - var root = root; - - // setup namepsaces - var snf = root.synnefo = root.synnefo || {}; - var models = snf.models = snf.models || {} - var storage = snf.storage = snf.storage || {}; - var ui = snf.ui = snf.ui || {}; - var util = snf.util || {}; - var views = snf.views = snf.views || {} - - // shortcuts - var bb = root.Backbone; - - // logging - var logger = new snf.logging.logger("SNF-VIEWS"); - var debug = _.bind(logger.debug, logger); - - views.DisksView = views.View.extend({ - - view_id: "disks", - pane: "#disks-pane", - el: "#disks-pane", - - initialize: function() { - - }, - - __update_layout: function() { - - } - }); - -})(this); diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_error_view.js b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_error_view.js index 8dd7245f570e101c9c83bf4eecfc7f98b350d302..e9441033d0aab26d12c8210b593a6a2b1e29758c 100644 --- a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_error_view.js +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_error_view.js @@ -1,35 +1,17 @@ -// Copyright 2011 GRNET S.A. All rights reserved. -// -// Redistribution and use in source and binary forms, with or -// without modification, are permitted provided that the following -// conditions are met: -// -// 1. Redistributions of source code must retain the above -// copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following -// disclaimer in the documentation and/or other materials -// provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// -// The views and conclusions contained in the software and -// documentation are those of the authors and should not be -// interpreted as representing official policies, either expressed -// or implied, of GRNET S.A. +// Copyright (C) 2010-2014 GRNET S.A. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. // ;(function(root){ @@ -216,7 +198,8 @@ this.details = details ? (details.toString ? details.toString() : details) : undefined; this.message = message; this.api_message = api_message; - this.title = _.escape(error_options.title) || undefined; + this.title = error_options.title ? + _.escape(error_options.title) : undefined; this.update_details(); @@ -248,15 +231,26 @@ this.$(".error-code").text(this.code || ""); this.$(".error-type").text(this.type || ""); this.$(".error-module").text(this.ns || ""); - if (this.api_message) { + + var extra_message = this.api_message || this.details; + + if (extra_message) { + var msg_html = "<span>{0}</span><br />" + + "<span class='api-message'>" + + "{1}</span>"; + this.$(".message p").html($( - "<span>{0}</span><br /><span class='api-message'>{1}</span>".format( + msg_html.format( _.escape(this.message), - _.escape(this.api_message)))); + _.escape(extra_message)) + )); } else { this.$(".message p").text(this.message || ""); } - this.$(".error-more-details p").html($("<pre />", {text:this.details}) || "no info"); + + this.$(".error-more-details p").html( + $("<pre />", {text:this.details}) || "no info" + ); this.$(".extra-details").remove(); _.each(this.error_options.extra_details, function(value, key){ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_feedback_view.js b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_feedback_view.js index 799cc7637fe14edb589dd67117d0c9a146e427e1..2686b72f6bc74f230564c31fbd62265b75a3d984 100644 --- a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_feedback_view.js +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_feedback_view.js @@ -1,35 +1,17 @@ -// Copyright 2011 GRNET S.A. All rights reserved. -// -// Redistribution and use in source and binary forms, with or -// without modification, are permitted provided that the following -// conditions are met: -// -// 1. Redistributions of source code must retain the above -// copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following -// disclaimer in the documentation and/or other materials -// provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// -// The views and conclusions contained in the software and -// documentation are those of the authors and should not be -// interpreted as representing official policies, either expressed -// or implied, of GRNET S.A. +// Copyright (C) 2010-2014 GRNET S.A. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. // ;(function(root){ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_icon_view.js b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_icon_view.js index e0ec8a734dff05a5771af41af37e6021c32a00f0..de892386e61f4f93d1287899605962f61a8c4892 100644 --- a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_icon_view.js +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_icon_view.js @@ -1,35 +1,17 @@ -// Copyright 2011 GRNET S.A. All rights reserved. -// -// Redistribution and use in source and binary forms, with or -// without modification, are permitted provided that the following -// conditions are met: -// -// 1. Redistributions of source code must retain the above -// copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following -// disclaimer in the documentation and/or other materials -// provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// -// The views and conclusions contained in the software and -// documentation are those of the authors and should not be -// interpreted as representing official policies, either expressed -// or implied, of GRNET S.A. +// Copyright (C) 2010-2014 GRNET S.A. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. // ;(function(root){ @@ -62,12 +44,16 @@ this.error = this.vm_view.find(".action-error"); this.close = this.vm_view.find(".close-action-error"); this.show_btn = this.vm_view.find(".show-action-error"); + this.project_view = this.vm_view.find(".project-name"); this.init_handlers(); this.update_layout(); }, - + init_handlers: function() { + this.project_view.bind('click', _.bind(function() { + synnefo.ui.main.vm_reassign_view.show(this.vm); + }, this)); // action call failed notify the user this.vm.bind("action:fail", _.bind(function(args){ if (this.vm.action_error) { @@ -75,7 +61,7 @@ var action = "undefined"; try { action = _.last(args).error_params.extra_details['Action']; - } catch (err) {console.log(err)}; + } catch (err) {console.error(err)}; this.error.find(".action").text(action); this.error.show(); @@ -102,6 +88,10 @@ }, this)); }, + show_reassign_view: function(vm) { + synnefo.ui.main.reassign_view.show(vm); + }, + show_error_overlay: function(args) { var args = util.parse_api_error.apply(util, args); @@ -127,10 +117,16 @@ this.view = view; this.vm_view = this.view.vm(vm); - this.info_toggle = $(".cont-toggler-wrapper.info .toggler", this.vm_view); - this.ips_toggle = $(".cont-toggler-wrapper.ips .toggler", this.vm_view); + this.info_toggle = $(".cont-toggler-wrapper.info .toggler", + this.vm_view); + this.ips_toggle = $(".cont-toggler-wrapper.ips .toggler", + this.vm_view); + this.volumes_toggle = $(".cont-toggler-wrapper.volumes .toggler", + this.vm_view); this.info_el = $("div.info-content.vm-info", this.vm_view); this.ips_el = $("div.info-content.ips", this.vm_view); + this.volumes_el = $("div.info-content.volumes", this.vm_view); + this.label = $(".label", this.vm_view); this.set_handlers(); @@ -140,46 +136,83 @@ this.info_toggle.click(_.bind(function(){ this.ips_el.slideUp(); this.ips_toggle.removeClass("open"); + this.volumes_el.slideUp(); + this.volumes_toggle.removeClass("open"); this.info_el.slideToggle(); - this.view.vm(this.vm).toggleClass("light-background"); + var vm_view = this.view.vm(this.vm); if (this.info_toggle.hasClass("open")) { this.info_toggle.removeClass("open"); this.vm.stop_stats_update(); + vm_view.removeClass("light-background"); } else { this.info_toggle.addClass("open"); this.view.details_views[this.vm.id].update_layout(); this.view.tags_views[this.vm.id].update_layout(); this.view.stats_views[this.vm.id].update_layout(); + vm_view.addClass("light-background"); } var self = this; - window.setTimeout(function() {$(self.view).trigger("resize")}, 300); + window.setTimeout(function() { + $(self.view).trigger("resize") + }, 300); }, this)); - this.ips_toggle.click(_.bind(function(){ - if(this.ips_toggle.parent().hasClass("disabled")) { + this.volumes_toggle.click(_.bind(function(){ + if(this.volumes_toggle.parent().hasClass("disabled")) { return; } this.info_el.slideUp(); this.info_toggle.removeClass("open"); + this.ips_el.slideUp(); + this.ips_toggle.removeClass("open"); + var vm_view = this.view.vm(this.vm); + + this.volumes_el.slideToggle(); + var self = this; + if (this.volumes_toggle.hasClass("open")) { + this.volumes_toggle.removeClass("open"); + vm_view.removeClass("light-background"); + } else { + this.volumes_toggle.addClass("open"); + vm_view.addClass("light-background"); + } + window.setTimeout(function() { + $(self.view).trigger("resize") + }, 300); + }, this)); + + + this.ips_toggle.click(_.bind(function(){ + if(this.ips_toggle.parent().hasClass("disabled")) { return; } + this.info_el.slideUp(); + this.info_toggle.removeClass("open"); + this.volumes_el.slideUp(); + this.volumes_toggle.removeClass("open"); + + var vm_view = this.view.vm(this.vm); this.ips_el.slideToggle(); - this.view.vm(this.vm).toggleClass("light-background"); var self = this; if (this.ips_toggle.hasClass("open")) { this.ips_toggle.removeClass("open"); + vm_view.removeClass("light-background"); } else { this.ips_toggle.addClass("open"); + vm_view.addClass("light-background"); } - window.setTimeout(function() {$(self.view).trigger("resize")}, 300); + window.setTimeout(function() { + $(self.view).trigger("resize") + }, 300); }, this)); - + this.vm_view.find(".stats-report").click(_.bind(function(e){ e.preventDefault(); snf.ui.main.show_vm_details(this.vm); - }, this)).attr("href", "#machines/single/details/{0}".format(this.vm.id)); + }, this)).attr("href", + "#machines/single/details/{0}".format(this.vm.id)); } }) @@ -291,10 +324,10 @@ }, submit: function() { - var value = _(self.$('input').val()).trim(); + var value = _(this.$('input').val()).trim(); if (value == "") { return }; this.renaming = false; - this.vm.rename(self.$('input').val()); + this.vm.rename(this.$('input').val()); this.update_layout(); } }); @@ -736,7 +769,6 @@ // stuff to do when a new vm has been created. // - create vm subviews post_add: function(vm) { - // rename views index this.rename_views = this.rename_views || {}; this.stats_views = this.stats_views || {}; this.connect_views = this.connect_views || {}; @@ -746,8 +778,11 @@ this.action_error_views = this.action_error_views || {}; this.action_views = this.action_views || {}; this.ports_views = this.ports_views || {}; + this.volumes_views = this.volumes_views || {}; + + this.action_views[vm.id] = new views.VMActionsView( + vm, this, this.vm(vm), this.hide_actions); - this.action_views[vm.id] = new views.VMActionsView(vm, this, this.vm(vm), this.hide_actions); this.rename_views[vm.id] = new views.IconRenameView(vm, this); this.stats_views[vm.id] = new views.VMStatsView(vm, this, {el:'.vm-stats'}); this.connect_views[vm.id] = new views.IconVMConnectView(vm, this); @@ -761,10 +796,20 @@ container: ports_container, parent: this }); - this.ports_views[vm.id] = ports_view + this.ports_views[vm.id] = ports_view; ports_view.show(); ports_view.el.hide(); + var volumes_container = this.vm(vm).find(".machine-data"); + var volumes_view = new views.VMVolumeListView({ + collection: vm.volumes, + container: ports_container, + parent: this + }); + this.volumes_views[vm.id] = volumes_view; + volumes_view.show(); + volumes_view.el.hide(); + this.info_views[vm.id] = new views.IconInfoView(vm, this); }, @@ -783,10 +828,7 @@ $(window).trigger("resize"); }, - // generic stuff to do on each view update - // called once after each vm has been updated update_layout: function() { - // TODO: why do we do this ?? if (storage.vms.models.length > 0) { this.$(".running").removeClass("disabled"); } else { @@ -798,7 +840,6 @@ // FIXME: code from old js api this.$("div.separator").show(); this.$("div.machine-container:last-child").find("div.separator").hide(); - fix_v6_addresses(); }, update_status_message: function(vm) { @@ -826,15 +867,26 @@ // update vm details update_details: function(vm) { var el = this.vm(vm); + var project = vm.get('project') + if (project) { + el.find(".project-name").text( + "[" + _.truncate(project.get('name'), 20) + "]" + ); + } // truncate name - el.find("span.name").text(util.truncate(vm.get("name"), 40)); + el.find("span.name").text(util.truncate(vm.get("name"), 37)); - el.find('.fqdn').text(vm.get('fqdn') || synnefo.config.no_fqdn_message); + el.find('.fqdn').val( + vm.get('fqdn') || synnefo.config.no_fqdn_message); el.find("div.status").text(STATE_TEXTS[vm.state()]); // set state class - el.find("div.state").removeClass().addClass(views.IconView.STATE_CLASSES[vm.state()].join(" ")); + var cls = views.IconView.STATE_CLASSES[vm.state()] || ['state']; + el.find("div.state").removeClass().addClass(cls.join(" ")); // os icon - el.find("div.logo").css({'background-image': "url(" + this.get_vm_icon_path(vm, "medium") + ")"}); + el.find("div.logo").css({ + 'background-image': "url(" + + this.get_vm_icon_path(vm, "medium") + ")" + }); el.removeClass("connectable"); if (vm.is_connectable()) { @@ -847,7 +899,8 @@ this.update_status_message(vm); icon_state = vm.is_active() ? "on" : "off"; - set_machine_os_image(el, "icon", icon_state, this.get_vm_icon_os(vm)); + set_machine_os_image(el, "icon", icon_state, + this.get_vm_icon_os(vm)); // update subviews this.rename_views[vm.id].update_layout(); @@ -906,6 +959,8 @@ 'START': ['state', 'starting-state'], 'CONNECT': ['state', 'connecting-state'], 'DISCONNECT': ['state', 'disconnecting-state'], + 'ATTACH_VOLUME': ['state', 'connecting-state'], + 'DETACH_VOLUME': ['state', 'disconnecting-state'], 'RESIZE': ['state', 'rebooting-state'] }; diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_invitations_view.js b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_invitations_view.js index 03a66aba49895684feb8435218735f05576a8131..9644891bb0f284fa3ea282cf42c0152a4b34b6f4 100644 --- a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_invitations_view.js +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_invitations_view.js @@ -1,35 +1,17 @@ -// Copyright 2011 GRNET S.A. All rights reserved. -// -// Redistribution and use in source and binary forms, with or -// without modification, are permitted provided that the following -// conditions are met: -// -// 1. Redistributions of source code must retain the above -// copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following -// disclaimer in the documentation and/or other materials -// provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// -// The views and conclusions contained in the software and -// documentation are those of the authors and should not be -// interpreted as representing official policies, either expressed -// or implied, of GRNET S.A. +// Copyright (C) 2010-2014 GRNET S.A. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. // ;(function(root){ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_ips_view.js b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_ips_view.js index 602661e0d9540d311dcdf679d570010b707f6c75..913c07e8f782f746235baa26a7dfec968721c18c 100644 --- a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_ips_view.js +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_ips_view.js @@ -1,35 +1,17 @@ -// Copyright 2011 GRNET S.A. All rights reserved. -// -// Redistribution and use in source and binary forms, with or -// without modification, are permitted provided that the following -// conditions are met: -// -// 1. Redistributions of source code must retain the above -// copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following -// disclaimer in the documentation and/or other materials -// provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// -// The views and conclusions contained in the software and -// documentation are those of the authors and should not be -// interpreted as representing official policies, either expressed -// or implied, of GRNET S.A. +// Copyright (C) 2010-2014 GRNET S.A. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. // ;(function(root){ @@ -41,6 +23,22 @@ var snf = root.synnefo = root.synnefo || {}; var views = snf.views = snf.views || {} var storage = snf.storage = snf.storage || {}; + var util = snf.util = snf.util || {}; + + var min_ip_quota = { + 'cyclades.floating_ip': 1 + }; + + views.CreateIPSelectProjectView = + views.CreateVMSelectProjectView.extend({ + tpl: '#create-view-projects-select-tpl', + required_quota: function() { + return min_ip_quota + }, + model_view_cls: views.CreateVMSelectProjectItemView.extend({ + display_quota: min_ip_quota + }) + }); views.IpPortView = views.ext.ModelView.extend({ tpl: '#ip-port-view-tpl', @@ -119,14 +117,35 @@ tpl: '#ip-view-tpl', auto_bind: ['connect_vm'], + show_reassign_view: function() { + synnefo.ui.main.ip_reassign_view.show(this.model); + }, + status_cls: function() { - return this.status_cls_map[this.model.get('status')]; + var status = this.model.get('status'); + var vm = this.model.get("port") && this.model.get("port").get("vm"); + if (status == "CONNECTED" && vm) { + return snf.views.ext.VM_STATUS_CLS_MAP[vm.state()].join(" "); + } else { + return this.status_cls_map[this.model.get('status')]; + } }, status_display: function(v) { - return this.status_map[this.model.get('status')]; + var vm_status = ""; + var vm = this.model.get("port") && this.model.get("port").get("vm"); + var ip_status = this.status_map[this.model.get('status')]; + if (vm) { + vm_status = STATE_TEXTS[vm.state()] || ""; + } + if (!vm_status) { return ip_status; } + return ip_status + " - " + vm_status; }, + show_reassign_view: function() { + synnefo.ui.main.ip_reassign_view.show(this.model); + }, + model_icon: function() { var img = 'ip-icon-detached.png'; var src = synnefo.config.images_url + '/{0}'; @@ -177,47 +196,118 @@ } }); + views.FloatingIPCreateView = views.Overlay.extend({ + view_id: "ip_create_view", + content_selector: "#ips-create-content", + css_class: 'overlay-ip-create overlay-info', + overlay_id: "ip-create-overlay", + + title: "Create new IP address", + subtitle: "IP addresses", + + initialize: function(options) { + views.FloatingIPCreateView.__super__.initialize.apply(this); + + this.create_button = this.$("form .form-action.create"); + this.form = this.$("form"); + this.projects_list = this.$(".projects-list"); + this.project_select_view = undefined; + this.init_handlers(); + }, + + init_handlers: function() { + this.create_button.click(_.bind(function(e){ + this.submit(); + }, this)); + + this.form.submit(_.bind(function(e){ + e.preventDefault(); + this.submit(); + return false; + }, this)) + }, + + submit: function() { + if (this.validate()) { + this.create(); + }; + }, + + validate: function() { + var project = this.get_project(); + if (!project || !project.quotas.can_fit({'cyclades.floating_ip': 1})) { + this.project_select.closest(".form-field").addClass("error"); + this.project_select.focus(); + return false; + } + return true; + }, + + create: function() { + if (this.create_button.hasClass("in-progress")) { return } + this.create_button.addClass("in-progress"); + + var project_id = this.get_project().get("id"); + var project = synnefo.storage.projects.get(project_id); + + + var cb = _.bind(function() { + synnefo.api.trigger("quota:update"); + this.hide(); + }, this); + + snf.storage.floating_ips.create({ + floatingip: { + project: project_id + } + }, + { + complete: cb + }); + }, + + get_project: function() { + var project = this.project_select_view.get_selected()[0]; + return project; + }, + + init_subviews: function() { + if (!this.project_select_view) { + var view_cls = views.CreateIPSelectProjectView; + this.project_select_view = new view_cls({ + container: this.projects_list, + collection: synnefo.storage.joined_projects, + parent_view: this + }); + } + this.project_select_view.show(true); + var project = synnefo.storage.quotas.get_available_projects(min_ip_quota)[0]; + if (project) { + this.project_select_view.set_current(project); + } + }, + + hide: function() { + this.project_select_view && this.project_select_view.hide(true); + views.FloatingIPCreateView.__super__.hide.apply(this, arguments); + }, + + beforeOpen: function() { + this.init_subviews(); + this.create_button.removeClass("in-progress") + this.$(".form-field").removeClass("error"); + } + }); + views.IpCollectionView = views.ext.CollectionView.extend({ collection: storage.floating_ips, collection_name: 'floating_ips', model_view_cls: views.IpView, - create_view: undefined, // no create overlay for IPs - quota_key: 'cyclades.floating_ip', + create_view_cls: views.FloatingIPCreateView, + quota_key: 'ip', initialize: function() { views.IpCollectionView.__super__.initialize.apply(this, arguments); this.connect_view = new views.IPConnectVmOverlay(); - this.creating = false; - }, - - set_creating: function() { - this.creating = true; - this.create_button.addClass("in-progress"); - }, - - reset_creating: function() { - this.creating = false; - this.create_button.removeClass("in-progress"); - }, - - handle_create_click: function() { - if (this.creating) { - return - } - - this.set_creating(); - network = synnefo.storage.networks.get_floating_ips_network(); - this.collection.create({ - floatingip: {} - }, - { - success: _.bind(function() { - this.post_create(); - }, this), - complete: _.bind(function() { - this.creating = false; - this.reset_creating(); - this.collection.fetch(); - }, this)}); } }); @@ -230,6 +320,7 @@ css_class: "overlay-info connect-ip", title: "Attach IP to machine", allow_multiple: false, + allow_empty: false, show_vms: function(ip, vms, selected, callback, subtitle) { views.IPConnectVmOverlay.__super__.show_vms.call(this, @@ -246,4 +337,5 @@ }); + })(this); diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_list_view.js b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_list_view.js index 1033243cfcc8c3510ebc96167ad59b66f7832177..fc4abc34d6447fc77a04acc6a653c5725204c68d 100644 --- a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_list_view.js +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_list_view.js @@ -1,35 +1,17 @@ -// Copyright 2011 GRNET S.A. All rights reserved. -// -// Redistribution and use in source and binary forms, with or -// without modification, are permitted provided that the following -// conditions are met: -// -// 1. Redistributions of source code must retain the above -// copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following -// disclaimer in the documentation and/or other materials -// provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// -// The views and conclusions contained in the software and -// documentation are those of the authors and should not be -// interpreted as representing official policies, either expressed -// or implied, of GRNET S.A. +// Copyright (C) 2010-2014 GRNET S.A. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. // ;(function(root){ @@ -353,8 +335,9 @@ }, get_vm_table_data: function(vm) { + var cls = views.ListView.STATE_CLASSES[vm.state()] || []; var checkbox = '<input type="checkbox" class="' + - views.ListView.STATE_CLASSES[vm.state()].join(" ") + + cls.join(" ") + ' list-vm-checkbox" id="checkbox-' + this.id_tpl + vm.id + '"/>'; var img = '<img class="os_icon" src="'+ this.get_vm_icon_path(vm, "small") +'" />'; @@ -505,7 +488,7 @@ return views.ListView.VM_OS_ICON_TPLS()[icon_type].format(os, st); } - }) + }); views.ListView.VM_OS_ICON_TPLS = function() { return { @@ -528,6 +511,8 @@ 'START': ['starting-state'], 'CONNECT': ['connecting-state'], 'DISCONNECT': ['disconnecting-state'], + 'ATTACH_VOLUME': ['connecting-state'], + 'DETACH_VOLUME': ['disconnecting-state'], 'RESIZE': ['rebooting-state'] }; diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_main_view.js b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_main_view.js index af238fa5ce0d27bf6598cdba6a6476f160ed4780..e17beee4c5335738dc537b5544b186db58a90aca 100644 --- a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_main_view.js +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_main_view.js @@ -1,35 +1,17 @@ -// Copyright 2011 GRNET S.A. All rights reserved. -// -// Redistribution and use in source and binary forms, with or -// without modification, are permitted provided that the following -// conditions are met: -// -// 1. Redistributions of source code must retain the above -// copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following -// disclaimer in the documentation and/or other materials -// provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// -// The views and conclusions contained in the software and -// documentation are those of the authors and should not be -// interpreted as representing official policies, either expressed -// or implied, of GRNET S.A. +// Copyright (C) 2010-2014 GRNET S.A. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. // ;(function(root){ @@ -228,6 +210,23 @@ } }); + this.init_ns("volumes", { + msg_tpl:"Your actions will affect 1 Volume.", + msg_tpl_plural:"{0} actions will affect {0} Volumes.", + actions_msg: {confirm: "Confirm all", cancel: "Cancel all"}, + limit: 1, + cancel_all: function(actions, config) { + _.each(actions, function(action, id) { + action.model.actions.reset_pending(); + }); + }, + do_all: function(actions, config) { + _.each(actions, function(action, id) { + var action_method = "do_{0}".format(action.actions[0]); + action.model[action_method].apply(action.model); + }); + } + }); this.init_ns("keys", { msg_tpl:"Your actions will affect 1 public key.", @@ -303,6 +302,7 @@ var ns_map = { 'ips': storage.floating_ips, 'keys': storage.keys, + 'volumes': storage.volumes, 'nets': storage.networks } _.each(ns_map, function(store, ns) { @@ -332,6 +332,10 @@ actions[model.id] = {model: model, actions: action}; } + if (type == "volumes") { + actions[model.id] = {model: model, actions: action}; + } + if (type == "nets") { actions[model.id] = {model: model, actions: action}; } @@ -473,36 +477,63 @@ self.title.text(self.parent.get_title()); }); - this.pane_view_selector.find("a#machines_view_link").click(_.bind(function(ev){ + this.pane_view_selector.find("a#machines_view_link").click( + _.bind(function(ev){ ev.preventDefault(); this.router.vms_index(); - }, this)) - this.pane_view_selector.find("a#networks_view_link").click(_.bind(function(ev){ + }, this) + ); + + this.pane_view_selector.find("a#networks_view_link").click( + _.bind(function(ev){ ev.preventDefault(); this.router.networks_view(); - }, this)) - this.pane_view_selector.find("a#ips_view_link").click(_.bind(function(ev){ + }, this) + ); + + this.pane_view_selector.find("a#ips_view_link").click( + _.bind(function(ev){ ev.preventDefault(); this.router.ips_view(); - }, this)) - this.pane_view_selector.find("a#public_keys_view_link").click(_.bind(function(ev){ + }, this) + ); + + this.machine_view_selector.find("a#volumes_view_list_link").click( + _.bind(function(ev){ + ev.preventDefault(); + this.router.volumes_view(); + }, this) + ); + + this.pane_view_selector.find("a#public_keys_view_link").click( + _.bind(function(ev){ ev.preventDefault(); this.router.public_keys_view(); - }, this)) + }, this) + ); - this.machine_view_selector.find("a#machines_view_icon_link").click(_.bind(function(ev){ + this.machine_view_selector.find("a#machines_view_icon_link").click( + _.bind(function(ev){ ev.preventDefault(); var d = $.now(); this.router.vms_icon_view(); - }, this)) - this.machine_view_selector.find("a#machines_view_list_link").click(_.bind(function(ev){ + }, this) + ); + + this.machine_view_selector.find("a#machines_view_list_link").click( + _.bind(function(ev){ ev.preventDefault(); this.router.vms_list_view(); - }, this)) - this.machine_view_selector.find("a#machines_view_single_link").click(_.bind(function(ev){ + }, this) + ); + + var selector = "a#machines_view_single_link" + this.machine_view_selector.find(selector).click( + _.bind(function(ev){ ev.preventDefault(); this.router.vms_single_view(); - }, this)) + }, this) + ); }, update_layout: function() { @@ -537,6 +568,7 @@ 'icon': 'machines', 'single': 'machines', 'list': 'machines', 'networks': 'networks', 'ips': 'IP addresses', + 'volumes': 'disks', 'public-keys': 'public keys' }, @@ -547,14 +579,16 @@ 1: 'list', 3: 'networks', 4: 'ips', - 5: 'public-keys' + 5: 'volumes', + 6: 'public-keys' }, views_pane_indexes: { 0: 'single', 1: 'networks', 2: 'ips', - 3: 'public-keys' + 3: 'volumes', + 4: 'public-keys' }, // views classes registry @@ -564,15 +598,32 @@ 'list': views.ListView, 'networks': views.NetworksPaneView, 'ips': views.IpsPaneView, + 'volumes': views.VolumesPaneView, 'public-keys': views.PublicKeysPaneView }, // view ids - views_ids: {'icon':0, 'single':2, 'list':1, 'networks':3, 'ips':4, 'public-keys': 5}, + views_ids: { + 'icon': 0, + 'single': 2, + 'list': 1, + 'networks': 3, + 'ips': 4, + 'volumes': 5, + 'public-keys': 6 + }, // on which pane id each view exists // machine views (icon,single,list) are all on first pane - pane_ids: {'icon':0, 'single':0, 'list':0, 'networks':1, 'ips':2, 'public-keys': 3}, + pane_ids: { + 'icon': 0, + 'single': 0, + 'list': 0, + 'volumes': 1, + 'networks': 2, + 'ips': 3, + 'public-keys': 4 + }, initialize: function(show_view) { if (!show_view) { show_view = 'icon' }; @@ -695,7 +746,8 @@ }, init_overlays: function() { - this.create_vm_view = new views.CreateVMView(); + this.create_vm_view = new views.VMCreateView(); + this.create_snapshot_view = new views.SnapshotCreateView(); this.api_info_view = new views.ApiInfoView(); this.details_view = new views.DetailsView(); this.suspended_view = new views.SuspendedVMView(); @@ -713,7 +765,7 @@ $(".css-panes").show(); }, - items_to_load: 6, + items_to_load: 8, completed_items: 0, check_status: function(loaded) { this.completed_items++; @@ -776,9 +828,11 @@ 'networks': storage.networks, 'vms': storage.vms, 'quotas': storage.quotas, + 'projects': storage.projects, 'ips': storage.floating_ips, 'subnets': storage.subnets, 'ports': storage.ports, + 'volumes': storage.volumes, 'keys': storage.keys }, function(col, name) { this.init_interval(name, col) @@ -825,7 +879,6 @@ this.loaded = true; // application start point - this.check_empty(); this.show_initial_view(); }, @@ -836,6 +889,10 @@ } this.error_view = new views.ErrorView(); this.vm_resize_view = new views.VmResizeView(); + this.vm_reassign_view = new views.VmReassignView(); + this.ip_reassign_view = new views.IPReassignView(); + this.network_reassign_view = new views.NetworkReassignView(); + this.volume_reassign_view = new views.VolumeReassignView(); // api request error handling synnefo.api.bind("error", _.bind(this.handle_api_error, this)); @@ -853,13 +910,6 @@ // display loading message this.show_loading_view(); - // sync load initial data - this.update_status("images", 0); - storage.images.fetch({refresh:true, update:false, success: function(){ - self.update_status("images", 1); - self.check_status(); - self.load_nets_and_vms(); - }}); this.update_status("flavors", 0); storage.flavors.fetch({refresh:true, update:false, success:function(){ self.update_status("flavors", 1); @@ -869,12 +919,29 @@ this.update_status("resources", 0); storage.resources.fetch({refresh:true, update:false, success: function(){ self.update_status("resources", 1); - self.update_status("quotas", 0); + self.update_status("projects", 0); self.check_status(); - storage.quotas.fetch({refresh:true, update:true, success: function() { - self.update_status("quotas", 1); - self.update_status("layout", 1); - self.check_status() + storage.projects.fetch({refresh:true, update:true, success: function() { + self.update_status("projects", 1); + self.update_status("quotas", 0); + self.check_status(); + storage.quotas.fetch({refresh:true, update:true, success: function() { + self.update_status("quotas", 1); + self.check_status(); + self.update_status("volumes", 0); + storage.volumes.fetch({refresh:true, update:false, success: function(){ + self.update_status("volumes", 1); + self.check_status(); + }}); + + // sync load initial data + self.update_status("images", 0); + storage.images.fetch({refresh:true, update:false, success: function(){ + self.update_status("images", 1); + self.check_status(); + self.load_nets_and_vms(); + }}); + }}); }}) }}) }, @@ -927,21 +994,15 @@ }, update_create_buttons_status: function() { - var nets = storage.quotas.get('cyclades.network.private'); - var vms = storage.quotas.get('cyclades.vm'); - - if (!nets || !vms) { return } - - if (!nets.can_consume()) { - $("#networks-pane a.createbutton").addClass("disabled"); - } else { - $("#networks-pane a.createbutton").removeClass("disabled"); - } - - if (!vms.can_consume()) { - $("#createcontainer #create").addClass("disabled"); + var vms = storage.quotas.can_create('vm'); + var create_button = $("#createcontainer #create"); + var msg = snf.config.limit_reached_msg; + if (!vms) { + create_button.addClass("disabled"); + snf.util.set_tooltip(create_button, msg, {tipClass: 'warning tooltip'}); } else { - $("#createcontainer #create").removeClass("disabled"); + create_button.removeClass("disabled"); + snf.util.unset_tooltip(create_button); } }, diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_metadata_view.js b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_metadata_view.js index 8eb6da268bbe23d1dd37a8231413da7a29eea5ce..e186f204b57de748e4fe4fba5f2e0da0bfb7840f 100644 --- a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_metadata_view.js +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_metadata_view.js @@ -1,35 +1,17 @@ -// Copyright 2011 GRNET S.A. All rights reserved. -// -// Redistribution and use in source and binary forms, with or -// without modification, are permitted provided that the following -// conditions are met: -// -// 1. Redistributions of source code must retain the above -// copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following -// disclaimer in the documentation and/or other materials -// provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// -// The views and conclusions contained in the software and -// documentation are those of the authors and should not be -// interpreted as representing official policies, either expressed -// or implied, of GRNET S.A. +// Copyright (C) 2010-2014 GRNET S.A. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. // ;(function(root){ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_model_views.js b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_model_views.js index 3fbf20ea4972a432ce3688ca1c28a02dd1bfd9b4..7c46ce5f44ecf5a6bcec33689011a12a8bd67439 100644 --- a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_model_views.js +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_model_views.js @@ -1,35 +1,17 @@ -// Copyright 2011 GRNET S.A. All rights reserved. -// -// Redistribution and use in source and binary forms, with or -// without modification, are permitted provided that the following -// conditions are met: -// -// 1. Redistributions of source code must retain the above -// copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following -// disclaimer in the documentation and/or other materials -// provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// -// The views and conclusions contained in the software and -// documentation are those of the authors and should not be -// interpreted as representing official policies, either expressed -// or implied, of GRNET S.A. +// Copyright (C) 2010-2014 GRNET S.A. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. // ;(function(root){ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_networks_view.js b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_networks_view.js index cc169edddd4561185a2c618f1dbc65f69aa9d80d..0838229dbd46ae586efc2105bbd0b841c20c5295 100644 --- a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_networks_view.js +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_networks_view.js @@ -1,35 +1,17 @@ -// Copyright 2013 GRNET S.A. All rights reserved. -// -// Redistribution and use in source and binary forms, with or -// without modification, are permitted provided that the following -// conditions are met: -// -// 1. Redistributions of source code must retain the above -// copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following -// disclaimer in the documentation and/or other materials -// provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// -// The views and conclusions contained in the software and -// documentation are those of the authors and should not be -// interpreted as representing official policies, either expressed -// or implied, of GRNET S.A. +// Copyright (C) 2010-2014 GRNET S.A. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. // ;(function(root){ @@ -51,6 +33,21 @@ // logging var logger = new snf.logging.logger("SNF-VIEWS"); var debug = _.bind(logger.debug, logger); + + var min_network_quota = { + 'cyclades.network.private': 1 + }; + + views.CreateNetworkSelectProjectView = + views.CreateVMSelectProjectView.extend({ + tpl: '#create-view-projects-select-tpl', + required_quota: function() { + return min_network_quota + }, + model_view_cls: views.CreateVMSelectProjectItemView.extend({ + display_quota: min_network_quota + }) + }); views.NetworkCreateView = views.Overlay.extend({ view_id: "network_create_view", @@ -72,6 +69,12 @@ this.type_select = this.$("#network-create-type"); this.subnet_select = this.$("#network-create-subnet"); this.subnet_custom = this.$("#network-create-subnet-custom"); + + this.gateway_select = this.$("#network-create-gateway"); + this.gateway_custom = this.$("#network-create-gateway-custom"); + + this.projects_list = this.$(".projects-list"); + this.project_select_view = undefined; this.dhcp_form = this.$("#network-create-dhcp-fields"); @@ -92,7 +95,7 @@ if (_.keys(synnefo.config.network_available_types).length <= 1) { this.type_select.closest(".form-field").hide(); } - + this.check_dhcp_form(); this.init_handlers(); }, @@ -100,6 +103,9 @@ reset_dhcp_form: function() { this.subnet_select.find("option")[0].selected = 1; this.subnet_custom.val(""); + this.gateway_select.find("option")[0].selected = 1; + this.gateway_custom.val(""); + this.dhcp_form.find(".form-field").removeClass("error"); }, check_dhcp_form: function() { @@ -114,6 +120,23 @@ } else { this.subnet_custom.hide(); } + + if (this.gateway_select.val() == "custom") { + this.gateway_custom.show(); + } else { + this.gateway_custom.hide(); + } + + if (this.subnet_select.val() == "auto") { + this.gateway_select.find("option")[1].disabled = true; + if (this.gateway_select.val() == "custom") { + this.gateway_select.val("auto"); + this.gateway_custom.val(""); + this.gateway_custom.hide(); + } + } else { + this.gateway_select.find("option")[1].disabled = false; + } }, init_handlers: function() { @@ -130,6 +153,13 @@ } }, this)); + this.gateway_select.change(_.bind(function(e){ + this.check_dhcp_form(); + if (this.gateway_custom.is(":visible")) { + this.gateway_custom.focus(); + } + }, this)); + this.create_button.click(_.bind(function(e){ this.submit(); }, this)); @@ -138,11 +168,11 @@ e.preventDefault(); this.submit; return false; - }, this)) + }, this)); this.text.keypress(_.bind(function(e){ if (e.which == 13) {this.submit()}; - },this)) + },this)); }, submit: function() { @@ -155,32 +185,56 @@ // sanitazie var t = this.text.val(); t = t.replace(/^\s+|\s+$/g,""); + is_valid = true; this.text.val(t); if (this.text.val() == "") { this.text.closest(".form-field").addClass("error"); this.text.focus(); - return false; + is_valid = false; } else { this.text.closest(".form-field").removeClass("error"); } + var project = this.get_project(); + if (!project || !project.quotas.can_fit({'cyclades.network.private': 1})) { + this.projects_list.addClass("error"); + this.projects_list.focus(); + is_valid = false; + } + if (this.dhcp_select.is(":checked")) { + if (this.gateway_select.val() == "custom") { + var gw = this.gateway_custom.val(); + gw = gw.replace(/^\s+|\s+$/g,""); + this.gateway_custom.val(gw); + + if (!synnefo.util.IP_REGEX.exec(this.gateway_custom.val())) { + this.gateway_custom.closest(".form-field").prev().addClass("error"); + this.gateway_custom.closest(".form-field").addClass("error"); + is_valid = false; + } else { + this.gateway_custom.closest(".form-field").prev().removeClass("error"); + this.gateway_custom.closest(".form-field").removeClass("error"); + } + } + if (this.subnet_select.val() == "custom") { var sub = this.subnet_custom.val(); sub = sub.replace(/^\s+|\s+$/g,""); this.subnet_custom.val(sub); - if (!synnefo.util.IP_REGEX.exec(this.subnet_custom.val())) { + if (!synnefo.util.SUBNET_REGEX.exec(this.subnet_custom.val())) { this.subnet_custom.closest(".form-field").prev().addClass("error"); - return false; + this.subnet_custom.closest(".form-field").addClass("error"); + is_valid = false; } else { this.subnet_custom.closest(".form-field").prev().removeClass("error"); + this.subnet_custom.closest(".form-field").removeClass("error"); } }; } - - return true; + return is_valid; }, get_next_available_subnet: function() { @@ -200,12 +254,16 @@ }, create: function() { + if (this.create_button.hasClass("in-progress")) { return } this.create_button.addClass("in-progress"); var name = this.text.val(); var dhcp = this.dhcp_select.is(":checked"); var subnet = null; var type = this.type_select.val(); + var project_id = this.get_project().get("id"); + var project = synnefo.storage.projects.get(project_id); + var gateway = undefined; if (dhcp) { @@ -217,24 +275,56 @@ subnet = this.subnet_select.val(); } + var gw_type = this.gateway_select.val(); + if (gw_type == "auto") { gateway = "auto"; } + if (gw_type == "custom") { + gateway = this.gateway_custom.val(); + } } - snf.storage.networks.create(name, type, subnet, dhcp, gateway, - _.bind(function(){ - this.hide(); - // trigger parent view create handler - this.parent_view.post_create(); - }, this)); + snf.storage.networks.create( + project, name, type, subnet, dhcp, gateway, _.bind(function(){ + this.hide(); + }, this)); + }, + + get_project: function() { + var project = this.project_select_view.get_selected()[0]; + return project; + }, + + init_subviews: function() { + if (!this.project_select_view) { + var view_cls = views.CreateNetworkSelectProjectView; + this.project_select_view = new view_cls({ + container: this.projects_list, + collection: synnefo.storage.joined_projects, + parent_view: this + }); + } + this.project_select_view.show(true); + var project = synnefo.storage.quotas.get_available_projects(min_network_quota)[0]; + if (project) { + this.project_select_view.set_current(project); + } + }, + + hide: function() { + this.project_select_view && this.project_select_view.hide(true); + views.NetworkCreateView.__super__.hide.apply(this, arguments); }, beforeOpen: function() { + this.init_subviews(); this.create_button.removeClass("in-progress") - this.text.closest(".form-field").removeClass("error"); + this.$(".form-field").removeClass("error"); this.text.val(""); this.text.show(); this.text.focus(); this.subnet_custom.val(""); this.subnet_select.val("auto"); + this.gateway_select.val("auto"); + this.gateway_custom.val(""); this.dhcp_select.attr("checked", true); this.type_select.val(_.keys(synnefo.config.network_available_types)[0]); this.check_dhcp_form(); @@ -242,7 +332,7 @@ onOpen: function() { this.text.focus(); - } + } }); views.NetworkPortView = views.ext.ModelView.extend({ @@ -449,6 +539,10 @@ this.ports_visible = false; }, + show_reassign_view: function() { + synnefo.ui.main.network_reassign_view.show(this.model); + }, + set_ports_empty: function() { if (this.ports_visible) { this.toggle_ports(); @@ -572,11 +666,11 @@ collection_name: 'networks', model_view_cls: views.NetworkView, create_view_cls: views.NetworkCreateView, - quota_key: 'cyclades.network.private', + quota_key: 'network', group_key: 'name', group_network: function(n) { - return n.get('is_public') + return n && n.get && n.get('is_public') }, init: function() { @@ -646,8 +740,9 @@ collection_view_selector: '#networks-list-view' }); - views.VMSelectView = views.ext.SelectModelView.extend({ + views.VMSelectItemView = views.ext.SelectModelView.extend({ tpl: '#vm-select-model-tpl', + max_title_length: 20, get_vm_icon: function() { return $(snf.ui.helpers.vm_icon_tag(this.model, "small")).attr("src") }, @@ -655,44 +750,24 @@ return (views.IconView.STATE_CLASSES[this.model.get("state")] || []).join(" ") + " status clearfix" }, status_display: function() { - return STATE_TEXTS[this.model.get("state")] + return STATE_TEXTS[this.model.get("state")]; + }, + truncate_title: function() { + return snf.util.truncate(this.model.get("name"), this.max_title_length); } }); - views.VMSelectView = views.ext.CollectionView.extend({ - init: function() { - views.VMSelectView.__super__.init.apply(this); - }, + views.VMSelectView = views.ext.CollectionSelectView.extend({ tpl: '#vm-select-collection-tpl', - model_view_cls: views.VMSelectView, - - trigger_select: function(view, select) { - this.trigger("change:select", view, select); - }, + model_view_cls: views.VMSelectItemView, + max_title_length: 20, post_add_model_view: function(view) { - view.bind("change:select", this.trigger_select, this); + views.VMSelectView.__super__.post_add_model_view.call(this, view); + view.max_title_length = this.max_title_length; if (!this.options.allow_multiple) { - view.input.prop("type", "radio"); + view.input.prop("type", "radio"); } - }, - - post_remove_model_view: function(view) { - view.unbind("change:select", this.trigger_select, this); - }, - - deselect_all: function(except) { - _.each(this._subviews, function(view) { - if (view != except) { view.deselect() } - }); - }, - - get_selected: function() { - return _.filter(_.map(this._subviews, function(view) { - if (view.selected) { - return view.model; - } - }), function(m) { return m }); } }); @@ -702,6 +777,7 @@ content_selector: "#network-vms-select-content", css_class: "overlay-info", allow_multiple: true, + allow_empty: true, initialize: function() { views.NetworkConnectVMsOverlay.__super__.initialize.apply(this); @@ -717,17 +793,11 @@ this.collection_view = new views.VMSelectView({ collection: collection, el: this.list, - allow_multiple: this.allow_multiple + allow_multiple: this.allow_multiple, + allow_empty: this.allow_empty }); this.collection_view.show(true); this.list.append($(this.collection_view.el)); - if (!this.allow_multiple) { - this.collection_view.bind("change:select", - function(view, selected) { - if (!selected) { return } - this.collection_view.deselect_all(view); - }, this); - } }, handle_vm_click: function(el) { @@ -829,6 +899,11 @@ classes: 'public-network', post_init_element: function() { views.NetworkSelectPublicNetwork.__super__.post_init_element.apply(this); + }, + resolve_floating_ip_view_params: function() { + return { + project: this.parent_view.parent_view.project + } } }); @@ -863,7 +938,7 @@ views.NetworkSelectFloatingIpsView = views.ext.CollectionView.extend({ tpl: '#networks-select-floating-ips-tpl', model_view_cls: views.NetworkSelectFloatingIpView, - + deselect_all: function() { this.each_ip_view(function(v) { v.deselect() }); }, @@ -876,16 +951,17 @@ }) }, - post_init: function() { + post_init: function(options) { var parent = this.parent_view; var self = this; - - this.quota = synnefo.storage.quotas.get("cyclades.floating_ip"); + + this.quotas = this.options.project.quotas.get("cyclades.floating_ip"); + this.project = this.options.project; this.selected_ips = []; this.handle_ip_select = _.bind(this.handle_ip_select, this); this.create = this.$(".floating-ip.create"); - this.quota.bind("change", _.bind(this.update_available, this)); + synnefo.storage.quotas.bind("change", _.bind(this.update_available, this)); this.collection.bind("change", _.bind(this.update_available, this)) this.collection.bind("add", _.bind(this.update_available, this)) this.collection.bind("remove", _.bind(this.update_available, this)) @@ -910,22 +986,21 @@ }, show_parent: function() { - var left = this.quota.get_available(); + var left = this.quotas.get_available(); var available = this.collection.length || left; if (!available) { this.hide_parent(); return; } - this.select_first(); this.parent_view.item.addClass("selected"); this.parent_view.input.attr("checked", true); this.parent_view.selected = true; + this.select_first(); this.show(true); }, update_available: function() { - var left = this.quota.get_available(); - var available = this.collection.length || left; + var can_create = synnefo.storage.quotas.can_create('ip'); var available_el = this.parent_view.$(".available"); var no_available_el = this.parent_view.$(".no-available"); var parent_check = this.parent_view.$("input[type=checkbox]"); @@ -933,23 +1008,7 @@ var create_link = this.$(".create a"); var create_no_available = this.$(".create .no-available"); - if (!available) { - // no ip's available to select - this.hide_parent(); - available_el.hide(); - no_available_el.show(); - parent_check.attr("disabled", true); - } else { - // available floating ip - var available_text = "".format( - this.collection.length + this.quota.get_available()); - available_el.removeClass("hidden").text(available_text).show(); - available_el.show(); - no_available_el.hide(); - parent_check.attr("disabled", false); - } - - if (left) { + if (can_create) { // available quota create.removeClass("no-available"); create.show(); @@ -960,7 +1019,6 @@ create.addClass("no-available"); create.hide(); create_link.hide(); - //create_no_available.show(); } this.update_selected(); }, @@ -987,6 +1045,7 @@ post_remove_model_view: function(view) { view.deselect(); view.unbind("change:select", this.handle_ip_select) + this.update_available(); }, handle_create_error: function() {}, @@ -1006,10 +1065,26 @@ }, create_ip: function() { - if (!this.quota.get_available()) { return } + var quotas = synnefo.storage.quotas; + var required_quota = quotas.required_quota['ip']; + var projects = quotas.get_available_projects(required_quota); + var project = undefined; + var use_view_project = projects.indexOf(this.project) > -1; + if (use_view_project) { + project = this.project; + } else { + if (projects.length) { + project = projects[0]; + } + } + var self = this; this.set_creating(); - synnefo.storage.floating_ips.create({floatingip:{}}, { + var data = {floatingip:{}}; + if (project) { + data.floatingip['project'] = project.get('id'); + } + synnefo.storage.floating_ips.create(data, { error: _.bind(this.handle_create_error, this), complete: function() { synnefo.storage.quotas.fetch(); @@ -1019,17 +1094,37 @@ }, select_first: function() { - if (this.selected_ips.length > 0) { return } + // automaticaly select a public IP address. Priority to the IPs + // assigned to the project selected in wizard. + + this.deselect_all(); if (this._subviews.length == 0) { return } - this._subviews[0].select(); - if (!_.contains(this.selected_ips, this._subviews[0].model)) { - this.selected_ips.push(this._subviews[0].model); + + var project_ip_found = false; + var project_uuid = this.project && this.project.get('id'); + _.each(this._subviews, function(view) { + var view_project_uuid = view.model.get('project') && + view.model.get('project').get('id'); + if (view_project_uuid == project_uuid) { + this.deselect_all(); + view.select(); + project_ip_found = true; + } + }, this); + + if (!project_ip_found) { + this._subviews[0] && this._subviews[0].select(); } }, - + post_add_model_view: function(view, model) { view.bind("change:select", this.handle_ip_select) - if (!this.selected_ips.length && this._subviews.length == 1) { + }, + + auto_select: true, + post_update_models: function() { + if (this.collection.length && this.auto_select) { + this.auto_select = false; this.select_first(); } }, @@ -1076,7 +1171,8 @@ }, initialize: function(options) { - this.quotas = synnefo.storage.quotas.get('cyclades.private_network'); + this.project = options.project; + this.quotas = this.project.quotas.get('cyclades.private_network'); options = options || {}; options.model = options.model || new models.Model(); this.private_networks = new Backbone.FilteredCollection(undefined, { diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_public_keys_view.js b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_public_keys_view.js index a5f82113ed6bf2544ed4a6c85669f85d5791bc8a..a400d83af8847fc68ab19ee8da4f5d18c2bdeb8e 100644 --- a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_public_keys_view.js +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_public_keys_view.js @@ -1,35 +1,17 @@ -// Copyright 2013 GRNET S.A. All rights reserved. -// -// Redistribution and use in source and binary forms, with or -// without modification, are permitted provided that the following -// conditions are met: -// -// 1. Redistributions of source code must retain the above -// copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following -// disclaimer in the documentation and/or other materials -// provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// -// The views and conclusions contained in the software and -// documentation are those of the authors and should not be -// interpreted as representing official policies, either expressed -// or implied, of GRNET S.A. +// Copyright (C) 2010-2014 GRNET S.A. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. // ;(function(root){ @@ -326,12 +308,13 @@ update_quota: function() { var quota = synnefo.config.userdata_keys_limit; var available = quota - this.collection.length; - if (available > 0) { - this.create_button.removeClass("disabled"); - this.create_button.attr("title", this.quota_limit_message || "Quota limit reached") - } else { + if (!available) { + var msg = snf.config.limit_reached_msg; + snf.util.set_tooltip(this.create_button, this.quota_limit_message || msg, {tipClass: 'warning tooltip'}); this.create_button.addClass("disabled"); - this.create_button.attr("title", ""); + } else { + snf.util.unset_tooltip(this.create_button); + this.create_button.removeClass("disabled"); } } }); diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_reassign_view.js b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_reassign_view.js new file mode 100644 index 0000000000000000000000000000000000000000..a518a039e95b2a3adcad7b07d7c6d9515d035e0f --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_reassign_view.js @@ -0,0 +1,372 @@ +// Copyright (C) 2010-2014 GRNET S.A. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. +// + +;(function(root){ + + // root + var root = root; + + // setup namepsaces + var snf = root.synnefo = root.synnefo || {}; + var models = snf.models = snf.models || {} + var storage = snf.storage = snf.storage || {}; + var ui = snf.ui = snf.ui || {}; + var util = snf.util = snf.util || {}; + + var views = snf.views = snf.views || {} + + // shortcuts + var bb = root.Backbone; + + views.ProjectSelectItemView = views.ext.SelectModelView.extend({ + tpl: '#project-select-model-tpl', + + select: function() { + views.ProjectSelectItemView.__super__.select.call(this); + this.model.trigger("change:_quotas"); + }, + + deselect: function() { + views.ProjectSelectItemView.__super__.deselect.call(this); + this.model.trigger("change:_quotas"); + }, + + set_current: function() { + this._is_current = true; + this.model.trigger('change:_project_is_current'); + $(this.el).addClass("current"); + this.set_enabled(); + }, + + unset_current: function() { + this._is_current = false; + this.model.trigger('change:_project_is_current'); + $(this.el).removeClass("current"); + }, + + is_current_str: function() { + if (this._is_current) { + return 'current' + } else { + return '' + } + }, + + quotas_html: function() { + var data = "<div>"; + var resolve = this.options.quotas_keys.length; + var found = 0; + _.each(this.options.quotas_keys, function(key) { + var q = this.model.quotas.get(key); + if (!q) { return } + found += 1; + + var limit = q.get_readable("limit"); + var usage = q.get("usage"); + + if (this.selected && !this._is_current) { + var model_usage = this.parent_view.model_usage; + var model = this.parent_view.resource_model; + var usage_keys = model_usage.call(this.parent_view, model); + var use = usage_keys[key]; + if (use) { + usage = usage + use; + } + } + + if (!this.selected && this._is_current) { + var model_usage = this.parent_view.model_usage; + var model = this.parent_view.resource_model; + var usage_keys = model_usage.call(this.parent_view, model); + var use = usage_keys[key]; + if (use) { + usage = usage - use; + if (usage < 0) { usage = 0; } + } + } + + usage = q.get_readable("usage", false, usage); + usage = "{0}/{1}".format(usage, limit); + if (q.infinite()) { + usage = "Unlimited"; + } + var content = '<span class="resource-key">{0}:</span>'; + content += '<span class="resource-value">{1}</span>'; + data += content.format(q.get('resource').get('display_name'), + usage); + }, this); + if (found == 0) { + data += "<p>No resources available</p>" + } + data += "</div>"; + return data; + } + }); + + views.ProjectSelectView = views.ext.CollectionView.extend({ + initialize: function(options) { + this.filter_func = options.filter_func || function(m) { return true } + this.update_disabled = _.bind(this.update_disabled, this); + views.ProjectSelectView.__super__.initialize.apply(this); + }, + + get_selected: function() { + var selected = undefined; + _.each(this._model_views, function(view, id){ + if (view.selected) { selected = view.model } + }); + return selected; + }, + + post_remove_model_view: function(view) { + view.unbind('click'); + }, + + disabled_filter: function(model) { + if (this.filter_func) { + return !this.filter_func(model); + } + return false; + }, + + post_add_model_view: function(view, model) { + this.update_disabled(view); + view.bind('click', function() { + if (view.disabled) { return } + this.deselect_all(); + }, this); + }, + + _model_bindings: {}, + + bind_custom_view_handlers: function(view, model) { + var func + model.quotas.bind('change', _.bind(function() { + this.update_disabled(view) + }, this)); + }, + + unbind_custom_view_handlers: function(view, model) { + model.quotas.unbind('change'); + }, + + deselect_all: function(model) { + this.collection.each(function(m) { + var view = this._model_views[m.id]; + //if (view.disabled) { return } + this._model_views[m.id].deselect(); + }, this); + }, + + set_selected: function(model) { + this.deselect_all(); + if (this._model_views[model.id]) { + this._model_views[model.id].select(); + } + }, + + set_current: function(model) { + _.each(this._model_views, function(v) { + v.unset_current(); + }); + if (this._model_views[model.id]) { + this._model_views[model.id].set_current(); + } + }, + + model_view_options: function(model) { + return {'quotas_keys': this.options.quotas_keys}; + }, + + tpl: '#project-select-collection-tpl', + model_view_cls: views.ProjectSelectItemView + }); + + views.ModelReassignView = views.Overlay.extend({ + title: "Reassign machine", + overlay_id: "overlay-select-projects", + content_selector: "#project-select-content", + css_class: "overlay-info", + can_fit_func: function(project) { + return project.quotas.can_fit(this.model_usage(this.model)) + }, + + initialize: function(options) { + views.ModelReassignView.__super__.initialize.apply(this); + this.list = this.$(".projects-list ul"); + this.empty_message = this.$(".empty-message"); + this.submit_button = this.$(".form-action.submit"); + this.in_progress = false; + this.init_handlers(); + + this.$(".description").text(this.description || ""); + if (this.collection) { this.init_collection_view(this.collection) } + }, + + init_collection_view: function(collection) { + if (this.collection_view) { this.collection_view.destroy() } + this.collection_view = new views.ProjectSelectView({ + collection: collection, + el: this.list, + filter_func: _.bind(this.can_fit_func, this), + quotas_keys: this.resources + }); + + this.collection_view.bind("change:select", function(item) { + this.handle_project_change(item.model); + }, this); + + this.collection_view.show(true); + this.collection_view.model_usage = this.model_usage; + this.collection_view.resource_model = this.model; + var project = this.model.get('project'); + if (project && !(project.get('missing') && !project.get('resolved'))) { + this.collection_view.set_current(this.model.get('project')); + this.collection_view.set_selected(this.model.get('project')); + } + this.list.append($(this.collection_view.el)); + this.handle_project_change(); + }, + + handle_project_change: function(project) { + var project = project || this.collection_view.get_selected(); + if (project.id == this.model.get('project').id) { + this.submit_button.addClass("disabled"); + } else { + this.submit_button.removeClass("disabled"); + } + }, + + init_handlers: function() { + this.submit_button.bind('click', _.bind(this.submit, this)); + }, + + submit: function() { + if (this.submit_button.hasClass("disabled")) { return } + if (this.in_progress) { return } + var project = this.collection_view.get_selected(); + if (project.id == this.model.get('project').id) { + this.hide(); + } + var complete = _.bind(function() { + this.submit_button.removeClass("in-progress"); + this.in_progress = false; + synnefo.storage.projects.delay_fetch(2000); + this.hide(); + }, this); + this.assign_to_project(this.model, project, complete, complete); + this.submit_button.addClass("in-progress"); + this.in_progress = true; + }, + + update_model_details: function() { + this.set_subtitle(this.model.get('name')); + }, + + show: function(model) { + this.model = model; + this.init_collection_view(synnefo.storage.projects); + views.ModelReassignView.__super__.show.call(this); + this.update_model_details(); + }, + + onClose: function() { + if (this.collection_view) { + this.collection_view.destroy(); + } + delete this.collection_view; + } + }); + + views.VmReassignView = views.ModelReassignView.extend({ + title: 'Set machine project', + description: 'Select project assign machine to', + resources: ['cyclades.vm', 'cyclades.ram', + 'cyclades.cpu', 'cyclades.disk'], + + model_usage: function(model) { + var quotas = model.get_flavor().quotas(); + var total = false; + if (model.get("status") == "STOPPED") { + total = true; + } + return quotas; + }, + + can_fit_func: function(project) { + var quotas = this.model.get_flavor().quotas(); + var total = false; + if (this.model.get("status") == "STOPPED") { + total = true; + } + return project.quotas.can_fit(quotas, total); + }, + + update_model_details: function() { + var name = _.escape(synnefo.util.truncate(this.model.get("name"), 70)); + this.set_subtitle(name + snf.ui.helpers.vm_icon_tag(this.model, "small")); + + var el = $("<div></div>"); + var cont = $(this.el).find(".model-usage"); + cont.empty().append(el); + + var flavor = this.model.get_flavor(); + _.each(['ram', 'disk', 'cpu'], function(key) { + var res = synnefo.storage.resources.get('cyclades.' + key) + el.append($('<span class="key">' + res.get('display_name') + + ':</span>')); + el.append($('<span class="value">' + + flavor.get_readable(key) + '</span>')); + }); + }, + + assign_to_project: function(model, project, complete, fail) { + model.call("reassign", complete, fail, {project_id: project.id}); + } + }); + + views.IPReassignView = views.ModelReassignView.extend({ + title: 'Set IP address project', + description: 'Select project to assign IP address to', + resources: ['cyclades.floating_ip'], + model_usage: function() { return {'cyclades.floating_ip': 1}}, + assign_to_project: function(model, project, complete, fail) { + this.model.reassign_to_project(project, complete, complete); + } + }); + + views.NetworkReassignView = views.ModelReassignView.extend({ + title: 'Set private network project', + resources: ['cyclades.network.private'], + description: 'Select project to assign private network to', + model_usage: function() { return {'cyclades.network.private': 1}}, + assign_to_project: function(model, project, complete, fail) { + this.model.reassign_to_project(project, complete, complete); + } + }); + + views.VolumeReassignView = views.ModelReassignView.extend({ + title: 'Set disk project', + resources: ['cyclades.disk'], + description: 'Select project to assign disk to', + model_usage: function(model) { + return {'cyclades.disk': model.get('size') * Math.pow(1024, 3)} + }, + assign_to_project: function(model, project, complete, fail) { + this.model.reassign_to_project(project, complete, complete); + } + }); +})(this); diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_router.js b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_router.js index f89e4fc20953604b3e78f63de9793c21d62c9543..883edf4fd9aebf8facad84f37c900f0dc772d16b 100644 --- a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_router.js +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_router.js @@ -1,35 +1,17 @@ -// Copyright 2011 GRNET S.A. All rights reserved. -// -// Redistribution and use in source and binary forms, with or -// without modification, are permitted provided that the following -// conditions are met: -// -// 1. Redistributions of source code must retain the above -// copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following -// disclaimer in the documentation and/or other materials -// provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// -// The views and conclusions contained in the software and -// documentation are those of the authors and should not be -// interpreted as representing official policies, either expressed -// or implied, of GRNET S.A. +// Copyright (C) 2010-2014 GRNET S.A. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. // ;(function(root){ @@ -63,10 +45,9 @@ "machines/single/details/:vm": "vm_details_view", "machines/single/": "vms_single_view", - // network views "networks/": "networks_view", - // network views "ips/": "ips_view", + "disks/": "volumes_view", "public-keys/": "public_keys_view", ":hash": "fallback" }, @@ -138,6 +119,11 @@ this.navigate("ips/"); ui.main.show_view("ips"); }, + + volumes_view: function() { + this.navigate("disks/"); + ui.main.show_view("volumes"); + }, public_keys_view: function() { this.navigate("public-keys/"); diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_single_view.js b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_single_view.js index dd9b2319e3b9fbd384286e33c7a5a5e73948f5c0..ce420a8e7b0978cb3a44665edcb4698df575999a 100644 --- a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_single_view.js +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_single_view.js @@ -1,35 +1,17 @@ -// Copyright 2011 GRNET S.A. All rights reserved. -// -// Redistribution and use in source and binary forms, with or -// without modification, are permitted provided that the following -// conditions are met: -// -// 1. Redistributions of source code must retain the above -// copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following -// disclaimer in the documentation and/or other materials -// provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// -// The views and conclusions contained in the software and -// documentation are those of the authors and should not be -// interpreted as representing official policies, either expressed -// or implied, of GRNET S.A. +// Copyright (C) 2010-2014 GRNET S.A. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. // ;(function(root){ @@ -49,51 +31,96 @@ // shortcuts var bb = root.Backbone; var hasKey = Object.prototype.hasOwnProperty; - - views.VMSinglePortListView = views.VMPortListView.extend({ - - init: function() { - views.VMSinglePortListView.__super__.init.apply(this); - this.open = false; - this.vm_el = $(this.options.vm_view); - this.tags_toggler = this.vm_el.find(".tags-header"); - this.tags_content = this.vm_el.find(".tags-content"); - this.toggler = this.vm_el.find(".toggler-header.ips"); - this.toggler_content = this.vm_el.find(".ips-content"); - this.toggler_content.hide(); - $(this.el).show(); - var self = this; - this.toggler.click(function() { - var disabled = self.toggler.parent().find(".cont-toggler-wrapper").hasClass("disabled"); - if (disabled) { return; } - self.toggle(); - }); - - this.tags_toggler.click(function() { - self.toggler.find(".toggler").removeClass("open"); - var f = function() { self.hide(true) } - self.toggler_content.slideUp(f); - }); - }, - - toggle: function() { - var self = this; - this.open = !this.open; - - if (this.open) { - this.show(true); - this.tags_toggler.find(".toggler").removeClass("open"); - this.tags_content.slideUp(); - this.toggler.find(".toggler").addClass("open"); - this.toggler_content.removeClass(".hidden").slideDown(); - } else { - this.toggler.find(".toggler").removeClass("open"); - var f = function() { self.hide(true) } - this.toggler_content.removeClass(".hidden").slideUp(); + views.SingleListViewMixin = { + + toggler_id: 'ips', + + init_togglers: function() { + var self = this; + + this._open = false; + this.vm_el = $(this.options.vm_view); + this.tags_toggler = this.vm_el.find(".tags-header"); + this.tags_content = this.vm_el.find(".tags-content"); + this.toggler = this.vm_el.find( + ".toggler-header." + this.toggler_id); + this.toggler_content = this.vm_el.find( + "."+this.toggler_id+"-content"); + this.toggler_content.hide(); + this.other_togglers = this.vm_el.find(".cont-toggler-wrapper"); + + $(this.el).show(); + + this.other_togglers.click(function() { + var toggler = $(this); + if (!toggler.hasClass(self.toggler_id) && self._open) { + self.toggle(); + } + }); + + this.toggler.click(function() { + var disabled = self.toggler.parent().find( + ".cont-toggler-wrapper").hasClass("disabled"); + if (disabled) { return; } + self.toggle(); + }); + + this.tags_toggler.click(function() { + self.toggler.find(".toggler").removeClass("open"); + self.toggler_content.slideUp(f); + var f = function() { + self.hide(true); + } + self._open = false; + }); + }, + + toggle: function() { + var self = this; + this._open = !this._open; + + if (this._open) { + this.show(true); + this.tags_toggler.find(".toggler").removeClass("open"); + this.tags_content.slideUp(); + this.toggler.find(".toggler").addClass("open"); + this.toggler_content.removeClass(".hidden").slideDown(); + } else { + this.toggler.find(".toggler").removeClass("open"); + var f = function() { self.hide(true) } + this.toggler_content.removeClass(".hidden").slideUp(); + } } - } - }); + }; + + views.VMSingleVolumesListView = views.VMVolumeListView.extend(_.extend({ + init: function() { + views.VMSingleVolumesListView.__super__.init.apply(this); + this.init_togglers(); + }, + + hide: function() { + views.VMSingleVolumesListView.__super__.init.apply(this); + this._open = false; + } + }, views.SingleListViewMixin, { + toggler_id: 'ips' + })); + + views.VMSinglePortListView = views.VMPortListView.extend(_.extend({ + init: function() { + views.VMSinglePortListView.__super__.init.apply(this); + this.init_togglers(); + }, + + hide: function() { + views.VMSingleVolumesListView.__super__.init.apply(this); + this._open = false; + } + }, views.SingleListViewMixin, { + toggler_id: 'volumes' + })); views.SingleDetailsView = views.VMDetailsView.extend({ @@ -253,6 +280,7 @@ this.action_views = this.action_views || {}; this.action_error_views = this.action_error_views || {}; this.ports_views = this.ports_views || {}; + this.volumes_views = this.volumes_views || {}; //this.stats_views[vm.id] = new views.IconStatsView(vm, this); @@ -266,6 +294,9 @@ var ports_container = this.vm(vm).find(".ips-content"); var ports_toggler = this.vm(vm).find(".toggler-header.ips"); + + var volumes_container = this.vm(vm).find(".volumes-content"); + var volumes_toggler = this.vm(vm).find(".toggler-header.volumes"); var ports_view = new views.VMSinglePortListView({ vm_view: this.vm(vm), @@ -276,14 +307,34 @@ }); this.ports_views[vm.id] = ports_view ports_view.show(); - ports_view.el.hide(); + var volumes_view = new views.VMSingleVolumesListView({ + vm_view: this.vm(vm), + collection: vm.volumes, + container: volumes_container, + parent: this, + truncate: 50 + }); + this.volumes_views[vm.id] = volumes_view; + volumes_view.show(); + if (storage.vms.models.length > 1) { this.vm(vm).hide(); }; }, post_update_vm: function(vm) { + if (vm.in_error_state()) { + var view = this.ports_views[vm.id]; + view.toggler.find(".toggler").removeClass("open"); + view.toggler_content.hide(); + view.hide(true); + view.open = false; + } }, + hide_vm: function(vm) { + this.vm(vm).hide(); + }, + // vm specific event handlers set_vm_handlers: function(vm) { }, @@ -295,16 +346,14 @@ this.$(".server-name").removeClass("column3-selected"); - if (vm) { - this.vm(vm).show(); - }; - _.each(storage.vms.models, function(vmo){ if (vm && (vm.id != vmo.id)) { if (!hasKey.call(this._vm_els, vmo.id)) { return }; - this.vm(vmo).hide(); + this.hide_vm(vmo); } - }, this) + }, this); + + if (vm) { this.vm(vm).show(); }; if (!vm) { // empty list @@ -354,7 +403,6 @@ // called once after each vm has been updated update_layout: function() { this.update_current_vm(); - fix_v6_addresses(); }, update_status_message: function(vm) { @@ -383,9 +431,13 @@ var el = this.vm(vm); if (vm != this.current_vm_instance) { return }; + var project = vm.get('project'); + if (project) { + el.find(".project-name").text(_.truncate(project.get('name'), 20)); + } // truncate name - el.find(".machine-detail.name").text(util.truncate(vm.get("name"), 53)); - el.find(".fqdn").text(vm.get("fqdn") || synnefo.config.no_fqdn_message); + el.find(".machine-detail.name").text(util.truncate(vm.get("name"), 45)); + el.find(".fqdn").val(vm.get("fqdn") || synnefo.config.no_fqdn_message); // set the state (i18n ??) el.find(".state-label").text(STATE_TEXTS[vm.state()]); // set state class @@ -454,6 +506,8 @@ 'START': ['state', 'starting-state'], 'CONNECT': ['state', 'connecting-state'], 'DISCONNECT': ['state', 'disconnecting-state'], + 'ATTACH_VOLUME': ['state', 'connecting-state'], + 'DETACH_VOLUME': ['state', 'disconnecting-state'], 'RESIZE': ['state', 'rebooting-state'] }; diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_vm_resize_view.js b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_vm_resize_view.js index d42b41765ae884215077914339031664c65f4a31..ef5b7c579565b5ec876b9a0d6e98c02cd20ebb7a 100644 --- a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_vm_resize_view.js +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_vm_resize_view.js @@ -1,35 +1,17 @@ -// Copyright 2013 GRNET S.A. All rights reserved. -// -// Redistribution and use in source and binary forms, with or -// without modification, are permitted provided that the following -// conditions are met: -// -// 1. Redistributions of source code must retain the above -// copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following -// disclaimer in the documentation and/or other materials -// provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// -// The views and conclusions contained in the software and -// documentation are those of the authors and should not be -// interpreted as representing official policies, either expressed -// or implied, of GRNET S.A. +// Copyright (C) 2010-2014 GRNET S.A. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. // ;(function(root){ @@ -71,6 +53,7 @@ this.quotas = options.quotas || synnefo.storage.quotas; this.selected_flavor = options.selected_flavor || undefined; this.extra_quotas = options.extra_quotas || undefined; + this.project = options.project; this.render(); if (this.selected_flavor) { this.set_flavor(this.selected_flavor)} }, @@ -158,7 +141,7 @@ set_unavailable: function() { this.$el.find("li.choice").removeClass("disabled"); - var quotas = this.quotas.get_available_for_vm({'active': true}); + var quotas = this.project.quotas.get_available_for_vm({'active': true}); var extra_quotas = this.extra_quotas; var user_excluded = storage.flavors.unavailable_values_for_quotas( quotas, @@ -223,7 +206,7 @@ view_id: "vm_resize_view", content_selector: "#vm-resize-overlay-content", - css_class: 'overlay-vm-resize overlay-info', + css_class: 'overlay-vm-resize overlay-info create-wizard-overlay', overlay_id: "vm-resize-overlay", subtitle: "", @@ -278,10 +261,11 @@ submit_resize: function(flv) { if (this.submit.hasClass("in-progress")) { return } this.submit.addClass("in-progress"); + var vm = this.vm; var complete = _.bind(function() { - this.vm.set({'flavor': flv}); - this.vm.set({'flavorRef': flv.id}); - this.hide(); + vm.set({'flavor': flv}); + vm.set({'flavorRef': flv.id}); + this.vm && this.hide(); }, this); this.vm.call("resize", complete, complete, {flavor:flv.id}); }, @@ -296,6 +280,7 @@ this.start_warning.hide(); this.submit.removeClass("in-progress"); this.vm = vm; + this.project = vm.get('project'); this.vm.bind("change", this.handle_vm_change); if (this.flavors_view) { this.flavors_view.remove(); @@ -314,7 +299,8 @@ el: this.$(".flavor-options-inner-cont div"), hidden_choices:['disk', 'disk_template'], selected_flavor: this.vm.get_flavor(), - extra_quotas: extra_quota + extra_quotas: extra_quota, + project: this.project }); this.selected_flavor = this.vm.get_flavor(); this.handle_flavor_select(this.selected_flavor); @@ -407,6 +393,7 @@ }, onClose: function() { + if (!this.visible()) { return } this.editing = false; this.vm.unbind("change", this.handle_vm_change); this.vm.unbind("change:status", this.handle_shutdown_complete); diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_vms_base_view.js b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_vms_base_view.js index b1e2bfb5a02a096e0a116c47a3990a75dd72d686..cd7e1c39c9c58ee9ddb35939f0e2b503c7243b21 100644 --- a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_vms_base_view.js +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_vms_base_view.js @@ -1,35 +1,17 @@ -// Copyright 2011 GRNET S.A. All rights reserved. -// -// Redistribution and use in source and binary forms, with or -// without modification, are permitted provided that the following -// conditions are met: -// -// 1. Redistributions of source code must retain the above -// copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following -// disclaimer in the documentation and/or other materials -// provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// -// The views and conclusions contained in the software and -// documentation are those of the authors and should not be -// interpreted as representing official policies, either expressed -// or implied, of GRNET S.A. +// Copyright (C) 2010-2014 GRNET S.A. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. // ;(function(root){ @@ -247,6 +229,14 @@ var self = this; var el = this.vm(vm); + var project = vm.get('project'); + if (project) { + project.bind('change', function() { + el.find(".project-name").text( + _.truncate(project.get('name'), 20)); + }, this); + }; + // hidden feature, double click on indicators to display // vm diagnostics. el.find(".indicators").bind("dblclick", function(){ @@ -298,24 +288,39 @@ _.each(vms, _.bind(function(vm){ // vm will be removed // no need to update - if (vm.get("status") == "DELETED") { - return; - } - - // this won't add it additional times + if (vm.get("status") == "DELETED") { return; } this.add(vm); this.update_vm(vm); - }, this)) + }, this)); // update view stuff this.__update_layout(); }, + disable_toggler: function(vm, t) { + this.vm(vm).find(".cont-toggler-wrapper."+t).addClass("disabled"); + var info_view = this.info_views && this.info_views[vm.id]; + var el = info_view && info_view[t+'_el']; + if (el) { + el.hide(); + } + }, + + enable_toggler: function(vm, t) { + this.vm(vm).find(".cont-toggler-wrapper." + t).removeClass("disabled"); + }, + update_toggles_visibility: function(vm) { if (vm.is_building() || vm.in_error_state() || vm.get("status") == "DESTROY") { - this.vm(vm).find(".cont-toggler-wrapper.ips").addClass("disabled"); + this.disable_toggler(vm, 'ips'); + this.disable_toggler(vm, 'volumes'); } else { - this.vm(vm).find(".cont-toggler-wrapper.ips").removeClass("disabled"); + this.enable_toggler(vm, 'ips'); + if (vm.volumes.length) { + this.enable_toggler(vm, 'volumes'); + } else { + this.disable_toggler(vm, 'volumes'); + } } }, @@ -335,10 +340,10 @@ } var el = this.vm(vm); - if (vm.can_resize()) { - el.addClass("can-resize"); + if (!vm.in_error_state()) { + el.addClass("can-resize"); } else { - el.removeClass("can-resize"); + el.removeClass("can-resize"); } if (vm.get('suspended')) { @@ -397,6 +402,10 @@ } }, + show_reassign_view: function(vm) { + synnefo.ui.main.vm_reassign_view.show(vm); + }, + show_indicator: function(vm, action) { var action = action || vm.pending_action; this.sel('vm_wave', vm.id).hide(); @@ -507,6 +516,16 @@ el.addClass("disabled-visible") }, + set_can_resize: function() { + var el = $(this.el).find("a.action-resize").parent(); + el.removeClass("disabled-visible"); + }, + + set_cannot_resize: function() { + var el = $(this.el).find("a.action-resize").parent(); + el.addClass("disabled-visible"); + }, + // update the actions layout, depending on the selected actions update_layout: function() { @@ -516,6 +535,12 @@ } else { this.set_cannot_start(); } + + if (this.vm.can_resize()) { + this.set_can_resize(); + } else { + this.set_cannot_resize(); + } } if (!this.vm_handlers_initialized) { @@ -574,6 +599,8 @@ this.view.hide_indicator(this.vm); } + var vm_view = this.view.vm(this.vm); + vm_view.removeClass("action-pending"); // update action link styles and shit _.each(models.VM.ACTIONS, function(action, index) { if (actions.indexOf(action) > -1) { @@ -591,6 +618,7 @@ this.action_confirm(action).show(); this.action(action).removeClass("disabled"); this.action_link(action).addClass("selected"); + vm_view.addClass("action-pending"); } else { this.action_confirm_cont(action).hide(); this.action_confirm(action).hide(); @@ -608,7 +636,7 @@ try { this.vm.unbind("action:fail", this.update_layout) this.vm.unbind("action:fail:reset", this.update_layout) - } catch (err) { console.log("Error")}; + } catch (err) { console.error(err)}; this.vm.bind("action:fail", this.update_layout) this.vm.bind("action:fail:reset", this.update_layout) @@ -636,6 +664,11 @@ // initial hide if (this.hide) { $(this.el).hide() }; + if (this.$('.snapshot').length) { + this.$('.snapshot').click(_.bind(function() { + synnefo.ui.main.create_snapshot_view.show(this.vm); + }, this)); + } // action links events _.each(models.VM.ACTIONS, function(action) { var action = action; @@ -654,14 +687,22 @@ // action links click events $(this.el).find(".action-container."+action+" a").click(function(ev) { ev.preventDefault(); - if (action == "start" && !self.vm.can_start() && !vm.in_error_state()) { + if ( + action == "start" && + !self.vm.can_start() && + !vm.in_error_state()) { + if (!vm.can_resize()) { return } ui.main.vm_resize_view.show_with_warning(self.vm); return; } if (action == "resize") { - ui.main.vm_resize_view.show(self.vm); - return; + //if (!vm.can_resize()) { return } + ui.main.vm_resize_view.show(self.vm); + return; + } else if (action == "reassign") { + ui.main.vm_reassign_view.show(self.vm); + return; } else { self.set(action); } @@ -785,7 +826,7 @@ views.VMPortView = views.ext.ModelView.extend({ tpl: '#vm-port-view-tpl', - classes: 'port-item clearfix', + classes: 'inner-item port-item clearfix', update_in_progress: function() { if (this.model.get("in_progress_no_vm")) { @@ -857,4 +898,19 @@ model_view_cls: views.VMPortIpView }); + views.VMVolumeView = views.ext.ModelView.extend({ + tpl: '#vm-volume-view-tpl', + classes: 'volume-item clearfix inner-item', + show_snapshot_create_overlay: function() { + var vm = this.model.get('vm'); + if (!vm) { return } + synnefo.ui.main.create_snapshot_view.show(vm, this.model); + } + + }); + + views.VMVolumeListView = views.ext.CollectionView.extend({ + tpl: '#vm-volume-list-view-tpl', + model_view_cls: views.VMVolumeView }); + })(this); diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_volumes_view.js b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_volumes_view.js new file mode 100644 index 0000000000000000000000000000000000000000..f77fb61503ca71ea48923fbd6c0e420d3d3efd18 --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_volumes_view.js @@ -0,0 +1,974 @@ +// Copyright (C) 2010-2014 GRNET S.A. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. +// + +;(function(root){ + + // root + var root = root; + + // setup namepsaces + var snf = root.synnefo = root.synnefo || {}; + var models = snf.models = snf.models || {} + var storage = snf.storage = snf.storage || {}; + var ui = snf.ui = snf.ui || {}; + var util = snf.util || {}; + var views = snf.views = snf.views || {} + + // shortcuts + var bb = root.Backbone; + + // logging + var logger = new snf.logging.logger("SNF-VIEWS"); + var debug = _.bind(logger.debug, logger); + + + var min_volume_quota = { + 'cyclades.disk': 1 + }; + + views.ext.VM_STATUS_CLS_MAP = { + 'UNKNOWN': ['status-error'], + 'BUILD': ['build-state status-progress'], + 'REBOOT': ['status-progress reboot-state'], + 'STOPPED': ['status-terminated'], + 'ACTIVE': ['status-active'], + 'ERROR': ['status-error'], + 'DELETED': ['destroying-state'], + 'DESTROY': ['destroying-state'], + 'SHUTDOWN': ['shutting-state'], + 'START': ['starting-state'], + 'CONNECT': ['connecting-state'], + 'DETACH_VOLUME': ['disconnecting-state'], + 'ATTACH_VOLUME': ['connecting-state'], + 'DISCONNECT': ['disconnecting-state'], + 'RESIZE': ['rebooting-state'] + }; + + views.CreateVolumeSelectProjectView = + views.CreateVMSelectProjectView.extend({ + tpl: '#create-view-projects-select-tpl', + required_quota: function() { + var size = Math.pow(1024, 3); + var img = this.parent_view.parent.steps[1].selected_image; + + if (img && !img.id == "empty-disk") { + size = img.get("size"); + } + return {'cyclades.disk': size} + }, + model_view_cls: views.CreateVMSelectProjectItemView.extend({ + display_quota: min_volume_quota + }) + }); + + views.CreateVolumeImageStepView = views.CreateImageSelectView.extend({ + step: 1, + + default_type: 'empty', + type_selections: {}, + type_selections_order: [], + + initialize: function() { + views.CreateVolumeImageStepView.__super__.initialize.apply(this, arguments); + this.$(".other-types-cont").removeClass("hidden"); + + this.empty_image = new synnefo.glance.models.GlanceImage(); + this.empty_image.set({ + id: "empty-disk", + name: "Empty disk", + size: 0, + description: "Empty disk" + }); + this.create_types_selection_options(); + this.create_snapshot_types_selection_options(); + }, + + get_image_icon_tag: function(image) { + if (image.get("id") == "empty-disk") { + var url = snf.config.images_url + "volume-icon-small.png"; + return '<img src="{0}" />'.format(url); + } + return views.CreateVolumeImageStepView.__super__.get_image_icon_tag.call(this, image); + }, + + display_warning_for_image: function(image) { + if (image && !image.is_system_image() && + !image.owned_by(synnefo.user) && image.get("id") != "empty-disk") { + this.parent.el.find(".image-warning").show(); + this.parent.el.find(".create-controls").hide(); + } else { + this.parent.el.find(".image-warning").hide(); + this.parent.el.find(".create-controls").show(); + } + }, + + update_images: function(images) { + if (this.selected_type == "empty") { + this.images = [this.empty_image]; + this.images_ids = [this.empty_image.get("id")]; + return this.images; + } + return views.CreateVolumeImageStepView.__super__.update_images.call(this, images); + }, + + get_available_projects_for_disk_size: function(size) { + var check = function(p) { + var q = p.quotas.get('cyclades.disk'); + if (!q) { return false } + return q.get('available') >= size; + } + return synnefo.storage.projects.filter(check) + }, + + validate: function() { + if (!this.selected_image) { + this.parent.$(".form-action.next").hide(); + } else { + if (this.get_available_projects_for_disk_size( + this.selected_image.get("size")).length > 0) { + this.parent.$(".form-action.next").show(); + this.parent.$(".no-project-notice").hide(); + } else { + this.parent.$(".form-action.next").hide(); + this.parent.$(".no-project-notice").show(); + } + + } + } + }); + + views.CreateVolumeProjectStepView = views.CreateWizardStepView.extend({ + step: 2, + + new_volume_title: "New disk", + + initialize: function() { + views.CreateVolumeDetailsStepView.__super__.initialize.apply( + this, arguments); + this.parent.bind("image:change", + _.bind(this.handle_image_change, this)); + this.parent.bind("project:change", + _.bind(this.handle_project_change, this)); + this.projects_list = this.$(".project-select"); + this.project_select_view = undefined; + + this.size_input = this.el.find(".size-slider"); + this.size_input.bind("keyup", _.bind(function() { + window.setTimeout(_.bind(function() { + var value = this.size_input.val(); + if (!parseInt(value)) { + value = this.min_size; + this.size_input.val(value); + } + if (parseInt(value)) { + this.size_input.simpleSlider("setValue", value); + } + }, this), 50); + }, this)); + this.size_input.simpleSlider(this.slider_settings); + this.min_size = 1; + }, + + slider_settings: { + range: [1, 1000], + theme: 'volume-size', + step: 1 + }, + + set_slider_max: function(val) { + this.size_input.simpleSlider("setMax", val); + }, + + set_slider_min: function(val) { + if (val < 1) { val = 1 } + this.size_input.simpleSlider("setMin", val); + }, + + init_subviews: function() { + if (!this.project_select_view) { + var view_cls = views.CreateVolumeSelectProjectView; + this.project_select_view = new view_cls({ + container: this.projects_list, + collection: synnefo.storage.joined_projects, + parent_view: this + }); + + this.project_select_view.show(true); + } + + this.project_select_view.bind("change", + _.bind(this.handle_project_select, this)); + + this.project_select_view.set_current(this.parent.project); + this.handle_project_select(this.parent.project); + }, + + hide_step: function() { + window.setTimeout(_.bind(function() { + this.project_select_view && this.project_select_view.hide(true); + }, this), 50); + }, + + hide: function() { + this.project_select_view.unbind("change"); + this.project_select_view && this.project_select_view.hide(true); + views.CreateVolumeProjectStepView.__super__.hide.apply(this, arguments); + }, + + update_layout: function() { + }, + + handle_project_select: function(projects) { + if (!projects.length ) { return } + var project = projects[0]; + this.parent.set_project(project); + }, + + handle_project_change: function() { + if (!this.parent.project) { return } + var disk = this.parent.project.quotas.get("cyclades.disk"); + var available = disk.get("available"); + var max_size = synnefo.config.volume_max_size; + available = available / Math.pow(1024, 3); + if (disk.infinite()) { + available = max_size; + } + if (available > max_size) { available = max_size; } + this.set_slider_max(parseInt(available)); + this.update_layout(); + }, + + handle_image_change: function(image) { + if (!this.parent.project) { return } + this.current_image = image; + var size = image ? image.get("size") : 1; + size = size / Math.pow(1024, 3); + if (size > parseInt(size)) { + size = parseInt(size) + 1; + } + this.set_slider_min(size); + this.min_size = size || 1; + this.update_layout(); + }, + + reset_slider: function() { + this.size_input.simpleSlider("setRatio", 0); + }, + + select_available_project: function() { + var img_view = this.parent.steps[1]; + var img = img_view.selected_image; + var size = 1 * Math.pow(1024, 3); + if (img) { size = img.get('size'); } + var current = this.parent.project; + if (current.quotas.can_fit({'cyclades.disk': size})) { return } + var projects = img_view.get_available_projects_for_disk_size(size); + if (!projects.length) { return } + this.project_select_view.set_current(projects[0]); + }, + + show: function() { + var args = _.toArray(arguments); + this.init_subviews(); + this.project_select_view.show(true); + this.select_available_project(); + this.reset_slider(); + views.CreateVolumeProjectStepView.__super__.show.apply(this, arguments); + }, + + get: function() { + return { + 'project': this.parent.project, + 'size': parseInt(this.size_input.val()) || 1 + } + } + }); + + views.CreateVolumeMachineStepView = views.CreateWizardStepView.extend({ + step: 3, + initialize: function() { + views.CreateVolumeMachineStepView.__super__.initialize.apply( + this, arguments); + this.vm_select_view = undefined; + }, + + update_layout: function() { + }, + + init_subviews: function() { + if (!this.vm_select_view) { + this.vms_collection = new Backbone.FilteredCollection(undefined, { + collection: synnefo.storage.vms, + collectionFilter: this.vm_filter + }); + this.vm_select_view = new views.VMSelectView({ + collection: this.vms_collection, + container: this.$(".vms-list"), + parent: this, + allow_multiple: false, + allow_empty: false + }); + this.vm_select_view.max_title_length = 38; + this.vm_select_view.show(true); + } + + this.vm_select_view.bind("deselect", + _.bind(this.handle_vm_select, this)); + this.vm_select_view.bind("change", + _.bind(this.handle_vm_select, this)); + this.vm_select_view.bind("change:select", + _.bind(this.handle_vm_select, this)); + }, + + vm_filter: function(m) { + return m.can_attach_volume(); + }, + + disabled_filter_ext: function(m) { + var check = !m.can_attach_volume() || !m.is_ext(); + if (check) { + return "You can only attach an empty disk to this machine." + } + return false; + }, + + disabled_filter: function(m) { + return !m.can_attach_volume(); + }, + + handle_vm_select: function() { + if (this.vm_select_view && this.vm_select_view.get_selected().length) { + this.parent.next_btn.show(); + } else { + this.parent.next_btn.hide(); + } + }, + + validate_vms: function() { + if (this.vm_select_view.get_selected().length == 0) { + this.parent.next_btn.hide(); + } else { + this.parent.next_btn.show(); + } + }, + + update_vms_filter: function() { + var img_view = this.parent.steps[1]; + var img = img_view.selected_image; + + if (img && img.id != "empty-disk") { + this.vm_select_view.disabled_filter = this.disabled_filter_ext; + } else { + this.vm_select_view.disabled_filter = this.disabled_filter; + } + this.vm_select_view.update_disabled(); + }, + + reset: function() { + }, + + hide_step: function() { + window.setTimeout(_.bind(function() { + this.vm_select_view && this.vm_select_view.hide(true); + }, this), 200); + }, + + hide: function() { + this.vm_select_view.unbind("deselect"); + this.vm_select_view.unbind("change"); + views.CreateVolumeMachineStepView.__super__.hide.apply(this, arguments); + }, + + show: function() { + var args = _.toArray(arguments); + this.init_subviews(); + this.validate_vms(); + this.vm_select_view.show(true); + this.update_vms_filter(); + views.CreateVolumeMachineStepView.__super__.show.apply(this, arguments); + }, + + get: function() { + return { + 'vm': this.vm_select_view.get_selected()[0] + } + } + + }); + + views.CreateVolumeDetailsStepView = views.CreateWizardStepView.extend({ + step: 4, + initialize: function() { + views.CreateVolumeDetailsStepView.__super__.initialize.apply( + this, arguments); + this.desc_input = this.$(".volume-info"); + this.name_input = this.$(".volume-name"); + this.name_changed = false; + this.desc_changed = false; + this.name_input.keyup( _.bind(function() { + this.name_changed = true; + }, this)); + this.desc_input.keyup(_.bind(function() { + this.desc_changed = true; + }, this)); + }, + + update_volume_details: function() { + var img = this.parent.steps[1].selected_image; + if (!this.name_changed && !this.desc_changed) { + if (img) { + this.name_input.val(img.get('name') + ' volume'); + this.desc_input.val(img.get('description')); + } else { + this.name_input.val(this.new_volume_title); + this.desc_input.val(''); + } + } + }, + + update_layout: function() { + }, + + reset: function() { + this.name_changed = false; + this.desc_changed = false; + this.name_input.val(this.new_volume_title); + this.desc_input.val(''); + }, + + show: function() { + this.update_volume_details(); + window.setTimeout(_.bind(function() { + this.name_input.select(); + }, this), 50); + views.CreateVolumeDetailsStepView.__super__.show.apply(this, arguments); + }, + + get: function() { + return { + 'name': _.trim(this.name_input.val()) || this.new_volume_title, + 'description': _.trim(this.desc_input.val()) || "" + } + } + }); + + views.CreateVolumeConfirmStepView = views.CreateWizardStepView.extend({ + step: 5, + update_layout: function() { + var params = this.parent.get_params(); + var image_name = "Empty disk" + if (params.image) { + image_name = params.image.get("name"); + } + this.$(".image-name").text(snf.util.truncate(image_name, 44)); + + var project_name = params.project.get("name"); + this.$(".project-name").text(snf.util.truncate(project_name, 44)); + + var name = params.name; + this.$(".volume-name").text(snf.util.truncate(name, 54)); + + var desc = params.description; + this.$(".volume-info").text(desc); + + var vm= params.vm; + this.$(".volume-machine").text(snf.util.truncate(vm.get("name"), 44)); + + var size = params.size; + this.$(".volume-size").text(size + " GB"); + }, + + get: function() { return {} } + }); + + views.VolumeCreateView = views.VMCreateView.extend({ + view_id: "create_volume_view", + content_selector: "#createvolume-overlay-content", + title: "Create new disk", + min_quota: min_volume_quota, + + setup_step_views: function() { + this.steps[1] = new views.CreateVolumeImageStepView(this); + this.steps[1].bind("change", _.bind(function(data) { + this.trigger("image:change", data) + }, this)); + this.steps[2] = new views.CreateVolumeProjectStepView(this); + this.steps[3] = new views.CreateVolumeMachineStepView(this); + this.steps[4] = new views.CreateVolumeDetailsStepView(this); + this.steps[5] = new views.CreateVolumeConfirmStepView(this); + }, + + show_step: function(step) { + views.VolumeCreateView.__super__.show_step.call(this, step); + }, + + validate: function() { return true }, + + onClose: function() { + this.current_view && this.current_view.hide && this.current_view.hide(true); + }, + + submit: function() { + if (this.submiting) { return }; + var self = this; + var data = this.get_params(); + var meta = {}; + var extra = {}; + + if (this.validate(data)) { + this.submit_btn.addClass("in-progress"); + this.submiting = true; + if (data.image.get("id") == "empty-disk") { + data.image = undefined; + } + storage.volumes.create( + data.name, data.size, data.vm, data.project, data.image, + data.description, {}, + _.bind(function(data) { + window.setTimeout(function() { + self.submiting = false; + self.close_all(); + }, 1000); + }, this)); + } + }, + }); + + views.VolumeView = views.ext.ModelView.extend({ + + init: function() { + views.VolumeView.__super__.init.apply(this, arguments); + this.desc_toggle = this.$(".cont-toggler.desc"); + this.desc_toggle.bind("click", _.bind(this.toggle_desc, this)); + this.desc_content = this.$(".content-cont"); + this.desc_actions = this.$(".content-cont .rename-actions"); + this.desc_save = this.$(".content-cont .btn.confirm"); + this.desc_reset = this.$(".content-cont .btn.cancel"); + this.desc_text = this.$(".content-cont textarea"); + this.desc_edit_btn = this.$(".content-cont .edit-btn"); + this.desc_content.hide(); + + var self = this; + this.desc_save.unbind('click').bind('click', function() { + self.update_description(); + }); + this.desc_reset.unbind('click').bind('click', function() { + self.reset_description(); + self.disable_desc_edit(); + }); + this.desc_text.bind('dblclick', function() { + if (self.desc_editing) { return } + self.enable_desc_edit(); + }); + this.desc_edit_btn.bind('click', function() { + if (self.desc_editing) { return } + self.enable_desc_edit(); + }); + + this.reset_description(); + this.disable_desc_edit(); + }, + + reset_description: function() { + var desc = this.model.get('display_description'); + this.desc_text.val(desc || "No info set"); + }, + + update_description: function() { + var desc = this.desc_text.val(); + this.model.update_description(desc); + this.disable_desc_edit(); + this.toggle_desc(); + }, + + enable_desc_edit: function() { + this.desc_edit_btn.addClass("hidden"); + this.desc_text.attr("readonly", false); + this.desc_text.removeClass("readonly"); + this.desc_text.focus(); + this.desc_actions.show(); + this.desc_editing = true; + }, + + disable_desc_edit: function() { + this.desc_edit_btn.removeClass("hidden"); + this.desc_text.attr("readonly", true); + this.desc_text.addClass("readonly"); + this.desc_actions.hide(); + this.desc_editing = false; + }, + + toggle_desc: function() { + this.reset_description(); + this.disable_desc_edit(); + + if (this.desc_toggle.hasClass("open")) { + this.desc_toggle.removeClass("open"); + this.el.removeClass("light-background"); + } else { + this.desc_toggle.addClass("open"); + this.el.addClass("light-background"); + } + this.desc_content.slideToggle({ + step: function() { + $(window).trigger("resize"); + } + }); + }, + + display_name: function() { + if (!this.model.get('display_name')) { + var vm = this.model.get('vm'); + if (vm) { return vm.get('name') + ' disk'}; + } + return this.model.get('name'); + }, + + status_map: { + 'in_use': 'Attached', + 'error': 'Error', + 'deleting': 'Destroying...', + 'creating': 'Building...' + }, + + status_cls_map: { + 'in_use': 'status-active', + 'error': 'status-error', + 'deleting': 'status-progress destroying-state', + 'creating': 'status-progress build-state' + }, + + tpl: '#volume-view-tpl', + + size_display: function() { + var size = this.model.get('size'); + size = size * Math.pow(1024, 3); + var display = snf.util.readablizeBytes(size); + display = display.replace(" ", "").replace(".00", ""); + return display; + }, + + show_reassign_view: function() { + if (this.model.get('is_root')) { return } + synnefo.ui.main.volume_reassign_view.show(this.model); + }, + + check_can_reassign: function() { + var action = this.$(".project-name"); + if (this.model.get("is_root")) { + snf.util.set_tooltip(action, "You cannot change the project of boot disks.", {tipClass:"tooltip warning"}); + return "project-name-cont disabled"; + } else { + snf.util.unset_tooltip(action); + return "project-name-cont"; + } + }, + + status_cls: function() { + var status = this.model.get('status'); + var vm = this.model.get("vm"); + if (status == "in_use" && vm) { + return snf.views.ext.VM_STATUS_CLS_MAP[vm.state()].join(" "); + } else { + return this.status_cls_map[this.model.get('status')]; + } + }, + + status_display: function(v) { + var vm_status = ""; + var volume_status = this.model.get('status'); + var volume_status_disp = this.status_map[volume_status]; + if (this.model.get('vm')) { + vm_status = STATE_TEXTS[this.model.get('vm').state()] || ""; + } + if (!vm_status || volume_status != "in_use") { + return volume_status_disp; + } + return volume_status_disp + " - " + vm_status; + }, + + model_icon: function() { + var img = 'volume-icon-detached.png'; + var src = synnefo.config.images_url + '{0}'; + if (this.model.get('port_id')) { + img = 'volume-icon.png'; + } + return src.format(img); + }, + + show_snapshot_create_overlay: function() { + var vm = this.model.get('vm'); + if (!vm) { return } + synnefo.ui.main.create_snapshot_view.show(vm, this.model); + }, + + remove: function(model, e) { + e && e.stopPropagation(); + this.model.do_destroy(); + } + }); + + views.VolumesCollectionView = views.ext.CollectionView.extend({ + collection: storage.volumes, + collection_name: 'volumes', + model_view_cls: views.VolumeView, + create_view_cls: views.VolumeCreateView, + quota_key: 'volume', + + check_empty: function() { + views.VolumesCollectionView.__super__.check_empty.apply(this, arguments); + if (this.collection.filter(function(n){ return !n.get('is_root')}).length == 0) { + this.list_el.find(".custom").hide(); + } else { + this.list_el.find(".custom").show(); + } + }, + + parent_for_model: function(m) { + if (m.get('is_root')) { + return this.list_el.find(".system"); + } else { + return this.list_el.find(".custom"); + } + } + }); + + views.VolumesPaneView = views.ext.PaneView.extend({ + id: "pane", + el: '#volumes-pane', + collection_view_cls: views.VolumesCollectionView, + collection_view_selector: '#volumes-list-view' + }); + + views.VolumeVmView = views.ext.ModelView.extend({ + tpl: '#volume-vm-view-tpl', + os_icon: function() { + var data = '<img src="{0}" />'; + return data.format(synnefo.ui.helpers.vm_icon_path(this.model)); + }, + + flavor_tpl: function() { + var vm = this.model.get("vm"); + var flavor = vm && vm.get_flavor(); + var tpl = flavor && flavor.get("disk_template"); + var map = synnefo.config.flavors_disk_templates_info; + if (tpl in map) { + tpl = map[tpl].name || tpl; + } + return tpl ? '- <span class="disk-template">' + tpl + '</span>' : ''; + }, + + vm_status_cls: function(vm) { + var cls = 'inner clearfix main-content'; + if (!this.model.get('vm')) { return cls } + if (this.model.get('vm').in_error_state()) { + cls += ' vm-status-error'; + } + return cls + }, + + vm_style: function() { + var cls, icon_state; + var style = "background-image: url('{0}')"; + var vm = this.model.get('vm') + if (!vm) { return } + this.$(".model-logo").removeClass("state1 state2 state3 state4"); + icon_state = vm.is_active() ? "on" : "off"; + if (icon_state == "on") { + cls = "state1" + } else { + cls = "state2" + } + this.$(".model-logo").addClass(cls); + return style.format(this.get_vm_icon_path(this.model.get('vm'), + 'medium2')); + }, + + get_vm_icon_path: function(vm, icon_type) { + var os = vm.get_os(); + var icons = window.os_icons || views.IconView.VM_OS_ICONS; + + if (icons.indexOf(os) == -1) { + os = "unknown"; + } + + return views.IconView.VM_OS_ICON_TPLS()[icon_type].format(os); + }, + }); + + views.SnapshotCreateView = views.Overlay.extend({ + view_id: "snapshot_create_view", + content_selector: "#snapshot-create-content", + css_class: 'overlay-snapshot-create overlay-info', + overlay_id: "snapshot-create-overlay", + + title: "Create new snapshot", + subtitle: "Machines", + + initialize: function(options) { + views.SnapshotCreateView.__super__.initialize.apply(this); + + this.create_button = this.$("form .form-action.create"); + this.text = this.$(".snapshot-create-name"); + this.description = this.$(".snapshot-create-desc"); + this.form = this.$("form"); + this.success = this.$("p.success"); + this.done_button = this.$(".form-action.btn-close"); + this.init_handlers(); + this.creating = false; + }, + + show: function(vm, volume) { + this.vm = vm; + this.volume = volume || null; + this.reset_success(); + views.SnapshotCreateView.__super__.show.apply(this); + }, + + init_handlers: function() { + this.done_button.click(_.bind(function(e){ + e.preventDefault(); + this.hide(); + }, this)); + + this.create_button.click(_.bind(function(e){ + this.submit(); + }, this)); + + this.form.submit(_.bind(function(e){ + e.preventDefault(); + this.submit(); + return false; + }, this)) + + this.text.keypress(_.bind(function(e){ + if (e.which == 13) {this.submit()}; + },this)) + }, + + submit: function() { + if (this.validate()) { + this.create(); + }; + }, + + validate: function() { + // sanitazie + var t = this.text.val(); + t = t.replace(/^\s+|\s+$/g,""); + this.text.val(t); + + if (this.text.val() == "") { + this.text.closest(".form-field").addClass("error"); + this.text.focus(); + return false; + } else { + this.text.closest(".form-field").removeClass("error"); + } + return true; + }, + + show_success: function() { + this.success.show(); + this.$(".col-fields").hide(); + this.create_button.hide(); + this.done_button.show(); + }, + + reset_success: function() { + this.success.hide(); + this.$(".col-fields").show(); + this.create_button.show(); + this.done_button.hide(); + }, + + create: function() { + if (this.creating) { return } + this.create_button.addClass("in-progress"); + + var name = this.text.val(); + var desc = this.description.val(); + + this.creating = true; + this.vm.create_snapshot({ + display_name: name, + display_description: desc, + volume_id: this.volume.id + }, _.bind(function() { + this.creating = false; + this.show_success(); + }, this), _.bind(function() { + this.creating = false; + }, this)); + }, + + _default_values: function() { + var d = new Date(); + var vmname = this.vm.get('name'); + var vmid = this.vm.id; + var index = this.volume.get_index(); + var id = this.vm.id; + var vname = this.volume.get('display_name'); + var vid = this.volume.get('id'); + var trunc = snf.util.truncate; + + var date = synnefo.util.formatDate(d).replace(/\//g, '-'); + + var trunc_len = 40; + var sname = trunc(vname, trunc_len); + if (index == 0) { + sname = trunc(vmname, trunc_len); + } + + var name = "'{0}' snapshot".format(sname); + name += " {0}".format(date); + name = snf.util.truncate(name, 120); + + var description = "Volume id: {0}".format(vid); + description += "\n" + "Volume name: {0}".format(vname); + description += "\n" + "Volume index: {0}".format(index); + description += "\n" + "Server id: {0}".format(vmid); + description += "\n" + "Server name: {0}".format(vmname); + description += "\n" + "Timestamp: {0}".format(d.toJSON()); + + return { + 'name': name, + 'description': description + } + }, + + beforeOpen: function() { + this.create_button.removeClass("in-progress") + this.text.closest(".form-field").removeClass("error"); + var defaults = this._default_values(); + + this.text.val(defaults.name); + this.description.val(defaults.description); + this.text.show(); + this.text.focus(); + this.description.show(); + }, + + onOpen: function() { + this.text.focus(); + } + }); + + views.VolumeItemRenameView = views.ModelRenameView.extend({ + title_attr: 'display_name' + }); + +})(this); diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/utils.js b/snf-cyclades-app/synnefo/ui/static/snf/js/utils.js index 8077dd1a0327010872a7625f31d83878a4d782f4..0e258b99a1a3caf69a458be7f017395e27c642e3 100644 --- a/snf-cyclades-app/synnefo/ui/static/snf/js/utils.js +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/utils.js @@ -1,35 +1,17 @@ -// Copyright 2011 GRNET S.A. All rights reserved. -// -// Redistribution and use in source and binary forms, with or -// without modification, are permitted provided that the following -// conditions are met: -// -// 1. Redistributions of source code must retain the above -// copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following -// disclaimer in the documentation and/or other materials -// provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// -// The views and conclusions contained in the software and -// documentation are those of the authors and should not be -// interpreted as representing official policies, either expressed -// or implied, of GRNET S.A. +// Copyright (C) 2010-2014 GRNET S.A. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. // ;(function(root){ @@ -199,11 +181,6 @@ synnefo.util.equalHeights = function() { var max_height = 0; var selectors = _.toArray(arguments); - - _.each(selectors, function(s){ - console.log($(s).height()); - }) - // TODO: implement me } synnefo.util.ClipHelper = function(wrapper, text, settings) { @@ -236,15 +213,27 @@ return string.substring(0, len) + append; } + synnefo.util.PRACTICALLY_INFINITE = 9223372036854776000; + synnefo.util.readablizeBytes = function(bytes, fix) { + if (parseInt(bytes) == 0) { return '0 bytes' } if (fix === undefined) { fix = 2; } + bytes = parseInt(bytes); + if (bytes >= synnefo.util.PRACTICALLY_INFINITE) { + return 'Infinite'; + } var s = ['bytes', 'kb', 'MB', 'GB', 'TB', 'PB']; var e = Math.floor(Math.log(bytes)/Math.log(1024)); - return (bytes/Math.pow(1024, Math.floor(e))).toFixed(fix)+" "+s[e]; + if (e > s.length) { + e = s.length - 1; + } + ret = (bytes/Math.pow(1024, Math.floor(e))).toFixed(fix)+" "+s[e]; + return ret; } - synnefo.util.IP_REGEX = /(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/([0-9]|[1-2][0-9]|3[0-2]?)$/ + synnefo.util.SUBNET_REGEX = /(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/([0-9]|[1-2][0-9]|3[0-2]?)$/; + synnefo.util.IP_REGEX = /(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; synnefo.i18n.API_ERROR_MESSAGES = { 'timeout': { @@ -253,7 +242,10 @@ 'allow_report': false, 'type': 'Network' }, - + 'limit_error': { + 'title': 'API error', + 'message': 'Not enough quota available to perform this action.' + }, 'error': { 'title': 'API error', 'message': null @@ -285,6 +277,28 @@ return {del: removed, add: added}; } + + synnefo.util.set_tooltip = function(el, title, custom_params) { + if ($(el).data.tooltip) { return } + var base_params = { + 'tipClass': 'tooltip', + 'position': 'top center', + 'offset': [-5, 0] + } + if (!custom_params) { custom_params = {}; } + var params = _.extend({}, base_params, custom_params); + + if (title !== undefined) { + $(el).attr("title", title); + } + + $(el).tooltip(params); + } + + synnefo.util.unset_tooltip = function(el) { + $(el).attr("title", ""); + $(el).tooltip("remove"); + } synnefo.util.open_window = function(url, name, opts) { // default specs @@ -563,7 +577,7 @@ $.each(json_data, function(key, obj) { code = obj.code; details = obj.details; - error_message = obj.message; + error_message = obj.message ? obj.message : error_message; }) } else { details = json_data; @@ -618,39 +632,30 @@ $.fn.setCursorPosition = function(pos) { - if ($(this).get(0).setSelectionRange) { - $(this).get(0).setSelectionRange(pos, pos); - } else if ($(this).get(0).createTextRange) { - var range = $(this).get(0).createTextRange(); - range.collapse(true); - range.moveEnd('character', pos); - range.moveStart('character', pos); - range.select(); - } - } - - // trim prototype for IE - if(typeof String.prototype.trim !== 'function') { - String.prototype.trim = function() { - return this.replace(/^\s+|\s+$/g, ''); - } + $(this).selectRange(pos, pos); } - // http://stackoverflow.com/questions/499126/jquery-set-cursor-position-in-text-area - $.fn.setCursorPosition = function(pos) { - // not all browsers support setSelectionRange - // put it in try/catch, fallback to no text selection + $.fn.selectRange = function(from, to) { try { + if (to == undefined) { + to = $(this).val().length; + } if ($(this).get(0).setSelectionRange) { - $(this).get(0).setSelectionRange(pos, pos); + $(this).get(0).setSelectionRange(from, to); } else if ($(this).get(0).createTextRange) { var range = $(this).get(0).createTextRange(); range.collapse(true); - range.moveEnd('character', pos); - range.moveStart('character', pos); + range.moveEnd('character', to); + range.moveStart('character', from); range.select(); } - } catch (err) { + } catch(err) {} + } + + // trim prototype for IE + if(typeof String.prototype.trim !== 'function') { + String.prototype.trim = function() { + return this.replace(/^\s+|\s+$/g, ''); } } @@ -674,4 +679,44 @@ }; } + $.fn.insertAt = function(elements, index){ + var children = this.children(); + if(index >= children.size()){ + this.append(elements); + return this; + } + var before = children.eq(index); + $(elements).insertBefore(before); + return this; + }; + + // https://gist.github.com/gid79/854708 + var tooltip = $.fn.tooltip, + slice = Array.prototype.slice; + + function removeTooltip($elements){ + $elements.each(function(){ + if (!$(this).data("tooltip")) { return } + var $element = $(this), + api = $element.data("tooltip"), + tip = api.getTip(), + trigger = api.getTrigger(); + api.hide(); + if( tip ){ + tip.remove(); + } + trigger.unbind('mouseenter mouseleave'); + $element.removeData("tooltip"); + }); + } + + $.fn.tooltip = function(p){ + if( p === 'remove'){ + removeTooltip(this); + return this; + } else { + return tooltip.apply(this, slice.call(arguments,0)); + } + } + })(this); diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/views.js b/snf-cyclades-app/synnefo/ui/static/snf/js/views.js index f844a28a59a818db0e9ef5153998511258319eae..3488e0633e8f3c9cc91cfce911fa2e420710e7d5 100644 --- a/snf-cyclades-app/synnefo/ui/static/snf/js/views.js +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/views.js @@ -1,35 +1,17 @@ -// Copyright 2011 GRNET S.A. All rights reserved. -// -// Redistribution and use in source and binary forms, with or -// without modification, are permitted provided that the following -// conditions are met: -// -// 1. Redistributions of source code must retain the above -// copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following -// disclaimer in the documentation and/or other materials -// provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// -// The views and conclusions contained in the software and -// documentation are those of the authors and should not be -// interpreted as representing official policies, either expressed -// or implied, of GRNET S.A. +// Copyright (C) 2010-2014 GRNET S.A. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. // ;(function(root){ @@ -71,6 +53,11 @@ this.log = new snf.logging.logger("SNF-VIEWS:" + this.view_id); this.parent_view = options && options.parent_view; }, + + destroy: function() { + this.hide(true); + $(this.el).remove(); + }, // is the view visible ? visible: function(){ diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/views_ext.js b/snf-cyclades-app/synnefo/ui/static/snf/js/views_ext.js index 8db902f56b78f17a888f7bb62e1bdabba2af4b9b..ca39298e698a7ab3cb5d655f90170540bd90ee10 100644 --- a/snf-cyclades-app/synnefo/ui/static/snf/js/views_ext.js +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/views_ext.js @@ -1,3 +1,19 @@ +// Copyright (C) 2010-2014 GRNET S.A. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. +// + ;(function(root){ // root @@ -39,7 +55,9 @@ this.container = options && options.container; this._subviews = []; if (this.tpl) { - this.el = $(this.tpl).clone().removeClass("hidden").removeAttr('id'); + var tpl = $(this.tpl); + if (tpl.hasClass("inner-tpl")) { tpl = tpl.children().get(0); } + this.el = $(tpl).clone().removeClass("hidden").removeAttr('id'); } this.init.apply(this, arguments); this.post_init.apply(this, arguments); @@ -53,7 +71,7 @@ var cont = $(this.container); cont.append(this.el); }, - + create_view: function(view_cls, options) { var options = _.extend({}, options); options.parent_view = this; @@ -203,8 +221,12 @@ animation_speed: 200, quota_key: undefined, quota_limit_message: undefined, - + list_el_selector: '.items-list', + disabled_filter: function(m) { return false }, init: function() { + if (this.options.disabled_filter) { + this.disabled_filter = this.options.disabled_filter; + } var handlers = {}; handlers[this.collection_name] = { 'collection_change': ['update', 'sort'], @@ -231,35 +253,28 @@ this.handle_create_click(); }, this)); - if (this.quota_key && !this.quota) { - this.quota = synnefo.storage.quotas.get(this.quota_key); - } - - if (this.quota) { - this.quota.bind("change", _.bind(this.update_quota, this)); + if (this.quota_key) { + synnefo.storage.quotas.bind("change", + _.bind(this.update_quota, this)); this.update_quota(); } + }, update_quota: function() { - var available = this.quota.get_available(); - if (available > 0) { + var can_create = synnefo.storage.quotas.can_create(this.quota_key); + var msg = snf.config.limit_reached_msg; + if (can_create) { this.create_button.removeClass("disabled"); - this.create_button.attr("title", ""); + snf.util.unset_tooltip(this.create_button); } else { this.create_button.addClass("disabled"); - this.create_button.attr("title", this.quota_limit_message || "Quota limit reached") + snf.util.set_tooltip(this.create_button, + this.quota_limit_message || msg, + {tipClass: 'warning tooltip'}); } }, - post_create: function() { - this.quota && this.quota.increase(); - }, - - post_destroy: function() { - this.quota && this.quota.decrease(); - }, - handle_create_click: function() { if (this.create_button.hasClass("disabled")) { return } @@ -267,8 +282,18 @@ this._create_view.show(); } }, + + post_hide: function() { + this.each_model_view(function(model, view) { + this.unbind_custom_view_handlers(view, model); + }, this); + views.ext.CollectionView.__super__.pre_hide.apply(this, arguments); + }, pre_show: function() { + this.each_model_view(function(model, view) { + this.bind_custom_view_handlers(view, model); + }, this); views.ext.CollectionView.__super__.pre_show.apply(this, arguments); this.update_models(); }, @@ -288,9 +313,7 @@ anim = true; this.place_in_parent(parent, el, model, index, anim); } - if (index != view.el.data('index')) { - this.place_in_parent(parent, el, model, index, false); - } + this.check_disabled(view); }, handle_collection_change: function() { @@ -299,6 +322,8 @@ handle_model_add: function(model, collection, options) { this.add_model(model); + var view = this._model_views[model.id]; + if (!view) { return } $(window).trigger("resize"); }, @@ -330,22 +355,8 @@ place_in_parent: function(parent, el, m, index, anim) { var place_func, place_func_context, position_found, exists; - - _.each(parent.find(">.model-item"), function(el) { - var el = $(el); - var el_index = el.data('index'); - if (!el_index || position_found) { return }; - if (parseInt(el_index) < index) { - place_func = el.before; - place_func_context = el; - position_found = true; - } - }); - - if (!position_found) { - place_func = parent.append; - place_func_context = parent; - } + place_func = parent.append; + place_func_context = parent; if (anim) { var self = this; @@ -362,6 +373,8 @@ get_model_view_cls: function(m) { return this.model_view_cls }, + + model_view_options: function(m) { return {} }, add_model: function(m, index) { // if no available class for model exists, skip model add @@ -375,8 +388,44 @@ this.check_empty(); // initialize view - var view = this.create_view(this.get_model_view_cls(m), {model: m}); + var model_view_options = {model: m} + var extra_options = this.model_view_options(m); + _.extend(model_view_options, extra_options); + var view = this.create_view(this.get_model_view_cls(m), + model_view_options); this.add_model_view(view, m, index); + this.fix_sort(); + this.check_disabled(view); + }, + + update_disabled: function() { + _.each(this._model_views, function(v) { + this.check_disabled(v); + }, this); + }, + + check_disabled: function(view) { + var disabled = this.disabled_filter(view.model); + // forced models are always disabled + if (view.model.get('forced')){ + disabled = true; + } + if (disabled) { + view.disable && view.disable(disabled); + if (_.isString(disabled)) { + var el = view.el; + var tooltip = { + 'tipClass': 'tooltip warning', + 'position': 'center right', + 'offset': [0, -200] + }; + snf.util.set_tooltip(el, disabled, tooltip); + } + } else { + var el = view.el; + snf.util.unset_tooltip(el); + view.enable && view.enable(); + } }, add_model_view: function(view, model, index) { @@ -393,7 +442,9 @@ this.add_subview(view); view.show(true); this.post_add_model_view(view, model); + this.bind_custom_view_handlers(view, model); }, + post_add_model_view: function() {}, each_model_view: function(cb, context) { @@ -413,14 +464,19 @@ model_view.hide(); model_view.el.remove(); this.remove_view(model_view); + this.unbind_custom_view_handlers(model_view, m); this.post_remove_model_view(model_view, m); $(window).trigger("resize"); delete this._model_views[m.id]; this.check_empty(); + this.fix_sort(); }, - + + bind_custom_view_handlers: function(view, model) {}, + unbind_custom_view_handlers: function(view, model) {}, post_remove_model_view: function() {}, - + + post_update_models: function() {}, update_models: function(m) { this.check_empty(); this.collection.each(function(model, index) { @@ -441,8 +497,121 @@ model = {'id': model_id}; this.remove_model(model); } + }); + + this.fix_sort(); + this.post_update_models(); + }, + + _get_view_at_index: function(i) { + var found = undefined; + _.each(this._model_views, function(view) { + if (found) { return } + if (view.el.index() == i) { found = view } + }); + return found; + }, + + fix_sort: function() { + var container_indexes = {}; + this.collection.each(function(m, i) { + var view = this._model_views[m.id]; + if (!view) { return } + var parent = view.el.parent().index(); + if (!container_indexes[parent]) { + container_indexes[parent] = []; + } + container_indexes[parent].push(view.model.id); + }, this); + + this.collection.each(function(m, i) { + var view = this._model_views[m.id]; + if (!view) { return } + var indexes = container_indexes[view.el.parent().index()]; + var model_index = indexes.indexOf(view.model.id); + var view_index = view.el.index(); + if (model_index != view_index) { + view.el.parent().insertAt(view.el, model_index); + } + }, this); + } + }); + + views.ext.CollectionSelectView = views.ext.CollectionView.extend({ + allow_multiple: false, + allow_empty: true, + initialize: function(options) { + views.ext.CollectionSelectView.__super__.initialize.apply(this, [options]); + this.allow_multiple = options.allow_multiple != undefined ? + options.allow_multiple : this.allow_multiple; + this.current = options.current != undefined ? + options.current : undefined; + this.allow_empty = options.allow_empty != undefined ? + options.allow_empty : this.allow_empty; + }, + + deselect_all: function(except_model) { + _.each(this._model_views, function(view) { + if (view.model == except_model) { return } + view.deselect(); }) + }, + + get_selected: function() { + var models = _.map(this._model_views, function(view) { + if (view.selected) { + return view.model + } + }); + var selected = _.filter(models, function(m) { return m }); + return selected; + }, + + select_any: function() { + var selected = false; + _.each(this._model_views, function(v) { + if (selected) { return } + if (!v.disabled) { v.select(); selected = v } + }); + return selected; + }, + + post_add_model_view: function(view, model) { + view.bind('deselected', function(view) { + var selected = this.get_selected(); + if (!this.allow_empty && selected.length == 0) { + if (!view.disabled) { + view.select(); + } else { + this.select_any(); + } + } + }, this); + + view.bind('selected', function(view) { + if (this.current != view.model) { + var selected = this.get_selected(); + if (!this.allow_multiple && selected.length) { + this.deselect_all(view.model); + } + this.trigger("change", this.get_selected()); + } + }, this); + + if (!this.allow_empty && !this.get_selected().length) { + view.select(); + } + }, + + set_current: function(model) { + if (!this._model_views[model.id]) { return } + this._model_views[model.id].select(); + }, + + post_remove_model_view: function(view, model) { + if (view.selected) { view.disabled = true; view.deselect() } } + }); views.ext.ModelView = views.ext.View.extend({ @@ -553,6 +722,10 @@ views.ModelRenameView = views.ext.ModelView.extend({ tpl: '#rename-view-tpl', title_attr: 'name', + + display_name: function() { + return this.model.get(this.title_attr) + }, init: function() { views.ModelRenameView.__super__.init.apply(this, arguments); @@ -568,6 +741,10 @@ if (this.model.get('rename_disabled')) { this.edit_btn.remove(); } + + this.model.bind("change:"+this.title_attr, function() { + this.model.trigger("change:_rename_view"); + }, this); this.value.dblclick(_.bind(function(e) { this.set_edit(); @@ -589,7 +766,7 @@ set_edit: function() { if (this.model.get('rename_disabled')) { return } var self = this; - this.input.val(this.model.get('name')); + this.input.val(this.model.get(this.title_attr)); window.setTimeout(function() { self.input.focus(); }, 20); @@ -612,25 +789,45 @@ }); views.ext.SelectModelView = views.ext.ModelView.extend({ + can_deselect: true, + + disable: function() { + this.set_disabled(); + }, + + enable: function() { + this.set_enabled(); + }, + select: function() { if (!this.delegate_checked) { this.input.attr("checked", true); this.item.addClass("selected"); + this.item.attr("selected", true); } this.selected = true; this.trigger("change:select", this, this.selected); + this.trigger("selected", this, this.selected); + this.parent_view && this.parent_view.trigger("change:select", this, this.selected); }, deselect: function() { if (!this.delegate_checked) { this.input.attr("checked", false); this.item.removeClass("selected"); + this.item.attr("selected", false); } this.selected = false; this.trigger("change:select", this, this.selected); + this.trigger("deselected", this, this.selected); + this.parent_view && this.parent_view.trigger("change:select", this, this.selected); }, toggle_select: function() { + if (!this.can_deselect) { + this.select(); + return; + } if (this.selected) { this.deselect(); } else { @@ -641,6 +838,9 @@ post_init_element: function() { this.input = $(this.$("input").get(0)); this.item = $(this.$(".select-item").get(0)); + if (!this.item.length) { + this.item = $(this.el); + } this.delegate_checked = this.model.get('noselect'); this.deselect(); @@ -657,17 +857,37 @@ } $(this.item).click(function(e) { + self.trigger('click'); if (self.model.get('forced')) { return } + if (self.input.attr('disabled')) { return } + if (self.disabled) { return } e.stopPropagation(); self.toggle_select(); }); views.ext.SelectModelView.__super__.post_init_element.apply(this, arguments); + }, + + set_disabled: function() { + this.disabled = true; + if (!this.model.get('forced')){ + this.deselect(); + } + this.input.attr("disabled", true); + this.item.addClass("disabled"); + this.item.attr("disabled", true); + }, + + set_enabled: function() { + this.disabled = false; + this.input.attr("disabled", false); + this.item.removeClass("disabled"); + this.item.attr("disabled", false); } + }); - views.ext.ModelCreateView = views.ext.ModelView.extend({}); views.ext.ModelEditView = views.ext.ModelCreateView.extend({}); diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/volumes.js b/snf-cyclades-app/synnefo/ui/static/snf/js/volumes.js new file mode 100644 index 0000000000000000000000000000000000000000..eef79c405ccfcb99795b9cbdd117732522af81df --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/static/snf/js/volumes.js @@ -0,0 +1,249 @@ +// Copyright (C) 2010-2014 GRNET S.A. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. +// + +;(function(root){ + // Neutron api models, collections, helpers + + // root + var root = root; + + // setup namepsaces + var snf = root.synnefo = root.synnefo || {}; + var snfmodels = snf.models = snf.models || {} + var models = snfmodels.networks = snfmodels.networks || {}; + var storage = snf.storage = snf.storage || {}; + var util = snf.util = snf.util || {}; + + // shortcuts + var bb = root.Backbone; + var slice = Array.prototype.slice + + // logging + var logger = new snf.logging.logger("SNF-MODELS"); + var debug = _.bind(logger.debug, logger); + + models.Volume = snfmodels.Model.extend({ + path: 'volumes', + api_type: 'volume', + storage_attrs: { + '_vm_id': ['vms', 'vm'], + 'tenant_id': ['projects', 'project'] + }, + + proxy_attrs: { + '_status': [['vm', 'vm.state', 'vm.status', 'status'], function() { + return this.get('status'); + }], + 'in_progress': [['status', 'vm', 'vm.status'], function() { + var vm = this.get('vm'); + if (vm && vm.get('in_progress')) { + return true + } + return false; + }], + 'ext': [['vm', 'volume_type'], function() { + var vm = this.get('vm'); + if (!vm) { return false } + var flavor = vm.get_flavor(); + var tpl = flavor.get('disk_template'); + return tpl.indexOf('ext_') === 0; + }] + }, + + initialize: function() { + var self = this; + this.vms = new Backbone.FilteredCollection(undefined, { + collection: synnefo.storage.vms, + collectionFilter: function(m) { + var devices = _.map(self.get('attachments'), + function(a) { return a.server_id + '' }); + return _.contains(devices, m.id + ''); + } + }); + + models.Volume.__super__.initialize.apply(this, arguments); + this.set({vm:this.vms.at(0)}); + }, + + model_actions: { + 'snapshot': [['status', 'vm', 'ext'], function() { + if (!synnefo.config.snapshots_enabled) { return false } + if (!this.get('ext')) { return false } + var removing = this.get('status') == 'deleting'; + var creating = this.get('status') == 'creating'; + var vm = this.get('vm'); + return vm && vm.can_connect() && !removing && !creating; + }], + + 'remove': [['is_root', 'vm', 'ext'], function() { + var removing = this.get('status') == 'deleting'; + var creating = this.get('status') == 'creating'; + if (this.get('is_root')) { return false } + var vm = this.get('vm'); + return vm && vm.can_connect() && !removing && !creating; + }] + }, + + get_index: function() { + var a = this.get("attachments"); + return a && a[0] && a[0].device_index; + }, + + reassign_to_project: function(project, success, cb) { + var project_id = project.id ? project.id : project; + var self = this; + var _success = function() { + success(); + self.set({'tenant_id': project_id}); + } + + synnefo.api.sync('create', this, { + url: this.url() + '/action', + success: _success, + complete: cb, + data: { + reassign: { + project: project_id + } + } + }); + }, + + update_description: function(new_desc) { + var self = this; + this.sync("update", this, { + critical: true, + data: { + 'volume': { + 'display_description': new_desc + } + }, + success: _.bind(function(){ + snf.api.trigger("call"); + this.set({'display_description': new_desc}); + }, this) + }); + }, + + rename: function(new_name) { + //this.set({'name': new_name}); + var self = this; + this.sync("update", this, { + critical: true, + data: { + 'volume': { + 'display_name': new_name + } + }, + success: _.bind(function(){ + snf.api.trigger("call"); + this.set({'display_name': new_name}); + }, this) + }); + }, + + do_remove: function(succ, err) { return this.do_destroy(succ, err) }, + + do_destroy: function(succ, err) { + this.actions.reset_pending(); + this.destroy({ + success: _.bind(function() { + synnefo.api.trigger("quotas:call", 10); + this.set({status: 'deleting'}); + succ && succ(); + }, this), + error: err || function() {}, + silent: true + }); + }, + + }); + + models.Volumes = snfmodels.Collection.extend({ + model: models.Volume, + path: 'volumes', + api_type: 'volume', + details: true, + noUpdate: true, + updateEntries: true, + add_on_create: true, + + parse: function(resp) { + var data = resp; + if (!resp) { return [] }; + data = _.filter(_.map(resp.volumes, + _.bind(this.parse_volume_api_data, this)), + function(v){ return v }); + return data; + }, + + parse_volume_api_data: function(volume) { + var attachments = volume.attachments || []; + volume._vm_id = attachments.length > 0 ? + attachments[0].server_id : undefined; + if (!volume.display_name) { + volume.display_name = "Disk {0}".format(volume.id); + } + + volume.is_root = false; + if (attachments.length) { + if (attachments[0].device_index === 0) { + volume.display_name = "Boot disk"; + volume.is_root = true; + volume.rename_disabled = true; + } + volume._index = attachments[0].device_index; + volume._index_set = true; + } else { + volume._index_set = false; + } + return volume; + }, + + comparator: function(m) { + return m.get('_vm_id') + m.get('_index'); + }, + + create: function(name, size, vm, project, source, description, + extra, callback) { + var volume = { + 'display_name': name, + 'display_description': description || null, + 'size': parseInt(size), + 'server_id': vm.id, + 'project': project.id, + 'metadata': {} + } + + if (source && source.is_snapshot()) { + volume.snapshot_id = source.get("id"); + } + + if (source && !source.is_snapshot()) { + volume.imageRef = source.get("id"); + } + + var cb = function(data) { + callback && callback(data); + } + + this.api_call(this.path + "/", "create", {'volume': volume}, undefined, + undefined, cb, {critical: true}); + } + }); + + snf.storage.volumes = new models.Volumes(); +})(this); diff --git a/snf-cyclades-app/synnefo/ui/templates/apps.html b/snf-cyclades-app/synnefo/ui/templates/apps.html index cc89e82ed9cb75a54a8050e49b79e45dde5f65f8..6a40ed22fc5e6c2330aece04cc1b072bb55dbc20 100644 --- a/snf-cyclades-app/synnefo/ui/templates/apps.html +++ b/snf-cyclades-app/synnefo/ui/templates/apps.html @@ -1,36 +1,18 @@ <!-- -Copyright 2011 GRNET S.A. All rights reserved. +Copyright (C) 2010-2014 GRNET S.A. -Redistribution and use in source and binary forms, with or -without modification, are permitted provided that the following -conditions are met: +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. - 1. Redistributions of source code must retain the above - copyright notice, this list of conditions and the following - disclaimer. +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 General Public License for more details. - 2. Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials - provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. - -The views and conclusions contained in the software and -documentation are those of the authors and should not be -interpreted as representing official policies, either expressed -or implied, of GRNET S.A. +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. --> <div id="apps" class="separator"></div> diff --git a/snf-cyclades-app/synnefo/ui/templates/desktops.html b/snf-cyclades-app/synnefo/ui/templates/desktops.html index b8144521c1d8392236c7eb791d4edf1ce643b88e..20fff9c5fa78aadc44f56ae9d8a4aa86ee52a97f 100644 --- a/snf-cyclades-app/synnefo/ui/templates/desktops.html +++ b/snf-cyclades-app/synnefo/ui/templates/desktops.html @@ -1,36 +1,18 @@ <!-- -Copyright 2011 GRNET S.A. All rights reserved. +Copyright (C) 2010-2014 GRNET S.A. -Redistribution and use in source and binary forms, with or -without modification, are permitted provided that the following -conditions are met: +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. - 1. Redistributions of source code must retain the above - copyright notice, this list of conditions and the following - disclaimer. +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 General Public License for more details. - 2. Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials - provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. - -The views and conclusions contained in the software and -documentation are those of the authors and should not be -interpreted as representing official policies, either expressed -or implied, of GRNET S.A. +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. --> <div id="desktops" class="separator"></div> diff --git a/snf-cyclades-app/synnefo/ui/templates/disks.html b/snf-cyclades-app/synnefo/ui/templates/disks.html deleted file mode 100644 index 3e1c8d26b9f1140f5872d288fae11fcf45babd16..0000000000000000000000000000000000000000 --- a/snf-cyclades-app/synnefo/ui/templates/disks.html +++ /dev/null @@ -1,41 +0,0 @@ -<!-- -Copyright 2011 GRNET S.A. All rights reserved. - -Redistribution and use in source and binary forms, with or -without modification, are permitted provided that the following -conditions are met: - - 1. Redistributions of source code must retain the above - copyright notice, this list of conditions and the following - disclaimer. - - 2. Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials - provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. - -The views and conclusions contained in the software and -documentation are those of the authors and should not be -interpreted as representing official policies, either expressed -or implied, of GRNET S.A. ---> -{% load i18n %} - -<div id="machines-pane" style="display:block"> -<iframe src="http://pithos.dev.grnet.gr" width="100%" height="500"> -</iframe> -</div> - diff --git a/snf-cyclades-app/synnefo/ui/templates/files.html b/snf-cyclades-app/synnefo/ui/templates/files.html index c6853fa0ea4a814325af9266102333e03bb838b5..f9a85f387e37a6655fbff6c0b36c9b7fa7bbcd00 100644 --- a/snf-cyclades-app/synnefo/ui/templates/files.html +++ b/snf-cyclades-app/synnefo/ui/templates/files.html @@ -1,36 +1,18 @@ <!-- -Copyright 2011 GRNET S.A. All rights reserved. +Copyright (C) 2010-2014 GRNET S.A. -Redistribution and use in source and binary forms, with or -without modification, are permitted provided that the following -conditions are met: +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. - 1. Redistributions of source code must retain the above - copyright notice, this list of conditions and the following - disclaimer. +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 General Public License for more details. - 2. Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials - provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. - -The views and conclusions contained in the software and -documentation are those of the authors and should not be -interpreted as representing official policies, either expressed -or implied, of GRNET S.A. +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. --> <div id="files" class="separator"></div> diff --git a/snf-cyclades-app/synnefo/ui/templates/home.html b/snf-cyclades-app/synnefo/ui/templates/home.html index ad9390bfe62da0bbb5c08238fc62ed31e7036f0d..4683b61fdd57c7212fac8d3a969fbd580e43e46f 100644 --- a/snf-cyclades-app/synnefo/ui/templates/home.html +++ b/snf-cyclades-app/synnefo/ui/templates/home.html @@ -5,15 +5,13 @@ <head> <title>{{ BRANDING_SERVICE_NAME }}</title> - <!--<meta http-equiv="X-UA-Compatible" content="IE=7">--> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="content-type" content="text/html; charset=UTF-8"> - {% if SYNNEFO_FONTS_BASE_URL %} - <link href="{{ SYNNEFO_FONTS_BASE_URL }}css?family=Ubuntu&subset=latin,greek" - rel="stylesheet" type="text/css" > - <link href='{{ SYNNEFO_FONTS_BASE_URL }}css?family=Open+Sans&subset=latin,greek' - rel='stylesheet' type='text/css'> - {% endif %} + + {% for url in BRANDING_FONTS_CSS_URLS %} + <link href="{{ url }}" rel="stylesheet" type="text/css" > + {% endfor %} <link rel="shortcut icon" href="{{ BRANDING_FAVICON_URL }}" /> @@ -35,6 +33,7 @@ <![endif]--> <script src="{{ SYNNEFO_JS_LIB_URL}}jquery-1.7.2.js"></script> + <script src="{{ SYNNEFO_JS_LIB_URL}}simple-slider.min.js"></script> <script src="{{ SYNNEFO_JS_LIB_URL}}jquery.cookie.js"></script> <script src="{{ SYNNEFO_JS_LIB_URL}}jquery.client.js"></script> <script src="{{ SYNNEFO_JS_LIB_URL}}jquery.tools.min.js"></script> @@ -46,10 +45,14 @@ <script src="{{ SYNNEFO_JS_LIB_URL}}filereader.js"></script> <script src="{{ SYNNEFO_JS_LIB_URL}}filesaver.js"></script> + <script src="{{ SYNNEFO_JS_LIB_URL}}select2.min.js"></script> + <link rel="stylesheet" type="text/css" + href="{{ SYNNEFO_CSS_URL }}select2.css?v={{ SYNNEFO_JS_LIB_VERSION }}"/> + <script src="{{ SYNNEFO_JS_LIB_URL}}underscore.js?v={{ SYNNEFO_JS_LIB_VERSION }}"></script> <script src="{{ SYNNEFO_JS_LIB_URL}}underscore.string.js?v={{ SYNNEFO_JS_LIB_VERSION }}"></script> - <script src="{{ SYNNEFO_JS_LIB_URL}}rivets.conf.js"></script> + <script src="{{ SYNNEFO_JS_LIB_URL}}rivets.conf.js?v={{ SYNNEFO_JS_LIB_VERSION }}"></script> <script src="{{ SYNNEFO_JS_LIB_URL}}backbone.js?v={{ SYNNEFO_JS_LIB_VERSION }}"></script> <script src="{{ SYNNEFO_JS_LIB_URL}}json2.js?v={{ SYNNEFO_JS_LIB_VERSION }}"></script> <script src="{{ SYNNEFO_JS_LIB_URL}}stacktrace.js?v={{ SYNNEFO_JS_LIB_VERSION }}"></script> @@ -69,6 +72,7 @@ <script src="{{ SYNNEFO_JS_URL }}sync.js?v={{ SYNNEFO_JS_LIB_VERSION }}"></script> <script src="{{ SYNNEFO_JS_URL }}models.js?v={{ SYNNEFO_JS_LIB_VERSION }}"></script> <script src="{{ SYNNEFO_JS_URL }}neutron.js?v={{ SYNNEFO_JS_LIB_VERSION }}"></script> + <script src="{{ SYNNEFO_JS_URL }}volumes.js?v={{ SYNNEFO_JS_LIB_VERSION }}"></script> <script src="{{ SYNNEFO_JS_URL }}glance_models.js?v={{ SYNNEFO_JS_LIB_VERSION }}"></script> <script src="{{ SYNNEFO_JS_URL }}views.js?v={{ SYNNEFO_JS_LIB_VERSION }}"></script> <script src="{{ SYNNEFO_JS_URL }}views_ext.js?v={{ SYNNEFO_JS_LIB_VERSION }}"></script> @@ -79,15 +83,16 @@ <script src="{{ SYNNEFO_JS_WEB_URL }}ui_icon_view.js?v={{ SYNNEFO_JS_LIB_VERSION }}"></script> <script src="{{ SYNNEFO_JS_WEB_URL }}ui_single_view.js?v={{ SYNNEFO_JS_LIB_VERSION }}"></script> <script src="{{ SYNNEFO_JS_WEB_URL }}ui_list_view.js?v={{ SYNNEFO_JS_LIB_VERSION }}"></script> - <script src="{{ SYNNEFO_JS_WEB_URL }}ui_networks_view.js?v={{ SYNNEFO_JS_LIB_VERSION }}"></script> <script src="{{ SYNNEFO_JS_WEB_URL }}ui_public_keys_view.js?v={{ SYNNEFO_JS_LIB_VERSION }}"></script> - <script src="{{ SYNNEFO_JS_WEB_URL }}ui_disks_view.js?v={{ SYNNEFO_JS_LIB_VERSION }}"></script> - <script src="{{ SYNNEFO_JS_WEB_URL }}ui_ips_view.js?v={{ SYNNEFO_JS_LIB_VERSION }}"></script> <script src="{{ SYNNEFO_JS_WEB_URL }}ui_metadata_view.js?v={{ SYNNEFO_JS_LIB_VERSION }}"></script> <script src="{{ SYNNEFO_JS_WEB_URL }}ui_feedback_view.js?v={{ SYNNEFO_JS_LIB_VERSION }}"></script> <script src="{{ SYNNEFO_JS_WEB_URL }}ui_create_view.js?v={{ SYNNEFO_JS_LIB_VERSION }}"></script> + <script src="{{ SYNNEFO_JS_WEB_URL }}ui_networks_view.js?v={{ SYNNEFO_JS_LIB_VERSION }}"></script> + <script src="{{ SYNNEFO_JS_WEB_URL }}ui_ips_view.js?v={{ SYNNEFO_JS_LIB_VERSION }}"></script> + <script src="{{ SYNNEFO_JS_WEB_URL }}ui_volumes_view.js?v={{ SYNNEFO_JS_LIB_VERSION }}"></script> <script src="{{ SYNNEFO_JS_WEB_URL }}ui_connect_view.js?v={{ SYNNEFO_JS_LIB_VERSION }}"></script> <script src="{{ SYNNEFO_JS_WEB_URL }}ui_vm_resize_view.js?v={{ SYNNEFO_JS_LIB_VERSION }}"></script> + <script src="{{ SYNNEFO_JS_WEB_URL }}ui_reassign_view.js?v={{ SYNNEFO_JS_LIB_VERSION }}"></script> <script src="{{ SYNNEFO_JS_WEB_URL }}ui_public_keys_view.js?v={{ SYNNEFO_JS_LIB_VERSION }}"></script> <script src="{{ SYNNEFO_JS_WEB_URL }}ui_custom_images.js?v={{ SYNNEFO_JS_LIB_VERSION }}"></script> @@ -103,8 +108,10 @@ }); if (!tmp_auth_client.get_token()) { tmp_auth_client.redirect_to_login(); } </script> + {{ CLOUDBAR_CODE }} - + <script>window.CLOUDBAR_INCLUDE_FONTS = false;</script> + <script> // empty object for console to avoid errors in browsers that don't support it if (!window.console) {window.console = {}; window.console.log = window.console.info = window.console.debug = @@ -112,6 +119,7 @@ //populate available image icons array var os_icons = {{image_icons|safe}}; + os_icons.push('snapshot'); // timeout value from settings.py var TIMEOUT = {{ timeout }}; @@ -136,11 +144,12 @@ 'START': '{% trans "Starting..." %}', 'CONNECT': '{% trans "Connecting..." %}', 'DISCONNECT': '{% trans "Disconnecting..." %}', + 'ATTACH_VOLUME': '{% trans "Attaching disk..." %}', + 'DETACH_VOLUME': '{% trans "Detaching disk..." %}', + 'MODIFY_VOLUME': '{% trans "Modifying disk..." %}', 'RESIZE': '{% trans "Resizing..." %}' } - - // building statuses var BUILDING_MESSAGES = { 'INIT': '{% trans "Initializing..." %}', @@ -237,6 +246,9 @@ <li><a href="#machines/icon/" title="{% trans "manage virtual machines" %}" data-hover-title="machines" class="primary" id="machines_view_link"> <img src="{{ SYNNEFO_IMAGES_URL }}machines-icon.png" /></a></li> + <li><a href="#disks/" title="{% trans "manage virtual disks" %}" + data-hover-title="disks" class="primary" id="volumes_view_link"> + <img src="{{ SYNNEFO_IMAGES_URL }}disks-icon.png" /></a></li> <li><a href="#networks/" title="{% trans "manage networks" %}" data-hover-title="networks" class="primary" id="networks_view_link"> <img src="{{ SYNNEFO_IMAGES_URL }}networks-icon.png" /></a></li> @@ -255,8 +267,8 @@ <div id="networks-pane" class="pane-view"> {% include "partials/networks.html" %} </div> - <div id="disks-pane" class="pane"> - {% include "partials/disks.html" %} + <div id="volumes-pane" class="pane-view"> + {% include "partials/volumes.html" %} </div> <div id="ips-pane" class="pane-view"> {% include "partials/ips.html" %} @@ -400,7 +412,7 @@ <div id="rename-view-tpl" class="hidden model-rename-view"> <div class="model-name"> <h3> - <span data-rv-text="model.name|list_truncate" class="value"></span> + <span data-rv-text="model._rename_view|display_name|list_truncate" class="value"></span> <span class="edit-btn" data-rv-on-click="view.set_edit"></span> </h3> </div> @@ -576,14 +588,36 @@ <div class="header clearfix images off">Loading images...<span></span></div> <div class="header clearfix flavors off">Loading flavors...<span></span></div> <div class="header clearfix resources off">Loading resources...<span></span></div> + <div class="header clearfix projects off">Loading projects...<span></span></div> <div class="header clearfix quotas off">Loading quotas...<span></span></div> <div class="header clearfix vms off">Loading machines...<span></span></div> + <div class="header clearfix volumes off">Loading disks...<span></span></div> <div class="header clearfix networks off">Loading networks...<span></span></div> <div class="header clearfix layout off">Rendering layout...<span></span></div> </div> <div id="user_custom_images" class="overlay-content overlay-content hidden"> {% include "partials/custom_images.html" %} </div> + + <div id="create-view-projects-select-tpl" class="hidden"> + <div class="collection"> + <select class="items-list"> + </select> + </div> + </div> + + <div id="create-view-select-project-item-tpl" class="hidden inner-tpl"> + <option class="clearfix select-item project clearfix" + data-rv-value="model.id"> + <div class="project-select-item-content clearfix"> + <span class-"project-name" + data-rv-text="model.name|truncate 25"></span> + <span class="quota" + data-rv-html="model._quota|quotas_option_html"></span> + </div> + </option> + </div> + {% include "partials/vm_resize.html" %} {% include "footer.html" %} @@ -591,7 +625,6 @@ $(document).ready(function() { $(".css-panes").hide(); - // TODO: match <= 1.9.1 if ($.browser.mozilla && $.browser.version.substr(0,3) == "1.9") { synnefo.config.overlay_speed = 0; $.fx.off = true; @@ -630,8 +663,9 @@ 'userdata': '{% url ui_userdata %}', 'compute': {{ compute_api_url|safe }}, 'network': {{ network_api_url|safe }}, + 'volume': {{ volume_api_url|safe }}, 'glance': {{ glance_api_url|safe }}, - 'accounts': {{ accounts_api_url|safe }}, + 'accounts': {{ accounts_api_url|safe }} }; // TODO: configurable userdata urls in models.js @@ -672,6 +706,9 @@ synnefo.config.automatic_network_range_format = {{ automatic_network_range_format|safe }}; synnefo.config.custom_image_help_url = '{{ custom_image_help_url|safe }}'; synnefo.config.forced_server_networks = {{ forced_server_networks|safe }}; + synnefo.config.limit_reached_msg = 'Usage limit reached'; + synnefo.config.volume_max_size = {{ volume_max_size|safe }}; + synnefo.config.snapshots_enabled = {{ snapshots_enabled|safe }}; synnefo.auth_client = new synnefo.auth.AstakosClient({ login_url: synnefo.config.login_redirect, @@ -697,6 +734,7 @@ synnefo.ui.init(); synnefo.ui.main.bind("ready", function(){ }); + }); </script> </body> diff --git a/snf-cyclades-app/synnefo/ui/templates/images.html b/snf-cyclades-app/synnefo/ui/templates/images.html index 14e54be7de3d226ca07330a7443373ef200c36d9..ec09eea89ce3071148e4ce6db109a3037a935fe8 100644 --- a/snf-cyclades-app/synnefo/ui/templates/images.html +++ b/snf-cyclades-app/synnefo/ui/templates/images.html @@ -1,36 +1,18 @@ <!-- -Copyright 2011 GRNET S.A. All rights reserved. +Copyright (C) 2010-2014 GRNET S.A. -Redistribution and use in source and binary forms, with or -without modification, are permitted provided that the following -conditions are met: +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. - 1. Redistributions of source code must retain the above - copyright notice, this list of conditions and the following - disclaimer. +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 General Public License for more details. - 2. Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials - provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. - -The views and conclusions contained in the software and -documentation are those of the authors and should not be -interpreted as representing official policies, either expressed -or implied, of GRNET S.A. +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. --> <div id="images" class="separator"></div> diff --git a/snf-cyclades-app/synnefo/ui/templates/machines_console.html b/snf-cyclades-app/synnefo/ui/templates/machines_console.html index e3e778a3bcd19b51bfc925904e45d2b841594faf..31d26f03e58bfc94c6a78f8d2636e20bf2dc8d0e 100644 --- a/snf-cyclades-app/synnefo/ui/templates/machines_console.html +++ b/snf-cyclades-app/synnefo/ui/templates/machines_console.html @@ -1,74 +1,263 @@ <!-- -Copyright 2011 GRNET S.A. All rights reserved. - -Redistribution and use in source and binary forms, with or -without modification, are permitted provided that the following -conditions are met: - - 1. Redistributions of source code must retain the above - copyright notice, this list of conditions and the following - disclaimer. - - 2. Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials - provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. - -The views and conclusions contained in the software and -documentation are those of the authors and should not be -interpreted as representing official policies, either expressed -or implied, of GRNET S.A. +Copyright (C) 2010-2014 GRNET S.A. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. --> {% load i18n %} - <html> - <head> - <link rel="stylesheet" type="text/css" href="{{ SYNNEFO_CSS_URL }}main.css"/> - <title>{% trans "Console" %}</title> - </head> - <body> - <div id="wrapper" class="console"> - <div class='console-info'> - <div class='machine-name'> - {% trans "machine" %}: {{machine|slice:":50"}} - </div> - <div class="host-ip"> - {% trans "IP" %}: <span class="ip-version-label">v4</span><span class="ipv4-text ip">{{host_ip}}</span><span class="ip-version-label">v6</span><span class="ipv4-text ip">{{host_ip_v6}}</span> +<head> - </div> - </div> - <div id='console-header'> + <title>{% trans "Console" %}</title> + <link rel="stylesheet" href="{{ UI_MEDIA_URL }}extra/noVNC/include/base.css"> + <link rel="stylesheet" href="{{ UI_MEDIA_URL }}extra/noVNC/more/keyboard.css"> + <link rel="stylesheet" type="text/css" href="{{ SYNNEFO_CSS_URL }}main.css"/> + <link rel="stylesheet" href="{{ UI_MEDIA_URL }}extra/noVNC/include/extra.css"> + <meta charset="utf-8"> + + <!-- Always force latest IE rendering engine (even in intranet) & Chrome Frame + Remove this if you use the .htaccess --> + <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> + + <!-- Apple iOS Safari settings --> + <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> + <meta name="apple-mobile-web-app-capable" content="yes" /> + <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> + <!-- App Start Icon --> + <link rel="apple-touch-startup-image" href="{{UI_MEDIA_URL}}extra/noVNC/images/screen_320x460.png" /> + <!-- For iOS devices set the icon to use if user bookmarks app on their homescreen --> + <link rel="apple-touch-icon" href="{{UI_MEDIA_URL}}extra/noVNC/images/screen_57x57.png"> + <!-- + <link rel="apple-touch-icon-precomposed" href="{{UI_MEDIA_URL}}extra/noVNC/images/screen_57x57.png" /> + --> + <!-- + <script type='text/javascript' + src='http://getfirebug.com/releases/lite/1.2/firebug-lite-compressed.js'></script> + --> + + <script type='text/javascript' src='{{UI_MEDIA_URL}}extra/noVNC/more/keyboard.js'></script> + <script type='text/javascript' src='{{UI_MEDIA_URL}}js/lib/jquery-1.7.2.js'></script> +</head> + +<body class="console-body"> + <div id="wrapper" class="console"> + <div id='console-header' class="clearfix"> <div class="console-header-logo"> <a href="/" title="{{ BRANDING_SERVICE_NAME }} Console "> - <img src="{{ BRANDING_CONSOLE_LOGO_URL }}" alt="{{ BRANDING_SERVICE_NAME }}"/> + <img src="{{BRANDING_IMAGE_MEDIA_URL}}console_logo.png" alt="{{ BRANDING_SERVICE_NAME }}"/> </a> </div> - <br/> - <div class="help-text">{% trans "This is a slow connection meant only for troubleshooting. For an optimal experience use the machine's Connect button on the main panel." %}</div> + <div class='console-info'> + <div class='machine-name'> + {% trans "machine" %}: {{machine|slice:":50"}} + </div> + <div class="host-ip"> + {% if machine_hostname %} + <span>{% trans "Hostname" %}: {{machine_hostname}}</span> + {% endif %} + </div> + </div> + </div> + <div class="clearfix actions-bar"> + <div id="noVNC-control-bar"> + <div id="noVNC-menu-bar" style="display:none;"> </div> - <div class="console-container"> - <applet code="VncViewer.class" archive="{{ UI_MEDIA_URL }}extra/vncviewer/VncViewer.jar"> - <param name="HOST" value="{{host}}"></param> - <param name="PORT" value="{{port}}"></param> - <param name="PASSWORD" value="{{password}}"></param> - </applet> + + + <!--noVNC Buttons--> + <div class="noVNC-buttons-right"> + <input type="image" src="{{UI_MEDIA_URL}}extra/noVNC/images/ctrl.png" id="toggleCtrlButton" title="Toggle Ctrl" class="noVNC_status_button"> + <input type="image" src="{{UI_MEDIA_URL}}extra/noVNC/images/alt.png" id="toggleAltButton" title="Toggle Alt" class="noVNC_status_button"> + <input type="image" src="{{UI_MEDIA_URL}}extra/noVNC/images/drag.png" + id="noVNC_view_drag_button" class="noVNC_status_button" + title="Move/Drag Viewport"> + <div id="noVNC_mobile_buttons"> + </div> + + <input type="checkbox" + id="noVNC_ctrl_box" class="noVNC_status_button" + title="Send Ctrl"/><label id="ctrl_label">Ctrl</label> + <input type="checkbox" + id="noVNC_alt_box" class="noVNC_status_button" + title="Send Alt" /><label id="alt_label">Alt</label> + + <!--<input type="checkbox" + id="noVNC_shift_box" class="noVNC_status_button" + title="Send Shift" /><label id="shift_label">Shift</label>--> + <input type="image" src="{{UI_MEDIA_URL}}extra/noVNC/images/mouse_none.png" + id="noVNC_mouse_button0" class="noVNC_status_button"> + <input type="image" src="{{UI_MEDIA_URL}}extra/noVNC/images/mouse_left.png" + id="noVNC_mouse_button1" class="noVNC_status_button"> + <input type="image" src="{{UI_MEDIA_URL}}extra/noVNC/images/mouse_middle.png" + id="noVNC_mouse_button2" class="noVNC_status_button"> + <input type="image" src="{{UI_MEDIA_URL}}extra/noVNC/images/mouse_right.png" + id="noVNC_mouse_button4" class="noVNC_status_button"> + <input type="image" src="{{UI_MEDIA_URL}}extra/noVNC/images/keyboard.png" + id="showKeyboard" class="noVNC_status_button" + value="Keyboard" title="Show Keyboard"/> + + <input type="button" + id="sendEnterButton" class="noVNC_status_button" value="Enter" title="Send Enter" /> + <input type="email" autocapitalize="off" autocorrect="off" + id="keyboardinput" class=""/> + + + <input id="keyboardText" type="text" value="" class="myKeyboardInput" size="4"/> + <input type="image" src="{{UI_MEDIA_URL}}extra/noVNC/images/ctrlaltdel.png" + id="sendCtrlAltDelButton" class="noVNC_status_button" + title="Send Ctrl-Alt-Del" /> + <input type="image" src="{{UI_MEDIA_URL}}extra/noVNC/images/clipboard.png" + id="clipboardButton" class="noVNC_status_button" + title="Clipboard" /> + <input type="image" src="{{UI_MEDIA_URL}}extra/noVNC/images/settings.png" + id="settingsButton" class="noVNC_status_button" + title="Settings" /> + <input type="image" src="{{UI_MEDIA_URL}}extra/noVNC/images/connect.png" + id="connectButton" class="noVNC_status_button" + title="Connect" /> + <input type="image" src="{{UI_MEDIA_URL}}extra/noVNC/images/disconnect.png" + id="disconnectButton" class="noVNC_status_button" + title="Disconnect" /> + + </div> + + <div id="noVNC_status">Loading</div> + <!-- Description Panel --> + <!-- Shown by default when hosted at for kanaka.github.com --> + <div id="noVNC_description" style="display:none;" class=""> + noVNC is a browser based VNC client implemented using HTML5 Canvas + and WebSockets. You will either need a VNC server with WebSockets + support (such as <a href="http://libvncserver.sourceforge.net/">libvncserver</a>) + or you will need to use + <a href="https://github.com/kanaka/websockify">websockify</a> + to bridge between your browser and VNC server. See the noVNC + <a href="https://github.com/kanaka/noVNC">README</a> + and <a href="http://kanaka.github.com/noVNC">website</a> + for more information. + <br /> + <input id="descriptionButton" type="button" value="Close"> </div> + + <!-- Popup Status Panel --> + <div id="noVNC_popup_status_panel" class=""> + </div> + + <!-- Clipboard Panel --> + <div id="noVNC_clipboard" class="triangle-right top"> + <textarea id="noVNC_clipboard_text" rows=5> + </textarea> + <br /> + <input id="noVNC_clipboard_clear_button" type="button" + value="Clear"> + </div> + + <!-- Settings Panel --> + <div id="noVNC_settings" class="triangle-right top"> + <span id="noVNC_settings_menu"> + <ul> + <li><input id="noVNC_encrypt" type="checkbox"> Encrypt</li> + <li><input id="noVNC_true_color" type="checkbox" checked> True Color</li> + <li><input id="noVNC_cursor" type="checkbox"> Local Cursor</li> + <li><input id="noVNC_clip" type="checkbox"> Clip to Window</li> + <li><input id="noVNC_shared" type="checkbox"> Shared Mode</li> + <li><input id="noVNC_view_only" type="checkbox"> View Only</li> + <li><input id="noVNC_connectTimeout" type="input"> Connect Timeout (s)</li> + <li><input id="noVNC_path" type="input" value="websockify"> Path</li> + <li><input id="noVNC_repeaterID" type="input" value=""> Repeater ID</li> + <hr> + <!-- Stylesheet selection dropdown --> + <li><label><strong>Style: </strong> + <select id="noVNC_stylesheet" name="vncStyle"> + <option value="default">default</option> + </select></label> + </li> + + <!-- Logging selection dropdown --> + <li><label><strong>Logging: </strong> + <select id="noVNC_logging" name="vncLogging"> + </select></label> + </li> + <hr> + <li><input type="button" id="noVNC_apply" value="Apply"></li> + </ul> + </span> + </div> + + <!-- Connection Panel --> + <div id="noVNC_controls" class="triangle-right top"> + <ul> + <li><label><strong>Host: </strong><input id="noVNC_host" value="{{host}}" /></label></li> + <li><label><strong>Port: </strong><input id="noVNC_port" value={{port}} /></label></li> + <li><label><strong>Password: </strong><input id="noVNC_password" type="password" value="{{password}}" /></label></li> + <li><input id="noVNC_connect_button" type="button" value="Connect"></li> + <input type="hidden" id="noVNC_encrypt" value=true /> + </ul> + </div> + + </div> <!-- End of noVNC-control-bar --> +</div> <!--End of clearfix div--> + + <div class="help-text"><span><a href="" title="Close info">X</a>{% trans "This is a slow connection meant only for troubleshooting. For an optimal experience use the machine's Connect button on the main panel." %}</span></div> + <div id="noVNC_screen"> + <div id="noVNC_screen_pad"></div> + + <h1 id="noVNC_logo"><span>no</span><br />VNC</h1> + + <!-- HTML5 Canvas --> + <div id="noVNC_container"> + <canvas id="noVNC_canvas" width="640px" height="20px"> + Canvas not supported. + </canvas> + </div> + </div> - <div class="console-footer"> - {% include "footer.html" %} + <script>var MEDIA_URL = "{{ UI_MEDIA_URL }}";</script> + <script src="{{UI_MEDIA_URL}}extra/noVNC/include/util.js"></script> +<script> + /*jslint white: false */ + /*global window, $, Util, RFB, */ + "use strict"; + + // Load supporting scripts + var scripts = ["webutil.js", "base64.js", "websock.js", "des.js", "input.js", "display.js", + "jsunzip.js", "rfb.js", "ui.js"]; + for (var i = 0; i < scripts.length; i++) { + scripts[i] = '{{ UI_MEDIA_URL }}extra/noVNC/include/' + scripts[i] + } + var INCLUDE_URI = ''; + + window.onscriptsload = function () { + UI.load(); + UI.updateSetting('encrypt', true); + UI.connect(); + } + Util.load_scripts(scripts); + + var keyboardEl = document.getElementById("keyboardText") + VKI_imageURI = "{{ UI_MEDIA_URL }}extra/noVNC/images/keyboard.png"; + VKI_attach(keyboardEl); + + $('.help-text a').click(function(e) { + e.preventDefault(); + $('.help-text').slideUp({ + 'duration': 400, + 'easing': 'linear', + }); + }); +</script> + + + </div> - </body> + </body> </html> diff --git a/snf-cyclades-app/synnefo/ui/templates/partials/create_snapshot.html b/snf-cyclades-app/synnefo/ui/templates/partials/create_snapshot.html new file mode 100644 index 0000000000000000000000000000000000000000..3af738bf129301dc0a5b4add4a2a1482efbd808e --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/templates/partials/create_snapshot.html @@ -0,0 +1,28 @@ +<div id="snapshot-create-content" class="overlay-content hidden"> + <div class="create-form snapshot-create-form"> + <p class="success">Snapshot creation started. The created snapshot + will be available in machine and disk create wizards.</p> + <p class="info"></p> + <form> + <div class="col-fields bordered clearfix"> + <div class="form-field"> + <label for="snapshot-create-name">Snapshot name:</label> + <input type="text" class="snapshot-create-name" name="snapshot-create-name" id="snapshot-create-name" /> + </div> + <div class="form-field"> + <div> + <label for="snapshot-create-name">Snapshot description:</label> + </div> + <textarea class="snapshot-create-desc" name="snapshot-create-desc" id="snapshot-create-desc"> + </textarea> + </div> + </div> + <div class="form-actions plain clearfix"> + <span class="form-action create">create snapshot</span> + <span class="form-action btn-close hidden">done</span> + </div> + </form> + </div> + + <div class="ajax-submit"></div> +</div> diff --git a/snf-cyclades-app/synnefo/ui/templates/partials/create_vm.html b/snf-cyclades-app/synnefo/ui/templates/partials/create_vm.html index cdf57914ffd05d88c2a4da39215e6e043249779f..1da03b210020ad97845bb710b68d57c4010dd1da 100644 --- a/snf-cyclades-app/synnefo/ui/templates/partials/create_vm.html +++ b/snf-cyclades-app/synnefo/ui/templates/partials/create_vm.html @@ -1,5 +1,6 @@ {% load i18n %} -<div id="createvm-overlay-content" class="hidden create-vm"> + +<div id="createvm-overlay-content" class="hidden create-wizard-overlay create-vm wizard-overlay-content"> <div class="steps-history clearfix"> <div class="steps-history-cont step1h completed step-header" id="vm-create-step-history-1"> <div class="steps-history-step clearfix"> @@ -97,55 +98,15 @@ <div class="clearfix step-header"> </div> <div class="step-cont clearfix"> - <div class="images-filter-cont content-cont"> - <div class="images-filters"> - <div class="image-types-cont"> - <h4>Image type</h4> - <ul class="type-filter"> - </ul> - </div> - </div> - </div> - <div class="images-list-cont content-cont"> - <h4>{% trans "Available images" %} - <span class="loading-indicator"></span> - <span class="custom-action"> - <a href="#" class="register-custom-image">{% trans "create image" %}</a> - </span> - </h4> - <ul class="images-list"> - </ul> - <span class="empty"> - {% trans "No images available" %}. - </span> - {% if custom_image_help_url %} - <span class="custom-image-help" style="display:none"> - <br /><br /> - {% blocktrans with custom_image_help_url as url %} - Consult <a href="{{ url }}">this guide</a> if you want to create a custom OS image - {% endblocktrans %} - </span> - {% endif %} - <span class="loading hidden">{% trans "loading..." %}</span> - </div> - <div class="images-info-cont content-cont"> - <h4>Image title</h4> - <span class="hide">close details</span> - <div class="image-detail description clearfix"> - <span class="title">{% trans "Description" %}</span> - <p></p> - </div> - <h3>Image metadata</h3> - <div class="extra-details clearfix"> - </div> - - </div> + {% include "partials/wizard_images_step.html" %} </div> </div> <div class="step-2 select-flavor create-step-cont clearfix"> <div class="clearfix step-header"> </div> <div class="step-cont clearfix"> + <div class="project-select"> + </div> <div class="flavors-predefined-cont content-cont"> <div class="flavors-predefined"> <h4>{% trans "Predefined" %}</h4> @@ -250,11 +211,6 @@ <div class="values clearfix"> </div> </li> - <!--<li class="predefined-meta clearfix">--> - <!--<span class="key">{% trans "Public key" %}</span>--> - <!--<input type="file"/>--> - <!--<textarea height="20"></textarea>--> - <!--</li>--> </ul> </div> </div> @@ -268,7 +224,7 @@ <div class="rename"> <div class="form-field"> <h4>Machine name</h4> - <h3 class="vm-name"> + <h3 class="vm-name item-name"> </h3> </div> @@ -300,9 +256,16 @@ <span class="value"></span> </li> </ul> + <h4>{% trans "Project" %}</h4> + <ul class="confirm-params"> + <li class="param flavor-project clearfix"> + <h4 class="project-name value"></h4> + </li> + </ul> </div> <div class="list-cont confirm-cont flavor"> <h4>{% trans "Flavor" %}</h4> + <ul class="confirm-params"> <li class="param flavor-cpu clearfix"> <span class="title">{% trans "CPUs" %}</span> @@ -325,7 +288,7 @@ <ul class="confirm-params meta"> </ul> </div> - <div class="list-cont confirm-cont meta"> + <div class="list-cont confirm-cont meta last"> <h4>{% trans "SSH Keys" %}</h4> <ul class="confirm-params ssh"> </ul> @@ -353,6 +316,7 @@ <div class="create-controls clearfix"> <div class="form-action cancel">{% trans "cancel" %}</div> <div class="form-action next">{% trans "next" %}</div> + <div class="no-project-notice">{% trans "No quotas available." %}</div> <div class="form-action prev">{% trans "previous" %}</div> <div class="form-action create submit">{% trans "create machine" %}</div> </div> @@ -364,7 +328,7 @@ </div> <div class="password-cont clearfix"> <div class="password-label">{% blocktrans %}Write down your password now:{% endblocktrans %}</div> - <div class="password" id="new-machine-password">password</div> + <input class="password reset" readonly id="new-machine-password"/> <div class="clipboard new-vm-password-copy"></div> </div> <div class="description subinfo"> diff --git a/snf-cyclades-app/synnefo/ui/templates/partials/create_volume.html b/snf-cyclades-app/synnefo/ui/templates/partials/create_volume.html new file mode 100644 index 0000000000000000000000000000000000000000..5e364de5672f8f5cb770e10e630da15863c18505 --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/templates/partials/create_volume.html @@ -0,0 +1,220 @@ +{% load i18n %} + +<div id="createvolume-overlay-content" class="hidden create-wizard-overlay create-volume wizard-overlay-content"> + <div class="steps-history clearfix"> + <div class="steps-history-cont step1h current step-header" id="vm-create-step-history-1"> + <div class="steps-history-step clearfix"> + <span class="header-step step-1 clearfix"> + <span class="num">1<span class="subnum">/5</span></span> + <span class="title"> + {% trans "Contents" %} + </span> + <div class="info"> + <span class="subtitle"> + {% trans "Select disk contents" %} + </span> + <span class="description"> + {% trans "Choose a source for the disk contents." %} + </span> + </div> + </span> + </div> + </div> + <div class="steps-history-cont step2h step-header" id="vm-create-step-history-2" > + <div class="steps-history-step clearfix"> + <span class="header-step step-2 clearfix"> + <span class="num">2<span class="subnum">/5</span></span> + <span class="title"> + {% trans "Size" %} + </span> + <div class="info"> + <span class="subtitle"> + {% trans "Select disk size" %} + </span> + <span class="description"> + {% trans "Set disk size and project" %} + </span> + </div> + </span> + </div> + </div> + <div class="steps-history-cont step3h step-header" id="vm-create-step-history-3" > + <div class="steps-history-step clearfix"> + <span class="header-step step-3 clearfix"> + <span class="num">3<span class="subnum">/5</span></span> + <span class="title"> + {% trans "Machine" %} + </span> + <div class="info"> + <span class="subtitle"> + {% trans "Select machine" %} + </span> + <span class="description"> + {% trans "Select machine to assign disk to" %} + </span> + </div> + </span> + </div> + </div> + <div class="steps-history-cont step4h step-header" id="vm-create-step-history-4" > + <div class="steps-history-step clearfix"> + <span class="header-step step-4 clearfix"> + <span class="num">4<span class="subnum">/5</span></span> + <span class="title"> + {% trans "Details" %} + </span> + <div class="info"> + <span class="subtitle"> + {% trans "Set disk details" %} + </span> + <span class="description"> + {% trans "Provide disk name and info" %} + </span> + </div> + </span> + </div> + </div> + <div class="steps-history-cont step5h last step-header" id="vm-create-step-history-5" > + <div class="steps-history-step clearfix"> + <span class="header-step step-5 clearfix"> + <span class="num">5<span class="subnum">/5</span></span> + <span class="title"> + {% trans "Confirm" %} + </span> + <div class="info"> + <span class="subtitle"> + {% trans "Confirm your settings" %} + </span> + <span class="description"> + {% trans "Confirm that the options you have selected are correct" %} + </span> + </div> + </span> + </div> + </div> + </div> + <div class="steps-container clearfix"> + <div class="step-1 select-image wide create-step-cont clearfix"> + <div class="clearfix step-header"> + </div> + <div class="step-cont clearfix"> + {% include "partials/wizard_images_step.html" %} + </div> + </div> + + <div class="step-2 select-volume-details create-step-cont clearfix"> + <div class="clearfix step-header"> + </div> + <div class="step-cont clearfix"> + + <div class="project-select"> + </div> + + <div class="list-cont"> + <h4>{% trans "Size" %} </h4> + <div class="clearfix slider-volume-size-cont"> + <input type="text" + class="size-slider" + data-slider-theme="volume" /> + <span class="metric">GB</span> + </div> + </div> + </div> + </div> + + <div class="step-3 select-volume-machine create-step-cont clearfix"> + <div class="clearfix step-header"> + </div> + <div class="step-cont clearfix"> + <div class="rename"> + <div class="personalize-conts clearfix top"> + <h4>{% trans "Machine" %}</h4> + <p class="desc"> + Select machine to connect disk to + </p> + <div class="vms-list"> + </div> + </div> + </div> + </div> + </div> + + <div class="step-4 select-volume-details create-step-cont clearfix"> + <div class="clearfix step-header"> + </div> + <div class="step-cont clearfix"> + <div class="rename"> + <div class="form-field"> + <h4><label for="volume-name"> + {% trans "Name" %} + </label></h4> + <input class="volume-name rename-field" name="volume-name" /> + </div> + <div class="form-field volume-description"> + <h4><label for="volume-description"> + {% trans "Info" %} + </label></h4> + <textarea class="volume-info rename-field" name="volume-info"></textarea> + </div> + </div> + </div> + </div> + <div class="step-5 vm-confirm create-step-cont clearfix"> + <div class="clearfix step-header"> + </div> + <div class="step-cont clearfix"> + <div class="rename"> + <div class="form-field"> + <h4>Disk name</h4> + <h3 class="volume-name item-name"> + </h3> + </div> + + <div class="confirm-conts clearfix"> + <div class="list-cont confirm-cont wide image"> + <h4>{% trans "Size" %}</h4> + <ul class="confirm-params"> + <li class="param volume-size clearfix"></li> + </ul> + <h4>{% trans "Machine" %}</h4> + <ul class="confirm-params"> + <li class="param volume-machine clearfix"></li> + </ul> + <h4>{% trans "Contents" %}</h4> + <ul class="confirm-params"> + <li class="param image-name clearfix"></li> + </ul> + </div> + <div class="list-cont confirm-cont wide info last"> + <h4>{% trans "Project" %}</h4> + <ul class="confirm-params"> + <li class="param flavor-project clearfix"> + <h4 class="project-name value"></h4> + </li> + </ul> + <h4>{% trans "Info" %}</h4> + <ul class="confirm-params"> + <li class="param volume-info clearfix"></li> + </ul> + </div> + </div> + </div> + </div> + </div> + </div> + <div class="image-warning clearfix"> + <p> + {% blocktrans %}You have selected a user-provided Image, which is not + officially endorsed by {{ BRANDING_SERVICE_NAME }}. Please make sure it is from a + trustworthy source.{% endblocktrans %} + </p> + <span class="untrusted-image-confirm confirm">confirm</span> + </div> + <div class="create-controls clearfix"> + <div class="form-action cancel">{% trans "cancel" %}</div> + <div class="form-action next">{% trans "next" %}</div> + <div class="no-project-notice">{% trans "No quotas available for image." %}</div> + <div class="form-action prev">{% trans "previous" %}</div> + <div class="form-action create submit hidden">{% trans "create disk" %}</div> + </div> +</div> diff --git a/snf-cyclades-app/synnefo/ui/templates/partials/disks.html b/snf-cyclades-app/synnefo/ui/templates/partials/disks.html deleted file mode 100644 index 953fb033669ece53e1eacea87a366b5a5a8e0930..0000000000000000000000000000000000000000 --- a/snf-cyclades-app/synnefo/ui/templates/partials/disks.html +++ /dev/null @@ -1,20 +0,0 @@ -{% load i18n %} -<!-- the create button --> -<div id="machines-pane-top" class="pane-top"> - <div id="createcontainer" class="create-container"> - <a id="create" rel="#wizard" href="#" class="createbutton" >{% trans "New Machine +" %}</a> - </div> - - <!-- changing between standard/list view --> - <div id="view-select" class="clearfix"> - <a class="machines_view_link" id="machines_view_icon_link" href="" title="{% trans "Icon view" %}"> - <span class="ico"></span><span class="title">{% trans "icon" %}</span> - </a> - <a class="machines_view_link" id="machines_view_list_link" href="" title="{% trans "List view" %}"> - <span class="ico"></span><span class="title">{% trans "list" %}</span> - </a> - <a class="machines_view_link" id="machines_view_single_link" href="" title="{% trans "Single view" %}"> - <span class="ico"></span><span class="title">{% trans "single" %}</span> - </a> - </div> -</div> \ No newline at end of file diff --git a/snf-cyclades-app/synnefo/ui/templates/partials/ips.html b/snf-cyclades-app/synnefo/ui/templates/partials/ips.html index 70a03e3b0b640c3864bc9781290061061865dfa9..4eacdbc50f5e6d2c833fe9b14e09712734843b23 100644 --- a/snf-cyclades-app/synnefo/ui/templates/partials/ips.html +++ b/snf-cyclades-app/synnefo/ui/templates/partials/ips.html @@ -1,11 +1,29 @@ {% load i18n %} +<!-- add new network overlay --> +<div id="ips-create-content" class="overlay-content hidden"> + <div class="create-form"> + <p class="info"></p> + <form> + <div class="col-fields clearfix"> + <div class="form-field projects-list project-select"> + </div> + </div> + <div class="form-actions plain clearfix"> + <span class="form-action create">create IP address</span> + </div> + </form> + </div> + + <div class="ajax-submit"></div> +</div> + <div id="ip-port-view-tpl" class="hidden ip-port-view model-item"> <div class="outer"> <div data-rv-class="model.vm.status|vm_status_cls"> <div class="model-logo vm-icon medium2 state1" data-rv-style="model.vm.status|vm_style"></div> - <h3 class="title" data-rv-text="model.vm.name|truncate 30"></h3> + <h3 class="title" data-rv-text="model.vm.name|truncate 50"></h3> <h5 class="subtitle"> <img data-rv-show="model.in_progress" src="{{ SYNNEFO_IMAGES_URL }}icons/indicators/small/progress.gif" @@ -26,23 +44,28 @@ <div id="ip-view-tpl" data-rv-class-actionpending="model.actions.pending" data-rv-class-clearfix="model.id" class="hidden model-item model-view with-actions"> <div class="clearfix"> - <div class="main-content clearfix"> + <div class="main-content clearfix" style="position: relative"> <div class="main-content-inner clearfix"> <img class="logo" data-rv-src="model.status|model_icon" /> <div class="entry inline" data-rv-class-connected="model.device_id"> <h3 class="title floating-ip"> <span data-rv-text="model.floating_ip_address">IP ADDRESS</span> </h3> + <div class="project-name-cont" + data-rv-on-click="view.show_reassign_view" + data-rv-show="model.tenant_id"> + <span class="project-name" data-rv-text="model.project.name|truncate 20|in_brackets"></span> + </div> <div data-rv-if="model.port" class="ports nested-model-list proxy inline"> - <div data-rv-if="model.port"> + <div data-rv-if="model.port" class="ip-port-view-cont"> <div data-rv-show="model.port" data-rv-model-view="model.port|IpPortView"></div> </div> </div> </div> <div class="entry-right"> - <div data-rv-class="model.status|status_cls" class="status"> + <div data-rv-class="model._status|status_cls" class="status"> <div class="status-title"> - <span data-rv-text="model.status|status_display">Active</span> + <span data-rv-text="model._status|status_display">Active</span> <span data-rv-show="model.in_progress">...</span> </div> <div class="status-indicator clearfix"> diff --git a/snf-cyclades-app/synnefo/ui/templates/partials/machines.html b/snf-cyclades-app/synnefo/ui/templates/partials/machines.html index 322c0f145e3209ab9aafe7c6c1cc93ed7b010d62..2ab95357eafdb90ef76347fe5bfcda1f14920d13 100644 --- a/snf-cyclades-app/synnefo/ui/templates/partials/machines.html +++ b/snf-cyclades-app/synnefo/ui/templates/partials/machines.html @@ -7,15 +7,15 @@ <!-- changing between standard/list view --> <div id="view-select" class="clearfix"> - <a class="machines_view_link" id="machines_view_icon_link" href="" title="{% trans "Icon view" %}"> - <span class="ico"></span><span class="title">{% trans "icon" %}</span> - </a> - <a class="machines_view_link" id="machines_view_list_link" href="" title="{% trans "List view" %}"> - <span class="ico"></span><span class="title">{% trans "list" %}</span> - </a> - <a class="machines_view_link" id="machines_view_single_link" href="" title="{% trans "Single view" %}"> - <span class="ico"></span><span class="title">{% trans "single" %}</span> - </a> + <a class="machines_view_link" id="machines_view_icon_link" href="" + title="{% trans "Icon view" %}"> <span class="ico"></span><span + class="title">{% trans "icon" %}</span> </a> <a + class="machines_view_link" id="machines_view_list_link" href="" + title="{% trans "List view" %}"> <span class="ico"></span><span + class="title">{% trans "list" %}</span> </a> <a + class="machines_view_link" id="machines_view_single_link" href="" + title="{% trans "Single view" %}"> <span class="ico"></span><span + class="title">{% trans "single" %}</span> </a> </div> </div> @@ -31,10 +31,11 @@ {% include "partials/create_vm.html" %} {% include "partials/manage_metadata.html" %} {% include "partials/vm_connect.html" %} +{% include "partials/create_snapshot.html" %} <div id="vm-select-collection-tpl" class="hidden"> <div class="collection fixed-ips-list"> - <div class="empty-list hidden">No available machines.</div> + <div class="empty-list hidden">No machines available.</div> <div class="items-list clearfix"></div> </div> </div> @@ -46,16 +47,16 @@ </div> <div class="ico"><img data-rv-src="model.status|get_vm_icon" /></div> <div class="name"> - <span class="" data-rv-text="model.name|truncate 40"></span> <div data-rv-class="model.state|status_cls"> - <span data-rv-text="model.state|status_display"></span> <div class="indicators"> <div class="indicator1"></div> <div class="indicator2"></div> <div class="indicator3"></div> <div class="indicator4"></div> </div> + <span data-rv-text="model.state|status_display"></span> </div> + <span class="" data-rv-text="model.name|truncate_title"></span> </div> </div> </div> @@ -67,16 +68,16 @@ </div> </div> -<div id="vm-port-ip-tpl" class="hidden port-ip-item"> +<div id="vm-port-ip-tpl" class="hidden port-ip-item nested-item"> <img src="{{ SYNNEFO_IMAGES_URL }}/icons/indicators/medium/progress.gif" class="in-progress hidden" /> - <div class="type" data-rv-text="model.type|prefix IP"></div> - <div class="ip" data-rv-text="model.ip_address"></div> + <div class="type-display" data-rv-text="model.type|prefix IP"></div> + <div class="ip title-display" data-rv-text="model.ip_address"></div> </div> <div id="vm-port-view-tpl" class="hidden"> - <div class="ips" data-rv-collection-view="model.ips|VMPortIpsView"></div> - <div class="clearfix network-header"> + <div class="ips nested" data-rv-collection-view="model.ips|VMPortIpsView"></div> + <div class="clearfix network-header nested"> <span data-rv-show="model.in_progress_no_vm|update_in_progress"></span> <img data-rv-src="model.network.is_public|get_network_icon" /> <div class="port" data-rv-text="model.network.name|get_network_name"></div> @@ -98,7 +99,7 @@ </div> </div> -<div id="vm-port-list-view-tpl" class="collection-list-view hidden info-content ips"> +<div id="vm-port-list-view-tpl" class="collection-list-view hidden inner-collection info-content ips"> <div class="collection"> <div class="empty-list hidden"> <p>{% trans "No IP addresses" %}</p> @@ -107,3 +108,71 @@ </div> </div> </div> + +<!-- project select --> +<div id="project-select-content" class="overlay-content hidden"> + <div class="description"> + <p>{% trans "Select project to reassing this object to" %}</p> + </div> + <div class="model-usage clearfix"> + </div> + <div class="clearfix projects-list"> + <ul class="options-list three"> + </ul> + </div> + <div class="form-actions clearfix"> + <span class="form-action submit">{% trans "assign to project" %}</span> + </div> +</div> + +<div id="project-select-collection-tpl" class="hidden"> + <div class="collection"> + <div class="empty-list hidden">No projects available.</div> + <div class="items-list clearfix"></div> + </div> +</div> + +<div id="project-select-model-tpl" class="hidden"> + <div class="select-item clearfix project"> + <div class="checkbox"> + <input type="radio" data-rv-data-id="model.id" name="project-reassign"/> + </div> + <div class="name"> + <span class="" data-rv-text="model.name|truncate 40"></span> + <span data-rv-text="model._project_is_current|is_current_str" class="current"> + </span> + </div> + <div class="quota clearfix"> + <div data-rv-html="model._quotas|quotas_html"></div> + </div> + </div> +</div> + +<div id="vm-volume-list-view-tpl" class="collection-list-view hidden inner-collection info-content volumes"> + <div class="collection"> + <div class="empty-list hidden"> + <p>{% trans "No volumes attached" %}</p> + </div> + <div class="items-list volumes-list clearfix"> + </div> + </div> +</div> + +<div id="vm-volume-view-tpl" class="hidden nested-item"> + <div class="clearfix volume-header"> + <img src="{{ SYNNEFO_IMAGES_URL }}/icons/indicators/medium/progress.gif" + class="in-progress hidden" /> + <div class="size-display index-display" data-rv-text="model._index|prefix Disk #"></div> + <div class="type-display" data-rv-text="model.size|disk_size_display"></div> + <div class="volume title-display" data-rv-text="model.display_name|truncate 34"></div> + <div class="actions-content inline"> + <div class="action-container snapshot warn" + data-rv-class-isactive="model.can_snapshot" + data-rv-class-selected="model.actions.snapshot|intEq 1" + data-rv-on-click="view.show_snapshot_create_overlay"> + <a>Snapshot</a> + </div> + </div> + </div> +</div> + diff --git a/snf-cyclades-app/synnefo/ui/templates/partials/machines_icon.html b/snf-cyclades-app/synnefo/ui/templates/partials/machines_icon.html index 0b493f239bf50c158245aaae658086aa5bba0830..965564e7d9e185fa3e276f732706be610e6e08bb 100644 --- a/snf-cyclades-app/synnefo/ui/templates/partials/machines_icon.html +++ b/snf-cyclades-app/synnefo/ui/templates/partials/machines_icon.html @@ -23,8 +23,11 @@ </div> </h5> </div> + <div class="project-name-cont"> + <span class="project-name"></span> + </div> <div class="machine-ips ip subtitle"> - <div class="fqdn"></div> + <input class="fqdn reset" disabled /> </div> <div class="build-progress subtitle"> <span class="message"></span> @@ -35,9 +38,14 @@ <span class="label">{% trans "info" %}</span> </span> </div> + <div class="cont-toggler-wrapper volumes"> + <span class="info-header cont-toggler toggler"> + <span class="label">{% trans "disks" %}</span> + </span> + </div> <div class="cont-toggler-wrapper ips"> <span class="info-header cont-toggler toggler"> - <span class="label">{% trans "IP addresses" %}</span> + <span class="label">{% trans "IPs" %}</span> </span> </div> </div> diff --git a/snf-cyclades-app/synnefo/ui/templates/partials/machines_single.html b/snf-cyclades-app/synnefo/ui/templates/partials/machines_single.html index 2ded69eed6b971f4bfa5829b0a4eab2be93a57ed..912cb9a099c78d0b3289cc1cb9ef6635732b0321 100644 --- a/snf-cyclades-app/synnefo/ui/templates/partials/machines_single.html +++ b/snf-cyclades-app/synnefo/ui/templates/partials/machines_single.html @@ -15,6 +15,9 @@ <div class="indicator3"></div> <div class="indicator4"></div> </div> + <div class="project-name-cont"> + <span class="project-name"></span> + </div> <div class="action-indicator" style="display:none">empty</div> <img class="spinner" style="display:none" src="{{ SYNNEFO_IMAGES_URL }}icons/indicators/medium/progress.gif" /> <img class="wave" style="display:none" src="{{ SYNNEFO_IMAGES_URL }}icons/indicators/medium/wave.gif" /> @@ -29,7 +32,7 @@ <div class="title machine-detail name">My Desktop</div> <div class="suspended-notice">SUSPENDED</div> <div class="column2"> - <div class="fqdn"></div> + <input class="fqdn reset" disabled /> <div class="machine-labels"> <div class="machine-label cpus">{% trans "CPUs" %}:</div> <div class="machine-label ram">{% trans "RAM (MB)" %}:</div> @@ -51,9 +54,14 @@ <span class="label">{% trans "tags" %}</span> </span> </div> + <div class="cont-toggler-wrapper toggler-header volumes"> + <span class="info-header cont-toggler toggler"> + <span class="label">{% trans "disks" %}</span> + </span> + </div> <div class="cont-toggler-wrapper toggler-header ips"> <span class="info-header cont-toggler toggler"> - <span class="label">{% trans "IP addresses" %}</span> + <span class="label">{% trans "IPs" %}</span> </span> </div> </div> @@ -67,6 +75,8 @@ </div> <div class="ips-content toggler-content hidden info-content"> </div> + <div class="volumes-content toggler-content hidden info-content"> + </div> </div> </div> </div> @@ -102,10 +112,6 @@ </div> <div class="action-container resize"> <a class="single-action action-resize">{% trans "Resize" %}</a> - <div class="confirm_single"> - <button class="yes">{% trans "Confirm" %}</button> - <button class="no">X</button> - </div> </div> <div class="action-container destroy"> <a class="single-action action-destroy">{% trans "Destroy" %}</a> diff --git a/snf-cyclades-app/synnefo/ui/templates/partials/networks.html b/snf-cyclades-app/synnefo/ui/templates/partials/networks.html index 0ce9ae7a535234fa8e40a08be005bfd1b36c9db4..667cd47a48be9ec13119e1567b4185bf3f6944b5 100644 --- a/snf-cyclades-app/synnefo/ui/templates/partials/networks.html +++ b/snf-cyclades-app/synnefo/ui/templates/partials/networks.html @@ -5,21 +5,25 @@ <div class="create-form"> <p class="info"></p> <form> + <div class="col-fields clearfix"> + <div class="form-field projects-list project-select"> + </div> + </div> <div class="col-fields bordered clearfix"> <div class="form-field"> - <label for="network-create-name">Network name:</label> + <label for="network-create-name">Network name</label> <input type="text" class="network-create-name" name="network-create-name" id="network-create-name" /> </div> <div class="form-field right-field"> - <label for="network-create-type">Network type:</label> + <label for="network-create-type">Network type</label> <select id="network-create-type"> </select> </div> </div> <div class="col-fields clearfix bordered"> <div class="form-field fixpos"> - <label for="network-create-dhcp">Assign IP addresses automatically:</label> - <input type="checkbox" class="use-dhcp" name="network-create-dhcp" + <label for="network-create-dhcp">Assign IP addresses automatically</label> + <input type="checkbox" class="use-dhcp" id="network-create-dhcp" name="network-create-dhcp" id="network-create-dhcp" checked /> <p class="description noborder fields-desc"> If you enable DHCP on the private network, @@ -30,16 +34,32 @@ </p> </div> <div id="network-create-dhcp-fields"> - <div class="form-field fixpos"> - <label for="network-type">Network subnet:</label> - <select id="network-create-subnet"> - <option selected class="auto" value="auto">Auto</option> - <option value="custom" class="manual">Manual...</option> - </select> + <div class="clearfix"> + <div class="form-field fixpos"> + <label for="network-create-subnet">Subnet</label> + <select id="network-create-subnet"> + <option selected class="auto" value="auto">Auto</option> + <option value="custom" class="manual">Manual...</option> + </select> + </div> + <div class="form-field"> + <label class="hidden" for="network-create-subnet-custom">Custom subnet</label> + <input type="text" id="network-create-subnet-custom" placeholder="e.g. 192.168.0.1/24"/> + </div> </div> - <div class="form-field"> - <label class="hidden" for="network-custom-subnet">Custom subnet:</label> - <input type="text" id="network-create-subnet-custom"/> + <div class="clearfix"> + <div class="form-field fixpos"> + <label for="network-create-gateway">Gateway</label> + <select id="network-create-gateway"> + <option selected class="auto" value="auto">Auto</option> + <option value="custom" class="manual">Manual...</option> + <option value="none">None</option> + </select> + </div> + <div class="form-field"> + <label class="hidden" for="network-create-gateway-custom">Custom gateway</label> + <input type="text" id="network-create-gateway-custom" placeholder="e.g. 192.168.0.1"/> + </div> </div> </div> </div> @@ -173,13 +193,18 @@ data-rv-class-clearfix="model.id" class="hidden model-item model-view with-actions"> <div class="clearfix"> - <div class="main-content clearfix"> + <div class="main-content clearfix" style="position: relative"> <div class="main-content-inner clearfix"> <img class="logo" data-rv-src="model.is_public|get_network_icon" /> <div class="entry"> <div data-rv-show="model" data-rv-model-view="model|ModelRenameView"> </div> + <div class="project-name-cont" + data-rv-on-click="view.show_reassign_view" + data-rv-show="model.tenant_id"> + <span class="project-name" data-rv-text="model.project.name|truncate 20|in_brackets"></span> + </div> <div class="toggler-wrap clearfix network-ports-toggler"> <div class="cont-toggler"> <span class="label machines-label"> @@ -264,7 +289,10 @@ <div class="checkbox"> <input type="checkbox" data-rv-data-id="model.id" /> </div> - <div class="name" data-rv-text="model.floating_ip_address"></div> + <div class="name"> + <span data-rv-text="model.floating_ip_address"></span> + <span class="project-name" data-rv-text="model.project.name|truncate 30"></span> + </div> </div> </div> @@ -274,11 +302,11 @@ </div> <div class="create model-item select-item floating-ip clearfix"> <span class="empty-list hidden" style="padding-left:0;"> - {% trans "No ip addresses available" %} + {% trans "No IP addresses available" %} </span> <a href="#">create new...</a> <span class="loading">creating...</span> - <span class="no-available hidden">{% trans "No IPs available" %}</span> + <span class="no-available hidden">{% trans "No IP addresses available" %}</span> </div> </div> </div> @@ -298,7 +326,8 @@ </div> </div> <div data-rv-show="model.is_public" class="floating-ips"> - <div data-rv-collection-view="model.available_floating_ips|NetworkSelectFloatingIpsView"> + <div + data-rv-collection-view="model.available_floating_ips|NetworkSelectFloatingIpsView,resolve_floating_ip_view_params"> </div> </div> </div> diff --git a/snf-cyclades-app/synnefo/ui/templates/partials/volumes.html b/snf-cyclades-app/synnefo/ui/templates/partials/volumes.html new file mode 100644 index 0000000000000000000000000000000000000000..1b35e13c7f5e4d23c74de96d2fb5f858afb4c07c --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/templates/partials/volumes.html @@ -0,0 +1,131 @@ +{% load i18n %} + +{% include "partials/create_volume.html" %} + +<div id="volumes-create-content" class="overlay-content hidden"> + <div class="ajax-submit"></div> +</div> + +<div id="volumes-list-view" class="collection-list-view hidden"> + <div class="collection"> + + <div id="create-volume" class="create-button"> + <a href="#">{% trans "New Disk +" %}</a> + </div> + + <div class="empty-list hidden"> + <p>{% trans "No disks available." %}</p> + </div> + + <div class="items-list clearfix"> + <div class="system items-sublist"></div> + <div class="custom items-sublist"></div> + </div> + </div> +</div> + +<div id="volume-vm-view-tpl" class="hidden"> + <div class="model-item"> + <div class="outer"> + <div data-rv-class="model.vm.status|vm_status_cls"> + <div class="model-logo vm-icon medium2 state1" + data-rv-style="model.vm.status|vm_style"></div> + <h3 class="title" data-rv-text="model.vm.name|truncate 40"></h3> + <h5 class="subtitle"> + <img data-rv-show="model.in_progress" + src="{{ SYNNEFO_IMAGES_URL }}icons/indicators/small/progress.gif" + class="progress-indicator" /> + <div data-rv-hide="model.in_progress"> + <span data-rv-show="model._index_set" class="key">Disk</span> + <span data-rv-show="model._index_set" class="value" data-rv-text="model._index|prefix #"></span> + <span data-rv-show="model._index_set" class="value" data-rv-html="model.vm|flavor_tpl"></span> + </div> + </h5> + </div> + </div> + </div> +</div> + +<div id="volume-view-tpl" data-rv-class-actionpending="model.actions.pending" + data-rv-class-clearfix="model.id" class="hidden volume-item model-item model-view with-actions"> + <div class="clearfix"> + <div class="main-content clearfix" style="position: relative"> + <div class="main-content-inner clearfix"> + <img class="logo" data-rv-src="model.status|model_icon" /> + <div class="volume-size"> + <span data-rv-text="model.size|size_display"></span> + </div> + <div class="entry inline" data-rv-class-connected="model.device_id"> + <div data-rv-show="model" + data-rv-model-view="model|VolumeItemRenameView"> + </div> + <div class="project-name-cont" + data-rv-on-click="view.show_reassign_view" + data-rv-class="model.is_root|check_can_reassign", + data-rv-show="model.tenant_id"> + <span class="project-name" data-rv-text="model.project.name|truncate 20|in_brackets"></span> + </div> + <div data-rv-if="model.vm" class="vms nested-model-list proxy inline"> + <div class="vm-view-cont"> + <div data-rv-show="model.vm" data-rv-model-view="model|VolumeVmView"></div> + </div> + </div> + <div class="toggler-wrap clearfix"> + <div class="cont-toggler desc"> + <span class="label">info</span> + </div> + </div> + <div class="content-cont"> + <div class="model-rename-view"> + <textarea data-rv-value="model.display_description|msg_if_empty No description"></textarea> + <span class="rename-desc-btn edit-btn"></span> + <div class="rename-actions"> + <div class="btn confirm"></div> + <div class="btn cancel"></div> + </div> + </div> + </div> + </div> + <div class="entry-right"> + <div data-rv-class="model._status|status_cls" class="status"> + <div class="status-title"> + <span data-rv-text="model._status|status_display">Active</span> + </div> + <div class="status-indicator clearfix"> + <div class="indicator indicator1"></div> + <div class="indicator indicator2"></div> + <div class="indicator indicator3"></div> + <div class="indicator indicator4"></div> + </div> + <div class="state state-indicator"> + <div class="action-indicator"></div> + </div> + <img data-rv-show="model.in_progress" + src="{{ SYNNEFO_IMAGES_URL }}icons/indicators/small/progress.gif" + class="progress-indicator spinner" /> + </div> + </div> + </div> + </div> + <div class="actions-content"> + <div class="action-container snapshot warn" + data-rv-class-isactive="model.can_snapshot" + data-rv-class-selected="model.actions.snapshot|intEq 1" + data-rv-on-click="view.show_snapshot_create_overlay"> + <a>Snapshot</a> + </div> + <div class="action-container remove warn" + data-rv-class-isactive="model.can_remove" + data-rv-class-selected="model.actions.remove|intEq 1" + data-rv-on-click="view.set_remove_confirm"> + <a>Destroy</a> + <div class="confirm-single clearfix"> + <span class="yes" data-rv-on-click="view.remove"> + {% trans "Confirm" %} + </span> + <span class="no" data-rv-on-click="view.unset_remove_confirm">X</span> + </div> + </div> + </div> + </div> +</div> diff --git a/snf-cyclades-app/synnefo/ui/templates/partials/wizard_images_step.html b/snf-cyclades-app/synnefo/ui/templates/partials/wizard_images_step.html new file mode 100644 index 0000000000000000000000000000000000000000..a6c523036f449a67774d7700b2315b97d52d13de --- /dev/null +++ b/snf-cyclades-app/synnefo/ui/templates/partials/wizard_images_step.html @@ -0,0 +1,63 @@ +{% load i18n %} +<div class="images-filter-cont content-cont"> + <div class="images-filters"> + <div class="other-types-cont hidden cont" + data-list-title="Available disks" + data-list-empty="No disks available." + > + <ul class="type-filter" > + <h4>Disks</h4> + <li id="type-select-empty">Empty</li> + </ul> + </div> + <div class="image-types-cont cont" + data-list-title="Available images" + data-list-empty="No images available." + > + <h4>Images</h4> + <ul class="type-filter"> + </ul> + </div> + <div class="snapshot-types-cont cont" + data-list-title="Available snapshots" + data-list-empty="No snapshots available." + > + <h4>Snapshots</h4> + <ul class="type-filter"> + </ul> + </div> + </div> +</div> +<div class="images-list-cont content-cont"> + <h4>{% trans "Available images" %} + <span class="loading-indicator"></span> + <span class="custom-action"> + <a href="#" class="register-custom-image">{% trans "create image" %}</a> + </span> + </h4> + <ul class="images-list"> + </ul> + <span class="empty"> + {% trans "No images available" %}. + </span> + {% if custom_image_help_url %} + <span class="custom-image-help" style="display:none"> + <br /><br /> + {% blocktrans with custom_image_help_url as url %} + Consult <a href="{{ url }}">this guide</a> if you want to create a custom OS image + {% endblocktrans %} + </span> + {% endif %} + <span class="loading hidden">{% trans "loading..." %}</span> +</div> +<div class="images-info-cont content-cont"> + <h4>Image title</h4> + <span class="hide">close details</span> + <div class="image-detail description clearfix"> + <span class="title">{% trans "Description" %}</span> + <p></p> + </div> + <h3>Image metadata</h3> + <div class="extra-details clearfix"> + </div> +</div> diff --git a/snf-cyclades-app/synnefo/ui/tests.py b/snf-cyclades-app/synnefo/ui/tests.py index 71b4c11c6ea0ee95331f407f6c6a7fcfe94ab324..2e5d8ba30d0776e4c78068ce58d1bd8e1897bcbb 100644 --- a/snf-cyclades-app/synnefo/ui/tests.py +++ b/snf-cyclades-app/synnefo/ui/tests.py @@ -1,35 +1,17 @@ -# Copyright 2011 GRNET S.A. All rights reserved. -# -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: -# -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. -# -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # from django.test import TestCase from selenium import selenium diff --git a/snf-cyclades-app/synnefo/ui/urls.py b/snf-cyclades-app/synnefo/ui/urls.py index b90efcbe5a6176d28dd08f77e6eb61f2df967fcd..6e02728d7325ecb4900fc0465b712185de0f827a 100644 --- a/snf-cyclades-app/synnefo/ui/urls.py +++ b/snf-cyclades-app/synnefo/ui/urls.py @@ -1,35 +1,17 @@ -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # from django.conf.urls import patterns, url diff --git a/snf-cyclades-app/synnefo/ui/views.py b/snf-cyclades-app/synnefo/ui/views.py index 1a11989fa711e6148d51aa8616ac57d1de4a03f7..e24d86449dad0775d3f5b04d694f3746d419b6f3 100644 --- a/snf-cyclades-app/synnefo/ui/views.py +++ b/snf-cyclades-app/synnefo/ui/views.py @@ -1,35 +1,17 @@ -# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # import os @@ -124,9 +106,6 @@ UI_SYNNEFO_JS_LIB_URL = \ "UI_SYNNEFO_JS_LIB_URL", UI_SYNNEFO_JS_URL + "lib/") UI_SYNNEFO_JS_WEB_URL = \ getattr(settings, "UI_SYNNEFO_JS_WEB_URL", UI_SYNNEFO_JS_URL + "ui/web/") -UI_SYNNEFO_FONTS_BASE_URL = \ - getattr(settings, - "UI_FONTS_BASE_URL", "//fonts.googleapis.com/") # extensions ENABLE_GLANCE = getattr(settings, 'UI_ENABLE_GLANCE', True) @@ -169,6 +148,9 @@ DEFAULT_HOTPLUG_ENABLED = getattr(settings, "CYCLADES_GANETI_USE_HOTPLUG", HOTPLUG_ENABLED = getattr(settings, "UI_HOTPLUG_ENABLED", DEFAULT_HOTPLUG_ENABLED) +VOLUME_MAX_SIZE = getattr(settings, "CYCLADES_VOLUME_MAX_SIZE", 200) +SNAPSHOTS_ENABLED = getattr(settings, "CYCLADES_SNAPSHOTS_ENABLED", True) + def template(name, request, context): template_path = os.path.join(os.path.dirname(__file__), "templates/") current_template = template_path + name + '.html' @@ -177,7 +159,6 @@ def template(name, request, context): 'UI_MEDIA_URL': UI_MEDIA_URL, 'SYNNEFO_JS_URL': UI_SYNNEFO_JS_URL, 'SYNNEFO_JS_LIB_URL': UI_SYNNEFO_JS_LIB_URL, - 'SYNNEFO_FONTS_BASE_URL': UI_SYNNEFO_FONTS_BASE_URL, 'SYNNEFO_JS_WEB_URL': UI_SYNNEFO_JS_WEB_URL, 'SYNNEFO_IMAGES_URL': UI_SYNNEFO_IMAGES_URL, 'SYNNEFO_CSS_URL': UI_SYNNEFO_CSS_URL, @@ -195,6 +176,7 @@ def home(request): 'request': request, 'current_lang': get_language() or 'en', 'compute_api_url': json.dumps(uisettings.COMPUTE_URL), + 'volume_api_url': json.dumps(uisettings.VOLUME_URL), 'network_api_url': json.dumps(uisettings.NETWORK_URL), 'user_catalog_url': json.dumps(uisettings.USER_CATALOG_URL), 'feedback_post_url': json.dumps(uisettings.FEEDBACK_URL), @@ -245,7 +227,9 @@ def home(request): 'group_public_networks': json.dumps(GROUP_PUBLIC_NETWORKS), 'hotplug_enabled': json.dumps(HOTPLUG_ENABLED), 'diagnostics_update_interval': json.dumps(DIAGNOSTICS_UPDATE_INTERVAL), - 'no_fqdn_message': json.dumps(NO_FQDN_MESSAGE) + 'no_fqdn_message': json.dumps(NO_FQDN_MESSAGE), + 'volume_max_size': json.dumps(VOLUME_MAX_SIZE), + 'snapshots_enabled': json.dumps(SNAPSHOTS_ENABLED) } return template('home', request, context) @@ -256,11 +240,9 @@ def machines_console(request): port = request.GET.get('port', '') password = request.GET.get('password', '') machine = request.GET.get('machine', '') - host_ip = request.GET.get('host_ip', '') - host_ip_v6 = request.GET.get('host_ip_v6', '') + machine_hostname = request.GET.get('machine_hostname', '') context = {'host': host, 'port': port, 'password': password, - 'machine': machine, 'host_ip': host_ip, - 'host_ip_v6': host_ip_v6} + 'machine': machine, 'machine_hostname': machine_hostname} return template('machines_console', request, context) diff --git a/snf-cyclades-app/synnefo/userdata/models.py b/snf-cyclades-app/synnefo/userdata/models.py index 96be9108b5e9d27b2ce92649eb91b23be13615ad..a1a07c079b26c110ce3b518fa43e18835263e286 100644 --- a/snf-cyclades-app/synnefo/userdata/models.py +++ b/snf-cyclades-app/synnefo/userdata/models.py @@ -1,36 +1,18 @@ # -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import base64 import binascii diff --git a/snf-cyclades-app/synnefo/userdata/rest.py b/snf-cyclades-app/synnefo/userdata/rest.py index 522c6631ccde9a9ce8bb0d647d6ef15e683cb0bb..f8c28fc715c301fd50310af6c0f952fab1f5d519 100644 --- a/snf-cyclades-app/synnefo/userdata/rest.py +++ b/snf-cyclades-app/synnefo/userdata/rest.py @@ -1,36 +1,18 @@ # -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import logging diff --git a/snf-cyclades-app/synnefo/userdata/tests.py b/snf-cyclades-app/synnefo/userdata/tests.py index c1d915e1f9a121e4a3c019bde121e3b0d411afa7..16a2be795055fc4e5ed079da5c9bcb6bf5662e60 100644 --- a/snf-cyclades-app/synnefo/userdata/tests.py +++ b/snf-cyclades-app/synnefo/userdata/tests.py @@ -1,35 +1,17 @@ -# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # from django import http diff --git a/snf-cyclades-app/synnefo/userdata/urls.py b/snf-cyclades-app/synnefo/userdata/urls.py index 6d8a437502e5d4f1792cdd775597d3aad7e20db1..ab25bd2ea8160e7e249b4ea0a79b6edad3963325 100644 --- a/snf-cyclades-app/synnefo/userdata/urls.py +++ b/snf-cyclades-app/synnefo/userdata/urls.py @@ -1,36 +1,18 @@ # -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.conf.urls import patterns, url diff --git a/snf-cyclades-app/synnefo/userdata/util.py b/snf-cyclades-app/synnefo/userdata/util.py index b059b8315d5b3fe79b41a50e3820d43be540e1d0..0e0982a55466d4b6c0dea50d7682031659c7055e 100644 --- a/snf-cyclades-app/synnefo/userdata/util.py +++ b/snf-cyclades-app/synnefo/userdata/util.py @@ -1,31 +1,17 @@ -# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import binascii diff --git a/snf-cyclades-app/synnefo/userdata/views.py b/snf-cyclades-app/synnefo/userdata/views.py index 8a4607d7fbc79863106094df35437cfa0cbe2d71..b2a3ca0e86fe4695c6a88a4f825e3ffefaa35b9a 100644 --- a/snf-cyclades-app/synnefo/userdata/views.py +++ b/snf-cyclades-app/synnefo/userdata/views.py @@ -1,36 +1,18 @@ # -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django import http from django.utils import simplejson as json diff --git a/snf-cyclades-app/synnefo/versions/__init__.py b/snf-cyclades-app/synnefo/versions/__init__.py index c78fbe672324992f6c941a7828e4827928b270c9..5cda842903d0bfec117db1c3c5a43281d46eb551 100644 --- a/snf-cyclades-app/synnefo/versions/__init__.py +++ b/snf-cyclades-app/synnefo/versions/__init__.py @@ -1,35 +1,17 @@ -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # this is a namespace package try: diff --git a/snf-cyclades-app/synnefo/vmapi/__init__.py b/snf-cyclades-app/synnefo/vmapi/__init__.py index fa60c961b438845fb48fd0257417cb70b0acb6a3..349ff2545f7965ac9e3f7a14ad45790ab6b906fd 100644 --- a/snf-cyclades-app/synnefo/vmapi/__init__.py +++ b/snf-cyclades-app/synnefo/vmapi/__init__.py @@ -1,35 +1,17 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from uuid import uuid4 diff --git a/snf-cyclades-app/synnefo/vmapi/models.py b/snf-cyclades-app/synnefo/vmapi/models.py index bbab0e7d6b47b734858a4267036cf913bdc90d39..11769019eed5e6677f40be9026915069cdbd474a 100644 --- a/snf-cyclades-app/synnefo/vmapi/models.py +++ b/snf-cyclades-app/synnefo/vmapi/models.py @@ -1,35 +1,17 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from logging import getLogger diff --git a/snf-cyclades-app/synnefo/vmapi/settings.py b/snf-cyclades-app/synnefo/vmapi/settings.py index 0f8a9eacde0c77cdb5658af2094e38bf0d5e0e7e..48554700dbdfd0a1e9b659de3b35c34e3cc51aaf 100644 --- a/snf-cyclades-app/synnefo/vmapi/settings.py +++ b/snf-cyclades-app/synnefo/vmapi/settings.py @@ -1,35 +1,17 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.conf import settings from synnefo.cyclades_settings import BASE_URL, BASE_HOST, BASE_PATH diff --git a/snf-cyclades-app/synnefo/vmapi/tests.py b/snf-cyclades-app/synnefo/vmapi/tests.py index ba63a4365ed4348b5b739fa73dd2bf907a740e09..a14742dd63107580902053a96fa2c92861de0c10 100644 --- a/snf-cyclades-app/synnefo/vmapi/tests.py +++ b/snf-cyclades-app/synnefo/vmapi/tests.py @@ -1,36 +1,18 @@ # -*- coding: utf8 -*- -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.test import TestCase from django.utils import simplejson as json diff --git a/snf-cyclades-app/synnefo/vmapi/urls.py b/snf-cyclades-app/synnefo/vmapi/urls.py index 3728c62943b932ad9be9938e3e766631d4ef63f5..e42c2c36ae72e4ae1bb0646de6b5e05f40b4591f 100644 --- a/snf-cyclades-app/synnefo/vmapi/urls.py +++ b/snf-cyclades-app/synnefo/vmapi/urls.py @@ -1,35 +1,17 @@ -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.conf.urls import patterns, url diff --git a/snf-cyclades-app/synnefo/vmapi/views.py b/snf-cyclades-app/synnefo/vmapi/views.py index 3d2a1b06c7fd066d20c9690969b5ebb9cd7b6aa5..97bad406358beb179f0acecc0a5946cd5e5706f2 100644 --- a/snf-cyclades-app/synnefo/vmapi/views.py +++ b/snf-cyclades-app/synnefo/vmapi/views.py @@ -1,35 +1,17 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from logging import getLogger diff --git a/snf-cyclades-app/synnefo/volume/__init__.py b/snf-cyclades-app/synnefo/volume/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/snf-cyclades-app/synnefo/volume/management/__init__.py b/snf-cyclades-app/synnefo/volume/management/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/snf-cyclades-app/synnefo/volume/management/commands/__init__.py b/snf-cyclades-app/synnefo/volume/management/commands/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/snf-cyclades-app/synnefo/volume/management/commands/snapshot-create.py b/snf-cyclades-app/synnefo/volume/management/commands/snapshot-create.py new file mode 100644 index 0000000000000000000000000000000000000000..caba1eddc27a6d02a53e330c35882a42da5f04ea --- /dev/null +++ b/snf-cyclades-app/synnefo/volume/management/commands/snapshot-create.py @@ -0,0 +1,64 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from optparse import make_option + +from snf_django.management.commands import SynnefoCommand, CommandError +from synnefo.management import common +#from snf_django.management.utils import parse_bool +from synnefo.volume import snapshots + + +class Command(SynnefoCommand): + args = "<volume ID>" + help = "Create a snapshot from the specified volume" + + option_list = SynnefoCommand.option_list + ( + make_option( + "--name", + dest="name", + default=None, + help="Display name of the snapshot"), + make_option( + "--description", + dest="description", + default=None, + help="Display description of the snapshot"), + ) + + @common.convert_api_faults + def handle(self, *args, **options): + if len(args) != 1: + raise CommandError("Please provide a volume ID") + + volume = common.get_resource("volume", args[0], for_update=True) + + name = options.get("name") + if name is None: + raise CommandError("'name' option is required") + + description = options.get("description") + if description is None: + description = "Snapshot of Volume '%s'" % volume.id + + snapshot = snapshots.create(volume.userid, + volume, + name=name, + description=description, + metadata={}) + + msg = ("Created snapshot of volume '%s' with ID %s\n" + % (volume.id, snapshot["id"])) + self.stdout.write(msg) diff --git a/snf-cyclades-app/synnefo/volume/management/commands/snapshot-list.py b/snf-cyclades-app/synnefo/volume/management/commands/snapshot-list.py new file mode 100644 index 0000000000000000000000000000000000000000..ec09259c6183df2dec07889d8b2ced56a98dcf3e --- /dev/null +++ b/snf-cyclades-app/synnefo/volume/management/commands/snapshot-list.py @@ -0,0 +1,58 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +from snf_django.management.commands import SynnefoCommand +from optparse import make_option + +from snf_django.management.utils import pprint_table +from synnefo.plankton.backend import PlanktonBackend + + +class Command(SynnefoCommand): + help = "List snapshots." + option_list = SynnefoCommand.option_list + ( + make_option( + '--user', + dest='userid', + default=None, + help="List only snapshots that are available to this user."), + make_option( + '--public', + dest='public', + action="store_true", + default=False, + help="List only public snapshots."), + ) + + def handle(self, **options): + user = options['userid'] + check_perm = user is not None + + with PlanktonBackend(user) as backend: + snapshots = backend.list_snapshots(user, + check_permissions=check_perm) + if options['public']: + snapshots = filter(lambda x: x['is_public'], snapshots) + + headers = ("id", "name", "volume_id", "size", "mapfile", "status", + "owner", "is_public") + table = [] + for snap in snapshots: + fields = (snap["id"], snap["name"], snap["volume_id"], + snap["size"], snap["mapfile"], snap["status"], + snap["owner"], snap["is_public"]) + table.append(fields) + pprint_table(self.stdout, table, headers) diff --git a/snf-cyclades-app/synnefo/volume/management/commands/snapshot-modify.py b/snf-cyclades-app/synnefo/volume/management/commands/snapshot-modify.py new file mode 100644 index 0000000000000000000000000000000000000000..8daa04b6529ef4c77d617f826a2f6f92b04aa199 --- /dev/null +++ b/snf-cyclades-app/synnefo/volume/management/commands/snapshot-modify.py @@ -0,0 +1,58 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +from optparse import make_option +from django.core.management.base import CommandError +from synnefo.volume import snapshots, util +from synnefo.management import common +from snf_django.management.commands import SynnefoCommand + + +class Command(SynnefoCommand): + args = "<Snapshot ID>" + help = "Modify a snapshot" + option_list = SynnefoCommand.option_list + ( + make_option( + "--user", + dest="user", + default=None, + help="UUID of the owner of the snapshot"), + make_option( + "--name", + dest="name", + default=None, + help="Update snapshot's name"), + make_option( + "--description", + dest="description", + default=None, + help="Update snapshot's description"), + ) + + @common.convert_api_faults + def handle(self, *args, **options): + if not args: + raise CommandError("Please provide a snapshot ID") + + snapshot_id = args[0] + userid = options["user"] + name = options["name"] + description = options["description"] + + snapshot = util.get_snapshot(userid, snapshot_id) + + snapshots.update(snapshot, name=name, description=description) + self.stdout.write("Successfully updated snapshot %s\n" + % snapshot_id) diff --git a/snf-cyclades-app/synnefo/volume/management/commands/snapshot-remove.py b/snf-cyclades-app/synnefo/volume/management/commands/snapshot-remove.py new file mode 100644 index 0000000000000000000000000000000000000000..b3140415e21f49ae53011a4c2f5fc3e7471a8470 --- /dev/null +++ b/snf-cyclades-app/synnefo/volume/management/commands/snapshot-remove.py @@ -0,0 +1,53 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +from optparse import make_option +from django.core.management.base import CommandError +from synnefo.volume import snapshots, util +from synnefo.management import common +from snf_django.management.commands import RemoveCommand + + +class Command(RemoveCommand): + args = "<Snapshot ID> [<Snapshot ID> ...]" + help = "Remove a snapshot" + option_list = RemoveCommand.option_list + ( + make_option( + "--user", + dest="user", + default=None, + help="UUID of the owner of the snapshot"), + ) + + @common.convert_api_faults + def handle(self, *args, **options): + if not args: + raise CommandError("Please provide a snapshot ID") + + force = options['force'] + message = "snapshots" if len(args) > 1 else "snapshot" + self.confirm_deletion(force, message, args) + userid = options["user"] + + for snapshot_id in args: + self.stdout.write("\n") + try: + snapshot = util.get_snapshot(userid, snapshot_id) + + snapshots.delete(snapshot) + self.stdout.write("Successfully removed snapshot %s\n" + % snapshot_id) + except CommandError as e: + self.stdout.write("Error -- %s\n" % e.message) diff --git a/snf-cyclades-app/synnefo/volume/management/commands/snapshot-show.py b/snf-cyclades-app/synnefo/volume/management/commands/snapshot-show.py new file mode 100644 index 0000000000000000000000000000000000000000..f6118a07baf9feff9c6f2a7d7e4c3c72433355a8 --- /dev/null +++ b/snf-cyclades-app/synnefo/volume/management/commands/snapshot-show.py @@ -0,0 +1,45 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +from snf_django.management.commands import SynnefoCommand, CommandError + +from synnefo.management import common +from synnefo.plankton.backend import PlanktonBackend +from snf_django.management import utils + + +class Command(SynnefoCommand): + args = "<snapshot_id>" + help = "Display available information about a snapshot" + + @common.convert_api_faults + def handle(self, *args, **options): + + if len(args) != 1: + raise CommandError("Please provide a snapshot ID") + + snapshot_id = args[0] + + try: + with PlanktonBackend(None) as backend: + snapshot = backend.get_snapshot(snapshot_id, + check_permissions=False) + except: + raise CommandError("An error occurred, verify that snapshot and " + "user ID are valid") + + utils.pprint_table(out=self.stdout, table=[snapshot.values()], + headers=snapshot.keys(), vertical=True) diff --git a/snf-cyclades-app/synnefo/volume/management/commands/volume-create.py b/snf-cyclades-app/synnefo/volume/management/commands/volume-create.py new file mode 100644 index 0000000000000000000000000000000000000000..c0630736fc79bb70e78c7345f61f91d23d06f696 --- /dev/null +++ b/snf-cyclades-app/synnefo/volume/management/commands/volume-create.py @@ -0,0 +1,151 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from optparse import make_option + +from snf_django.management.commands import SynnefoCommand, CommandError + +from snf_django.management.utils import parse_bool +from synnefo.management import common, pprint +from synnefo.volume import volumes + +HELP_MSG = """Create a new volume.""" + + +class Command(SynnefoCommand): + help = HELP_MSG + umask = 0o007 + + option_list = SynnefoCommand.option_list + ( + make_option( + "--name", + dest="name", + default=None, + help="Display name of the volume."), + make_option( + "--description", + dest="description", + default=None, + help="Display description of the volume."), + make_option( + "--user", + dest="user_id", + default=None, + help="UUID of the owner of the volume."), + make_option( + "--project-id", + dest="project", + default=None, + help="UUID of the project of the volume."), + make_option( + "-s", "--size", + dest="size", + default=None, + help="Size of the new volume in GB"), + make_option( + "--source", + dest="source", + default=None, + help="Initialize volume with data from the specified source. The" + " source must be of the form <source_type>:<source_uuid>." + " Available source types are 'image' and 'snapshot'."), + make_option( + "--server", + dest="server_id", + default=None, + help="The ID of the server that the volume will be connected to."), + make_option( + "--volume-type", + dest="volume_type_id", + default=None, + help="The ID of the volume's type. If the volume will be attached" + " to a server, the volume's and the server's volume type" + " must match."), + make_option( + "--wait", + dest="wait", + default="True", + choices=["True", "False"], + metavar="True|False", + help="Wait for Ganeti jobs to complete."), + ) + + @common.convert_api_faults + def handle(self, *args, **options): + if args: + raise CommandError("Command doesn't accept any arguments") + + size = options.get("size") + user_id = options.get("user_id") + project_id = options.get("project") + server_id = options.get("server_id") + volume_type_id = options.get("volume_type_id") + wait = parse_bool(options["wait"]) + + display_name = options.get("name", "") + display_description = options.get("description", "") + + if size is None: + raise CommandError("Please specify the size of the volume") + + if server_id is None: + raise CommandError("Please specify the server to attach the" + " volume.") + + vm = common.get_resource("server", server_id, for_update=True) + + if user_id is None: + user_id = vm.userid + + if volume_type_id is not None: + vtype = common.get_resource("volume-type", volume_type_id) + else: + vtype = vm.flavor.volume_type + + if project_id is None: + project_id = vm.project + + source_image_id = source_volume_id = source_snapshot_id = None + source = options.get("source") + if source is not None: + try: + source_type, source_uuid = source.split(":", 1) + except (ValueError, TypeError): + raise CommandError("Invalid '--source' option. Value must be" + " of the form <source_type>:<source_uuid>") + if source_type == "image": + source_image_id = source_uuid + elif source_type == "snapshot": + source_snapshot_id = source_uuid + else: + raise CommandError("Unknown volume source type '%s'" + % source_type) + + volume = volumes.create(user_id, size, server_id, + name=display_name, + description=display_description, + source_image_id=source_image_id, + source_snapshot_id=source_snapshot_id, + source_volume_id=source_volume_id, + volume_type_id=vtype.id, + metadata={}, project=project_id) + + self.stdout.write("Created volume '%s' in DB:\n" % volume.id) + + pprint.pprint_volume(volume, stdout=self.stdout) + self.stdout.write("\n") + if volume.machine is not None: + volume.machine.task_job_id = volume.backendjobid + common.wait_server_task(volume.machine, wait, stdout=self.stdout) diff --git a/snf-cyclades-app/synnefo/volume/management/commands/volume-inspect.py b/snf-cyclades-app/synnefo/volume/management/commands/volume-inspect.py new file mode 100644 index 0000000000000000000000000000000000000000..2213a88983ef49b50fa2ac31c03e1fc3982823ec --- /dev/null +++ b/snf-cyclades-app/synnefo/volume/management/commands/volume-inspect.py @@ -0,0 +1,47 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from optparse import make_option +from snf_django.management.commands import SynnefoCommand, CommandError + +from synnefo.management.common import convert_api_faults +from synnefo.management import pprint, common + + +class Command(SynnefoCommand): + help = "Inspect a Volume on DB and Ganeti" + args = "<volume ID>" + + option_list = SynnefoCommand.option_list + ( + make_option( + '--display-mails', + action='store_true', + dest='displaymail', + default=False, + help="Display both uuid and email"), + ) + + @convert_api_faults + def handle(self, *args, **options): + if len(args) != 1: + raise CommandError("Please provide a volume ID") + + volume = common.get_resource("volume", args[0]) + + pprint.pprint_volume(volume, stdout=self.stdout, + display_mails=options['displaymail']) + self.stdout.write('\n\n') + + pprint.pprint_volume_in_ganeti(volume, stdout=self.stdout) diff --git a/snf-cyclades-app/synnefo/volume/management/commands/volume-list.py b/snf-cyclades-app/synnefo/volume/management/commands/volume-list.py new file mode 100644 index 0000000000000000000000000000000000000000..9024369c945c34452842aecb9406ca8131f4cb99 --- /dev/null +++ b/snf-cyclades-app/synnefo/volume/management/commands/volume-list.py @@ -0,0 +1,55 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +#from optparse import make_option + +from snf_django.management.commands import ListCommand +from synnefo.db.models import Volume +from synnefo.settings import (CYCLADES_SERVICE_TOKEN as ASTAKOS_TOKEN, + ASTAKOS_AUTH_URL) +from logging import getLogger +log = getLogger(__name__) + + +class Command(ListCommand): + help = "List Volumes" + + option_list = ListCommand.option_list + + object_class = Volume + deleted_field = "deleted" + user_uuid_field = "userid" + astakos_auth_url = ASTAKOS_AUTH_URL + astakos_token = ASTAKOS_TOKEN + select_related = ["volume_type"] + + FIELDS = { + "id": ("id", "ID of the server"), + "name": ("name", "Name of the server"), + "user.uuid": ("userid", "The UUID of the server's owner"), + "size": ("size", "The size of the volume (GB)"), + "server_id": ("machine_id", "The UUID of the server that the volume" + " is currently attached"), + "source": ("source", "The source of the volume"), + "status": ("status", "The status of the volume"), + "created": ("created", "The date the server was created"), + "deleted": ("deleted", "Whether the server is deleted or not"), + "volume_type": ("volume_type_id", "ID of volume's type"), + "disk_template": ("volume_type.disk_template", + "The disk template of the volume") + } + + fields = ["id", "user.uuid", "size", "status", "source", "disk_template", + "volume_type", "server_id"] diff --git a/snf-cyclades-app/synnefo/volume/management/commands/volume-modify.py b/snf-cyclades-app/synnefo/volume/management/commands/volume-modify.py new file mode 100644 index 0000000000000000000000000000000000000000..5aa8955b25bd9c786068e1aa8e7a1b82e15f1da2 --- /dev/null +++ b/snf-cyclades-app/synnefo/volume/management/commands/volume-modify.py @@ -0,0 +1,65 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from optparse import make_option +from snf_django.management.commands import SynnefoCommand, CommandError + +from snf_django.management.utils import parse_bool +from synnefo.management.common import convert_api_faults +from synnefo.management import pprint, common +from synnefo.volume import volumes + + +class Command(SynnefoCommand): + help = "Modify a volume" + args = "<volume ID>" + + option_list = SynnefoCommand.option_list + ( + make_option( + '--name', + dest='name', + help="Modify a volume's display name"), + make_option( + '--description', + dest='description', + help="Modify a volume's display description"), + make_option( + '--delete-on-termination', + dest='delete_on_termination', + default="True", + choices=["True", "False"], + metavar="True|False", + help="Set whether volume will be preserved when the server" + " the volume is attached will be deleted"), + ) + + @convert_api_faults + def handle(self, *args, **options): + if len(args) != 1: + raise CommandError("Please provide a volume ID") + + volume = common.get_resource("volume", args[0], for_update=True) + + name = options.get("name") + description = options.get("description") + delete_on_termination = options.get("delete_on_termination") + if delete_on_termination is not None: + delete_on_termination = parse_bool(delete_on_termination) + + volume = volumes.update(volume, name, description, + delete_on_termination) + + pprint.pprint_volume(volume, stdout=self.stdout) + self.stdout.write('\n\n') diff --git a/snf-cyclades-app/synnefo/volume/management/commands/volume-remove.py b/snf-cyclades-app/synnefo/volume/management/commands/volume-remove.py new file mode 100644 index 0000000000000000000000000000000000000000..917244bc160cd57868ce610c62a00e824aa918e7 --- /dev/null +++ b/snf-cyclades-app/synnefo/volume/management/commands/volume-remove.py @@ -0,0 +1,64 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +from optparse import make_option +from django.core.management.base import CommandError +from synnefo.volume import volumes +from synnefo.management import common +from snf_django.management.utils import parse_bool +from snf_django.management.commands import RemoveCommand + + +class Command(RemoveCommand): + can_import_settings = True + args = "<Volume ID> [<Volume ID> ...]" + help = "Remove a volume from the Database and from the VM attached to" + option_list = RemoveCommand.option_list + ( + make_option( + "--wait", + dest="wait", + default="True", + choices=["True", "False"], + metavar="True|False", + help="Wait for Ganeti jobs to complete."), + ) + + @common.convert_api_faults + def handle(self, *args, **options): + if not args: + raise CommandError("Please provide a volume ID") + + force = options['force'] + message = "volumes" if len(args) > 1 else "volume" + self.confirm_deletion(force, message, args) + + for volume_id in args: + self.stdout.write("\n") + try: + volume = common.get_resource("volume", volume_id, + for_update=True) + volumes.delete(volume) + + wait = parse_bool(options["wait"]) + if volume.machine is not None: + volume.machine.task_job_id = volume.backendjobid + common.wait_server_task(volume.machine, wait, + stdout=self.stdout) + else: + self.stdout.write("Successfully removed volume %s\n" + % volume) + except CommandError as e: + self.stdout.write("Error -- %s\n" % e.message) diff --git a/snf-cyclades-app/synnefo/volume/management/commands/volume-show.py b/snf-cyclades-app/synnefo/volume/management/commands/volume-show.py new file mode 100644 index 0000000000000000000000000000000000000000..1930f97acd666fa458fb86f1aa1b187079b4b75e --- /dev/null +++ b/snf-cyclades-app/synnefo/volume/management/commands/volume-show.py @@ -0,0 +1,45 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from optparse import make_option +from snf_django.management.commands import SynnefoCommand, CommandError + +from synnefo.management.common import convert_api_faults +from synnefo.management import pprint, common + + +class Command(SynnefoCommand): + help = "Show Volume information" + args = "<volume ID>" + + option_list = SynnefoCommand.option_list + ( + make_option( + '--display-mails', + action='store_true', + dest='displaymail', + default=False, + help="Display both uuid and email"), + ) + + @convert_api_faults + def handle(self, *args, **options): + if len(args) != 1: + raise CommandError("Please provide a volume ID") + + volume = common.get_resource("volume", args[0]) + + pprint.pprint_volume(volume, stdout=self.stdout, + display_mails=options["displaymail"]) + self.stdout.write('\n\n') diff --git a/snf-cyclades-app/synnefo/volume/management/commands/volume-type-create.py b/snf-cyclades-app/synnefo/volume/management/commands/volume-type-create.py new file mode 100644 index 0000000000000000000000000000000000000000..98705138b3b1c9135e7256b101f0208f7acd310f --- /dev/null +++ b/snf-cyclades-app/synnefo/volume/management/commands/volume-type-create.py @@ -0,0 +1,71 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from optparse import make_option + +from django.db import IntegrityError +from snf_django.management.commands import SynnefoCommand, CommandError + +from synnefo.management import pprint, common +from synnefo.db.models import VolumeType + +HELP_MSG = """Create a new Volume Type.""" + + +class Command(SynnefoCommand): + help = HELP_MSG + + option_list = SynnefoCommand.option_list + ( + make_option( + "--name", + dest="name", + default=None, + help="The display name of the volume type."), + make_option( + "--disk-template", + dest="disk_template", + default=None, + help="The disk template of the volume type"), + ) + + @common.convert_api_faults + def handle(self, *args, **options): + if args: + raise CommandError("Command doesn't accept any arguments") + + name = options.get("name") + disk_template = options.get("disk_template") + + if name is None: + raise CommandError("Please specify the name of the volume type.") + if disk_template is None: + raise CommandError("Please specify the disk template of the volume" + " type.") + if len(name) > VolumeType.NAME_LENGTH: + raise CommandError("Name of the volume type can't be more than %s" + " characters." % VolumeType.NAME_LENGTH) + if len(disk_template) > VolumeType.DISK_TEMPLATE_LENGTH: + raise CommandError("Disk template of the volume type can't be more" + " than %s characters." % + VolumeType.DISK_TEMPLATE_LENGTH) + try: + vtype = VolumeType.objects.create(name=name, + disk_template=disk_template) + except IntegrityError as e: + raise CommandError("Failed to create volume type: %s" % e) + + self.stdout.write("Created volume Type '%s' in DB:\n" % vtype.id) + + pprint.pprint_volume_type(vtype, stdout=self.stdout) diff --git a/snf-cyclades-app/synnefo/volume/management/commands/volume-type-list.py b/snf-cyclades-app/synnefo/volume/management/commands/volume-type-list.py new file mode 100644 index 0000000000000000000000000000000000000000..9d58fb25152729988263f55b744b9b155a712eed --- /dev/null +++ b/snf-cyclades-app/synnefo/volume/management/commands/volume-type-list.py @@ -0,0 +1,53 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +#from optparse import make_option + +from snf_django.management.commands import ListCommand +from synnefo.db.models import VolumeType +from logging import getLogger +log = getLogger(__name__) + + +def get_flavors(vtype): + return vtype.flavors.filter(deleted=False).count() + + +def get_volumes(vtype): + return vtype.volumes.filter(deleted=False).count() + + +def get_servers(vtype): + return vtype.servers.filter(deleted=False).count() + + +class Command(ListCommand): + help = "List Volume Types" + + option_list = ListCommand.option_list + + object_class = VolumeType + deleted_field = "deleted" + + FIELDS = { + "id": ("id", "ID"), + "name": ("name", "Name"), + "disk_template": ("disk_template", "Disk template"), + "flavors": (get_flavors, "Number of flavors using this volume type"), + "volumes": (get_volumes, "Number of volumes using this volume type"), + "deleted": ("deleted", "Whether volume type is deleted or not"), + } + + fields = ["id", "name", "disk_template", "flavors", "volumes", "deleted"] diff --git a/snf-cyclades-app/synnefo/volume/management/commands/volume-type-modify.py b/snf-cyclades-app/synnefo/volume/management/commands/volume-type-modify.py new file mode 100644 index 0000000000000000000000000000000000000000..1d942e181eec2a4fc66a361cbfe169bdffeee874 --- /dev/null +++ b/snf-cyclades-app/synnefo/volume/management/commands/volume-type-modify.py @@ -0,0 +1,61 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from optparse import make_option + +from django.core.management.base import CommandError +from synnefo.db import transaction + +from snf_django.management.commands import SynnefoCommand +from synnefo.management.common import get_resource +from snf_django.management.utils import parse_bool + + +from logging import getLogger +log = getLogger(__name__) + + +class Command(SynnefoCommand): + args = "<volume_type_id>" + help = "Modify a Volume Type" + + option_list = SynnefoCommand.option_list + ( + make_option( + "--deleted", + dest="deleted", + metavar="True|False", + choices=["True", "False"], + default=None, + help="Mark/unmark a volume type as deleted. Deleted volume types" + " cannot be used for creation of new volumes. All related" + " flavors will also be updated."), + ) + + @transaction.commit_on_success + def handle(self, *args, **options): + if len(args) != 1: + raise CommandError("Please provide a volume type ID") + + vtype = get_resource("volume-type", args[0], for_update=True) + + deleted = options['deleted'] + + if deleted: + deleted = parse_bool(deleted) + self.stdout.write("Marking volume type '%s' and all related" + " flavors as deleted=%s\n" % (vtype.id, deleted)) + vtype.deleted = deleted + vtype.save() + vtype.flavors.update(deleted=deleted) diff --git a/snf-cyclades-app/synnefo/volume/management/commands/volume-type-show.py b/snf-cyclades-app/synnefo/volume/management/commands/volume-type-show.py new file mode 100644 index 0000000000000000000000000000000000000000..ded090fd738235bb18d1ee6c024e7e00c8427f09 --- /dev/null +++ b/snf-cyclades-app/synnefo/volume/management/commands/volume-type-show.py @@ -0,0 +1,33 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from snf_django.management.commands import SynnefoCommand, CommandError + +from synnefo.management.common import convert_api_faults +from synnefo.management import pprint, common + + +class Command(SynnefoCommand): + help = "Show Volume Type information" + args = "<VolumeType ID>" + + @convert_api_faults + def handle(self, *args, **options): + if len(args) != 1: + raise CommandError("Please provide a volume ID") + + volume_type = common.get_resource("volume-type", args[0]) + + pprint.pprint_volume_type(volume_type, stdout=self.stdout) diff --git a/snf-cyclades-app/synnefo/volume/models.py b/snf-cyclades-app/synnefo/volume/models.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/snf-cyclades-app/synnefo/volume/snapshots.py b/snf-cyclades-app/synnefo/volume/snapshots.py new file mode 100644 index 0000000000000000000000000000000000000000..6287cbd7eac99cb2adb9065262afc9bfd33de15d --- /dev/null +++ b/snf-cyclades-app/synnefo/volume/snapshots.py @@ -0,0 +1,168 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import logging +import simplejson as json +from synnefo.db import transaction +from snf_django.lib.api import faults +from synnefo.plankton.backend import PlanktonBackend, OBJECT_ERROR +from synnefo.logic import backend +from synnefo.volume import util +from synnefo.util import units + +log = logging.getLogger(__name__) + + +@transaction.commit_on_success +def create(user_id, volume, name, description, metadata, force=False): + """Create a snapshot from a given volume + + Create a snapshot from a given volume. The snapshot is first created as + a file in Pithos, with specified metadata to indicate that it is a + snapshot. Then a job is sent to Ganeti backend to create the actual + snapshot of the volume. + + Snapshots are only supported for volumes of ext_ disk template. Also, + the volume must be attached to some server. + + """ + + if name is None: + raise faults.BadRequest("Snapshot 'name' is required") + + # Check that taking a snapshot is feasible + if volume.machine is None: + raise faults.BadRequest("Cannot snapshot a detached volume!") + if volume.status not in ["AVAILABLE", "IN_USE"]: + raise faults.BadRequest("Cannot create snapshot while volume is in" + " '%s' status" % volume.status) + + volume_type = volume.volume_type + if not volume_type.disk_template.startswith("ext_"): + msg = ("Cannot take a snapshot from a volume with volume type '%s' and" + " '%s' disk template" % + (volume_type.id, volume_type.disk_template)) + raise faults.BadRequest(msg) + + # Increase the snapshot counter of the volume that is used in order to + # generate unique snapshot names + volume.snapshot_counter += 1 + volume.save() + transaction.commit() + + snapshot_metadata = { + "name": name, + "disk_format": "diskdump", + "container_format": "bare", + # Snapshot specific + "description": description, + "volume_id": volume.id, + } + + # Snapshots are used as images. We set the most important properties + # that are being used for images. We set 'EXCLUDE_ALL_TASKS' to bypass + # image customization. Also, we get some basic metadata for the volume from + # the server that the volume is attached + metadata.update({"exclude_all_tasks": "yes", + "description": description}) + if volume.index == 0: + # Copy the metadata of the VM into the image properties only when the + # volume is the root volume of the VM. + vm_metadata = dict(volume.machine.metadata + .filter(meta_key__in=["OS", "users"]) + .values_list("meta_key", + "meta_value")) + metadata.update(vm_metadata) + + snapshot_properties = PlanktonBackend._prefix_properties(metadata) + snapshot_metadata.update(snapshot_properties) + + # Generate a name for the Archipelago mapfile. + mapfile = generate_mapfile_name(volume) + + # Convert size from Gbytes to bytes + size = volume.size << 30 + + with PlanktonBackend(user_id) as b: + try: + snapshot_id = b.register_snapshot(name=name, + mapfile=mapfile, + size=size, + metadata=snapshot_metadata) + except faults.OverLimit: + msg = ("Resource limit exceeded for your account." + " Not enough storage space to create snapshot of" + " %s size." % units.show(size, "bytes", "gb")) + raise faults.OverLimit(msg) + + try: + job_id = backend.snapshot_instance(volume.machine, volume, + snapshot_name=mapfile, + snapshot_id=snapshot_id) + except: + # If failed to enqueue job to Ganeti, mark snapshot as ERROR + b.update_snapshot_state(snapshot_id, OBJECT_ERROR) + raise + + # Store the backend and job id as metadata in the snapshot in order + # to make reconciliation based on the Ganeti job possible. + backend_info = { + "ganeti_job_id": job_id, + "ganeti_backend_id": volume.machine.backend_id + } + metadata = {"backend_info": json.dumps(backend_info)} + b.update_metadata(snapshot_id, metadata) + + snapshot = util.get_snapshot(user_id, snapshot_id) + + return snapshot + + +def generate_mapfile_name(volume): + """Helper function to generate a name for the Archipelago mapfile.""" + # time = isoformat(datetime.datetime.now()) + return "snf-snap-%s-%s" % (volume.id, + volume.snapshot_counter) + + +@transaction.commit_on_success +def delete(snapshot): + """Delete a snapshot. + + Delete a snapshot by deleting the corresponding file from Pithos. + + """ + user_id = snapshot["owner"] + log.info("Deleting snapshot '%s'", snapshot["location"]) + with PlanktonBackend(user_id) as pithos_backend: + pithos_backend.delete_snapshot(snapshot["id"]) + return snapshot + + +def update(snapshot, name=None, description=None): + """Update a snapshot + + Update the name or description of a snapshot. + """ + metadata = {} + if name is not None: + metadata["name"] = name + if description is not None: + metadata["description"] = description + if not metadata: + return + user_id = snapshot["owner"] + with PlanktonBackend(user_id) as b: + return b.update_metadata(snapshot["id"], metadata) diff --git a/snf-cyclades-app/synnefo/volume/tests/__init__.py b/snf-cyclades-app/synnefo/volume/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c4ddcf442d02cdcc7091803c2fc23620f6fe4e1d --- /dev/null +++ b/snf-cyclades-app/synnefo/volume/tests/__init__.py @@ -0,0 +1,3 @@ +#from .api_tests import * +from .api import * +from .volumes import * diff --git a/snf-cyclades-app/synnefo/volume/tests/api.py b/snf-cyclades-app/synnefo/volume/tests/api.py new file mode 100644 index 0000000000000000000000000000000000000000..fb547a3d433c2dbaf6ea8febecf80214a4769ac2 --- /dev/null +++ b/snf-cyclades-app/synnefo/volume/tests/api.py @@ -0,0 +1,398 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import json + +from mock import patch, Mock +from snf_django.utils.testing import BaseAPITest, mocked_quotaholder +from synnefo.db.models_factory import (VolumeFactory, VolumeTypeFactory, + VirtualMachineFactory) +from synnefo.lib.services import get_service_path +from synnefo.cyclades_settings import cyclades_services +from synnefo.lib import join_urls +from copy import deepcopy + +VOLUME_URL = get_service_path(cyclades_services, 'volume', + version='v2.0') +VOLUMES_URL = join_urls(VOLUME_URL, "volumes") + + +@patch("synnefo.logic.rapi_pool.GanetiRapiClient") +class VolumeAPITest(BaseAPITest): + def test_create_volume(self, mrapi): + vm = VirtualMachineFactory( + operstate="ACTIVE", + flavor__volume_type__disk_template="ext_vlmc") + user = vm.userid + _data = {"display_name": "test_vol", + "size": 2, + "server_id": vm.id} + + # Test Success + mrapi().ModifyInstance.return_value = 42 + with mocked_quotaholder(): + r = self.post(VOLUMES_URL, user, + json.dumps({"volume": _data}), "json") + self.assertSuccess(r) + + # Test create without size, name and server + for attr in ["display_name", "size", "server_id"]: + data = deepcopy(_data) + del data["size"] + with mocked_quotaholder(): + r = self.post(VOLUMES_URL, user, + json.dumps({"volume": data}), "json") + self.assertBadRequest(r) + + # Test invalid size + data = deepcopy(_data) + data["size"] = -2 + with mocked_quotaholder(): + r = self.post(VOLUMES_URL, user, + json.dumps({"volume": data}), "json") + self.assertBadRequest(r) + + # Test deleted server or invalid state + data = deepcopy(_data) + vm.deleted = True + vm.save() + with mocked_quotaholder(): + r = self.post(VOLUMES_URL, user, + json.dumps({"volume": data}), "json") + self.assertBadRequest(r) + + vm.deleted = False + vm.operstate = "ERROR" + vm.save() + with mocked_quotaholder(): + r = self.post(VOLUMES_URL, user, + json.dumps({"volume": data}), "json") + self.assertBadRequest(r) + vm.operstate = "ACTIVE" + vm.save() + + # Test volume type different from VM's flavor or invalid vype + data = deepcopy(_data) + for disk_type in ["file", "plain", "drbd", "rbd"]: + vtype = VolumeTypeFactory(disk_template=disk_type) + data["volume_type"] = vtype.id + with mocked_quotaholder(): + r = self.post(VOLUMES_URL, user, + json.dumps({"volume": data}), "json") + self.assertBadRequest(r) + for vtype in [434132421243, "foo"]: + data["volume_type"] = vtype + with mocked_quotaholder(): + r = self.post(VOLUMES_URL, user, + json.dumps({"volume": data}), "json") + self.assertBadRequest(r) + + # Test source for invalid disk template + for disk_type in ["file", "plain", "drbd", "rbd"]: + temp_vm = VirtualMachineFactory( + operstate="ACTIVE", + flavor__volume_type__disk_template=disk_type) + for attr in ["snapshot_id", "imageRef"]: + data = deepcopy(_data) + data["server_id"] = temp_vm.id + data[attr] = "3214231-413242134123-431242" + with mocked_quotaholder(): + r = self.post(VOLUMES_URL, user, + json.dumps({"volume": data}), "json") + self.assertBadRequest(r) + + # Test snapshot and image together + data = deepcopy(_data) + data["snapshot_id"] = "3214231-413242134123-431242" + data["imageRef"] = "3214231-413242134123-431242" + with mocked_quotaholder(): + r = self.post(VOLUMES_URL, user, + json.dumps({"volume": data}), "json") + self.assertBadRequest(r) + + # Test with Snapshot source + + # Test unknwon snapshot + data = deepcopy(_data) + data["snapshot_id"] = "94321904321-432142134214-23142314" + with mocked_quotaholder(): + r = self.post(VOLUMES_URL, user, + json.dumps({"volume": data}), "json") + self.assertBadRequest(r) + + vm.task = None + vm.action = None + vm.save() + # Test success + snapshot = Mock() + snapshot.return_value = {'location': 'pithos://foo', + 'mapfile': '1234', + 'id': 1, + 'name': 'test_image', + 'size': 1024, + 'is_snapshot': True, + 'is_public': True, + 'version': 42, + 'owner': 'user', + 'status': 'AVAILABLE', + 'disk_format': 'diskdump'} + data["snapshot_id"] = 1 + with patch("synnefo.volume.util.get_snapshot", snapshot): + with mocked_quotaholder(): + r = self.post(VOLUMES_URL, user, + json.dumps({"volume": data}), "json") + self.assertSuccess(r) + + # Test with Snapshot source + + # Test unknwon snapshot + data = deepcopy(_data) + data["imageRef"] = "94321904321-432142134214-23142314" + with mocked_quotaholder(): + r = self.post(VOLUMES_URL, user, + json.dumps({"volume": data}), "json") + self.assertBadRequest(r) + + vm.task = None + vm.action = None + vm.save() + data["server_id"] = vm.id + # Test success + image = Mock() + image.return_value = {'location': 'pithos://foo', + 'mapfile': '1234', + 'id': 2, + 'name': 'test_image', + 'size': 1024, + 'is_snapshot': False, + 'is_image': False, + 'is_public': True, + 'owner': 'user', + 'version': 42, + 'status': 'AVAILABLE', + 'disk_format': 'diskdump'} + data["imageRef"] = 2 + with patch("synnefo.api.util.get_image", image): + with mocked_quotaholder(): + r = self.post(VOLUMES_URL, user, + json.dumps({"volume": data}), "json") + self.assertSuccess(r) + + def test_rud(self, mrapi): + vol = VolumeFactory(status="IN_USE") + user = vol.userid + # READ + r = self.get(join_urls(VOLUMES_URL, "detail"), user) + api_vols = json.loads(r.content)["volumes"] + self.assertEqual(len(api_vols), 1) + api_vol = api_vols[0] + self.assertEqual(api_vol["id"], str(vol.id)) + self.assertEqual(api_vol["display_name"], vol.name) + self.assertEqual(api_vol["display_description"], vol.description) + + volume_url = join_urls(VOLUMES_URL, str(vol.id)) + r = self.get(volume_url, user) + self.assertSuccess(r) + + # UPDATE + data = { + "volume": { + "display_name": "lolo", + "display_description": "lala" + } + } + + r = self.put(volume_url, user, json.dumps(data), "json") + self.assertSuccess(r) + api_vol = json.loads(r.content)["volume"] + self.assertEqual(api_vol["display_name"], "lolo") + self.assertEqual(api_vol["display_description"], "lala") + + # DELETE + mrapi().ModifyInstance.return_value = 42 + r = self.delete(volume_url, user) + self.assertSuccess(r) + + +class VolumeMetadataAPITest(BaseAPITest): + def test_volume_metadata(self): + vol = VolumeFactory() + volume_metadata_url = join_urls(join_urls(VOLUMES_URL, str(vol.id)), + "metadata") + # Empty metadata + response = self.get(volume_metadata_url, vol.userid) + self.assertSuccess(response) + metadata = json.loads(response.content)["metadata"] + self.assertEqual(metadata, {}) + + # Create metadata items + meta1 = {"metadata": {"key1": "val1", "\u2601": "\u2602"}} + response = self.post(volume_metadata_url, vol.userid, + json.dumps(meta1), "json") + self.assertSuccess(response) + response = self.get(volume_metadata_url, vol.userid) + self.assertSuccess(response) + metadata = json.loads(response.content) + self.assertEqual(metadata, meta1) + + # Update existing metadata and add new + meta2 = {"metadata": {"\u2601": "unicode_val_2", "key3": "val3"}} + meta_db = {"metadata": {"key1": "val1", + "\u2601": "unicode_val_2", + "key3": "val3"}} + response = self.post(volume_metadata_url, vol.userid, + json.dumps(meta2), "json") + self.assertSuccess(response) + metadata = json.loads(response.content) + self.assertEqual(metadata, meta_db) + response = self.get(volume_metadata_url, vol.userid) + self.assertSuccess(response) + metadata = json.loads(response.content) + self.assertEqual(metadata, meta_db) + # Replace all metadata + meta3 = {"metadata": {"key4": "val4"}} + response = self.put(volume_metadata_url, vol.userid, + json.dumps(meta3), "json") + self.assertSuccess(response) + response = self.get(volume_metadata_url, vol.userid) + self.assertSuccess(response) + metadata = json.loads(response.content) + self.assertEqual(metadata, meta3) + + # Delete metadata key + response = self.delete(join_urls(volume_metadata_url, "key4"), + vol.userid) + self.assertSuccess(response) + response = self.get(volume_metadata_url, vol.userid) + self.assertSuccess(response) + metadata = json.loads(response.content)["metadata"] + self.assertEqual(metadata, {}) + + +VOLUME_TYPES_URL = join_urls(VOLUME_URL, "types/") + + +class VolumeTypeAPITest(BaseAPITest): + def test_list(self): + VolumeTypeFactory(disk_template="drbd", name="drbd1") + VolumeTypeFactory(disk_template="file", name="file1") + VolumeTypeFactory(disk_template="plain", name="deleted", + deleted=True) + response = self.get(VOLUME_TYPES_URL) + self.assertSuccess(response) + api_vtypes = json.loads(response.content)["volume_types"] + self.assertEqual(len(api_vtypes), 2) + self.assertEqual(api_vtypes[0]["SNF:disk_template"], "drbd") + self.assertEqual(api_vtypes[0]["name"], "drbd1") + self.assertEqual(api_vtypes[1]["SNF:disk_template"], "file") + self.assertEqual(api_vtypes[1]["name"], "file1") + + def test_get(self): + vtype1 = VolumeTypeFactory(disk_template="drbd", name="drbd1") + vtype2 = VolumeTypeFactory(disk_template="drbd", name="drbd2") + response = self.get(join_urls(VOLUME_TYPES_URL, str(vtype1.id))) + self.assertSuccess(response) + api_vtype = json.loads(response.content)["volume_type"] + self.assertEqual(api_vtype["SNF:disk_template"], "drbd") + self.assertEqual(api_vtype["name"], "drbd1") + self.assertEqual(api_vtype["deleted"], False) + + vtype2.deleted = True + vtype2.save() + response = self.get(join_urls(VOLUME_TYPES_URL, str(vtype2.id))) + self.assertSuccess(response) + api_vtype = json.loads(response.content)["volume_type"] + self.assertEqual(api_vtype["SNF:disk_template"], "drbd") + self.assertEqual(api_vtype["name"], "drbd1") + self.assertEqual(api_vtype["deleted"], True) + + +SNAPSHOTS_URL = join_urls(VOLUME_URL, "snapshots") + + +@patch("synnefo.plankton.backend.PlanktonBackend") +class SnapshotMetadataAPITest(BaseAPITest): + def test_snapshot_metadata(self, mimage): + snap_id = u"1234-4321-1234" + snap_meta_url = join_urls(join_urls(SNAPSHOTS_URL, snap_id), + "metadata") + mimage().__enter__().get_snapshot.return_value = {"properties": {}} + + # Empty metadata + response = self.get(snap_meta_url, "user") + self.assertSuccess(response) + metadata = json.loads(response.content)["metadata"] + self.assertEqual(metadata, {}) + + # Create metadata items + properties = {"key1": "val1", "\u2601": "\u2602"} + meta = {"metadata": properties} + mimage().__enter__().get_snapshot.return_value = \ + {"properties": properties} + response = self.post(snap_meta_url, "user", + json.dumps(meta), "json") + self.assertSuccess(response) + mimage().__enter__().update_properties.assert_called_with( + snap_id, properties, replace=False) + response = self.get(snap_meta_url, "user") + self.assertSuccess(response) + metadata = json.loads(response.content) + self.assertEqual(metadata, meta) + + # Update existing metadata and add new + properties = {"\u2601": "unicode_val_2", "key3": "val3"} + db_properties = {"key1": "val1", + "\u2601": "unicode_val_2", + "key3": "val3"} + + meta = {"metadata": properties} + meta_db = {"metadata": {"key1": "val1", + "\u2601": "unicode_val_2", + "key3": "val3"}} + mimage().__enter__().get_snapshot.return_value = \ + {"properties": db_properties} + response = self.post(snap_meta_url, "user", + json.dumps(meta), "json") + self.assertSuccess(response) + mimage().__enter__().update_properties.assert_called_with( + snap_id, properties, replace=False) + metadata = json.loads(response.content) + self.assertEqual(metadata, meta_db) + response = self.get(snap_meta_url, "user") + self.assertSuccess(response) + metadata = json.loads(response.content) + self.assertEqual(metadata, meta_db) + + properties = {"key4": "val4"} + meta = {"metadata": properties} + mimage().__enter__().get_snapshot.return_value = \ + {"properties": properties} + # Replace all metadata + response = self.put(snap_meta_url, "user", + json.dumps(meta), "json") + mimage().__enter__().update_properties.assert_called_with( + snap_id, properties, replace=True) + self.assertSuccess(response) + response = self.get(snap_meta_url, "user") + self.assertSuccess(response) + metadata = json.loads(response.content) + self.assertEqual(metadata, meta) + + # Delete metadata key + response = self.delete(join_urls(snap_meta_url, "key4"), + "user") + self.assertSuccess(response) + mimage().__enter__().remove_property.assert_called_with( + snap_id, "key4") diff --git a/snf-cyclades-app/synnefo/volume/tests/volumes.py b/snf-cyclades-app/synnefo/volume/tests/volumes.py new file mode 100644 index 0000000000000000000000000000000000000000..c070cb2de8c3980f7414b96d5f8c340c92ae5af6 --- /dev/null +++ b/snf-cyclades-app/synnefo/volume/tests/volumes.py @@ -0,0 +1,205 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from snf_django.utils.testing import BaseAPITest, mocked_quotaholder +#from synnefo.db.models import Volume +from synnefo.db import models_factory as mf +from synnefo.volume import volumes +from snf_django.lib.api import faults +from django.conf import settings +from mock import patch +from copy import deepcopy + + +@patch("synnefo.logic.rapi_pool.GanetiRapiClient") +class VolumesTest(BaseAPITest): + def setUp(self): + self.userid = "test_user" + self.size = 1 + self.vm = mf.VirtualMachineFactory( + userid=self.userid, + flavor__volume_type__disk_template="ext_archipelago") + self.kwargs = {"user_id": self.userid, + "size": self.size, + "server_id": self.vm.id} + + def test_create(self, mrapi): + # No server id + kwargs = deepcopy(self.kwargs) + kwargs["server_id"] = None + self.assertRaises(faults.BadRequest, + volumes.create, + **kwargs) + + # Invalid server + vm = mf.VirtualMachineFactory(userid="other_user") + kwargs["server_id"] = vm.id + self.assertRaises(faults.BadRequest, + volumes.create, + **kwargs) + + # Invalid size + kwargs = deepcopy(self.kwargs) + max_size = settings.CYCLADES_VOLUME_MAX_SIZE + kwargs["size"] = max_size + 1 + self.assertRaises(faults.BadRequest, + volumes.create, + **kwargs) + + # Create server without source! + mrapi().ModifyInstance.return_value = 42 + with mocked_quotaholder(): + vol = volumes.create(**self.kwargs) + + self.assertEqual(vol.size, self.size) + self.assertEqual(vol.userid, self.userid) + self.assertEqual(vol.name, None) + self.assertEqual(vol.description, None) + self.assertEqual(vol.source_snapshot_id, None) + self.assertEqual(vol.source, None) + self.assertEqual(vol.origin, None) + self.assertEqual(vol.source_volume_id, None) + self.assertEqual(vol.source_image_id, None) + self.assertEqual(vol.machine, self.vm) + self.assertEqual(vol.volume_type, self.vm.flavor.volume_type) + + name, args, kwargs = mrapi().ModifyInstance.mock_calls[0] + self.assertEqual(kwargs["instance"], self.vm.backend_vm_id) + disk_info = kwargs["disks"][0][2] + self.assertEqual(disk_info["size"], self.size << 10) + self.assertEqual(disk_info["name"], vol.backend_volume_uuid) + self.assertFalse("origin" in disk_info) + + def test_create_from_volume(self, mrapi): + svol = mf.VolumeFactory(userid=self.userid, status="IN_USE", + volume_type=self.vm.flavor.volume_type) + kwargs = deepcopy(self.kwargs) + kwargs["size"] = svol.size + self.assertRaises(faults.BadRequest, + volumes.create, + source_volume_id=svol.id, + **self.kwargs) + # # Check permissions + # svol = mf.VolumeFactory(userid="other_user", + # volume_type=self.vm.flavor.volume_type) + # self.assertRaises(faults.BadRequest, + # volumes.create, + # source_volume_id=svol.id, + # **self.kwargs) + # # Invalid volume status + # svol = mf.VolumeFactory(userid=self.userid, status="CREATING", + # volume_type=self.vm.flavor.volume_type) + # self.assertRaises(faults.BadRequest, + # volumes.create, + # source_volume_id=svol.id, + # **self.kwargs) + # svol = mf.VolumeFactory(userid=self.userid, status="AVAILABLE", + # volume_type=self.vm.flavor.volume_type) + # self.assertRaises(faults.BadRequest, + # volumes.create, + # source_volume_id=svol.id, + # **self.kwargs) + + # svol.status = "IN_USE" + # svol.save() + # mrapi().ModifyInstance.return_value = 42 + # kwargs = deepcopy(self.kwargs) + # kwargs["size"] = svol.size + # with mocked_quotaholder(): + # vol = volumes.create(source_volume_id=svol.id, **kwargs) + + # self.assertEqual(vol.size, svol.size) + # self.assertEqual(vol.userid, self.userid) + # self.assertEqual(vol.name, None) + # self.assertEqual(vol.description, None) + # self.assertEqual(vol.source, "volume:%s" % svol.id) + # self.assertEqual(vol.origin, svol.backend_volume_uuid) + # self.assertEqual(vol.volume_type, svol.volume_type) + + # name, args, kwargs = mrapi().ModifyInstance.mock_calls[0] + # self.assertEqual(kwargs["instance"], self.vm.backend_vm_id) + # disk_info = kwargs["disks"][0][2] + # self.assertEqual(disk_info["size"], svol.size << 10) + # self.assertEqual(disk_info["name"], vol.backend_volume_uuid) + # self.assertEqual(disk_info["origin"], svol.backend_volume_uuid) + + @patch("synnefo.plankton.backend.PlanktonBackend") + def test_create_from_snapshot(self, mimage, mrapi): + # Wrong source + mimage().__enter__().get_snapshot.side_effect = faults.ItemNotFound + self.assertRaises(faults.BadRequest, + volumes.create, + source_snapshot_id=421, + **self.kwargs) + + mimage().__enter__().get_snapshot.side_effect = None + mimage().__enter__().get_snapshot.return_value = { + 'location': 'pithos://foo', + 'mapfile': 'snf-snapshot-43', + 'id': 12, + 'name': "test_image", + 'version': 42, + 'size': 1242, + 'disk_format': 'diskdump', + 'status': 'AVAILABLE', + 'properties': {'source_volume': 42}} + + mrapi().ModifyInstance.return_value = 42 + with mocked_quotaholder(): + vol = volumes.create(source_snapshot_id=12, **self.kwargs) + + self.assertEqual(vol.size, self.size) + self.assertEqual(vol.userid, self.userid) + self.assertEqual(vol.name, None) + self.assertEqual(vol.description, None) + self.assertEqual(int(vol.source_snapshot_id), 12) + self.assertEqual(vol.source_volume_id, None) + self.assertEqual(vol.source_image_id, None) + self.assertEqual(vol.origin, "snf-snapshot-43") + + name, args, kwargs = mrapi().ModifyInstance.mock_calls[0] + self.assertEqual(kwargs["instance"], self.vm.backend_vm_id) + disk_info = kwargs["disks"][0][2] + self.assertEqual(disk_info["size"], self.size << 10) + self.assertEqual(disk_info["name"], vol.backend_volume_uuid) + self.assertEqual(disk_info["origin"], "snf-snapshot-43") + + def test_delete(self, mrapi): + # We can not deleted detached volumes + vol = mf.VolumeFactory(machine=None, status="AVAILABLE") + self.assertRaises(faults.BadRequest, + volumes.delete, + vol) + + vm = mf.VirtualMachineFactory(userid=vol.userid) + # Also we cannot delete root volume + vol.index = 0 + vol.machine = vm + vol.status = "IN_USE" + vol.save() + self.assertRaises(faults.BadRequest, + volumes.delete, + vol) + + # We can delete everything else + vol.index = 1 + mrapi().ModifyInstance.return_value = 42 + with mocked_quotaholder(): + volumes.delete(vol) + self.assertEqual(vol.backendjobid, 42) + args, kwargs = mrapi().ModifyInstance.call_args + self.assertEqual(kwargs["instance"], vm.backend_vm_id) + self.assertEqual(kwargs["disks"][0], ("remove", + vol.backend_volume_uuid, {})) diff --git a/snf-cyclades-app/synnefo/volume/urls.py b/snf-cyclades-app/synnefo/volume/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..21db97f4074c1e8c466fe2083325b95c9452895b --- /dev/null +++ b/snf-cyclades-app/synnefo/volume/urls.py @@ -0,0 +1,154 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from django.conf import settings +from django.conf.urls.defaults import patterns, include +from django.http import HttpResponseNotAllowed +from snf_django.lib import api +from synnefo.volume import views +from snf_django.lib.api import faults, utils + + +def volume_demux(request): + if request.method == 'GET': + return views.list_volumes(request) + elif request.method == 'POST': + return views.create_volume(request) + else: + return HttpResponseNotAllowed(['GET', 'POST']) + + +def volume_item_demux(request, volume_id): + if request.method == "GET": + return views.get_volume(request, volume_id) + elif request.method == "PUT": + return views.update_volume(request, volume_id) + elif request.method == "DELETE": + return views.delete_volume(request, volume_id) + else: + return HttpResponseNotAllowed(["GET", "PUT", "DELETE"]) + + +def volume_metadata_demux(request, volume_id): + if request.method == 'GET': + return views.list_volume_metadata(request, volume_id) + elif request.method == 'POST': + return views.update_volume_metadata(request, volume_id, reset=False) + elif request.method == 'PUT': + return views.update_volume_metadata(request, volume_id, reset=True) + else: + return HttpResponseNotAllowed(['GET', 'POST', 'PUT']) + + +def volume_metadata_item_demux(request, volume_id, key): + if request.method == 'DELETE': + return views.delete_volume_metadata_item(request, volume_id, key) + else: + return HttpResponseNotAllowed(['DELETE']) + + +VOLUME_ACTIONS = { + "reassign": views.reassign_volume, + } + + +def volume_action_demux(request, volume_id): + req = utils.get_json_body(request) + + if not isinstance(req, dict) and len(req) != 1: + raise faults.BadRequest("Malformed request") + + action = req.keys()[0] + if not isinstance(action, basestring): + raise faults.BadRequest("Malformed Request. Invalid action.") + + try: + action_func = VOLUME_ACTIONS[action] + except KeyError: + raise faults.BadRequest("Action %s not supported" % action) + action_args = utils.get_attribute(req, action, required=True, + attr_type=dict) + + return action_func(request, volume_id, action_args) + + +def snapshot_demux(request): + if request.method == 'GET': + return views.list_snapshots(request) + elif request.method == 'POST': + return views.create_snapshot(request) + else: + return HttpResponseNotAllowed(['GET', 'POST']) + + +def snapshot_item_demux(request, snapshot_id): + if request.method == "GET": + return views.get_snapshot(request, snapshot_id) + elif request.method == "PUT": + return views.update_snapshot(request, snapshot_id) + elif request.method == "DELETE": + return views.delete_snapshot(request, snapshot_id) + else: + return HttpResponseNotAllowed(["GET", "PUT", "DELETE"]) + + +def snapshot_metadata_demux(request, snapshot_id): + if request.method == 'GET': + return views.list_snapshot_metadata(request, snapshot_id) + elif request.method == 'POST': + return views.update_snapshot_metadata(request, snapshot_id, + reset=False) + elif request.method == 'PUT': + return views.update_snapshot_metadata(request, snapshot_id, reset=True) + else: + return HttpResponseNotAllowed(['GET', 'POST', 'PUT']) + + +def snapshot_metadata_item_demux(request, snapshot_id, key): + if request.method == 'DELETE': + return views.delete_snapshot_metadata_item(request, snapshot_id, key) + else: + return HttpResponseNotAllowed(['DELETE']) + + +volume_v2_patterns = patterns( + '', + (r'^volumes/?(?:.json)?$', volume_demux), + (r'^volumes/detail(?:.json)?$', views.list_volumes, {'detail': True}), + (r'^volumes/(\d+)(?:.json)?$', volume_item_demux), + (r'^volumes/(\d+)/metadata/?(?:.json)?$', volume_metadata_demux), + (r'^volumes/(\d+)/metadata/(.+)(?:.json)?$', volume_metadata_item_demux), + (r'^volumes/(\d+)/action(?:.json|.xml)?$', volume_action_demux), + (r'^types/?(?:.json)?$', views.list_volume_types), + (r'^types/(\d+)(?:.json)?$', views.get_volume_type), +) + +if settings.CYCLADES_SNAPSHOTS_ENABLED: + volume_v2_patterns += patterns( + '', + (r'^snapshots/?(?:.json)?$', snapshot_demux), + (r'^snapshots/detail$', views.list_snapshots, {'detail': True}), + (r'^snapshots/([\w-]+)(?:.json)?$', snapshot_item_demux), + (r'^snapshots/([\w-]+)/metadata/?(?:.json)?$', + snapshot_metadata_demux), + (r'^snapshots/([\w-]+)/metadata/(.+)(?:.json)?$', + snapshot_metadata_item_demux), + ) + +urlpatterns = patterns( + '', + (r'^v2.0/', include(volume_v2_patterns)), + (r'^.*', api.api_endpoint_not_found) +) diff --git a/snf-cyclades-app/synnefo/volume/util.py b/snf-cyclades-app/synnefo/volume/util.py new file mode 100644 index 0000000000000000000000000000000000000000..7eb07dcc1fbb9280e410aedfd84db4d6626323a9 --- /dev/null +++ b/snf-cyclades-app/synnefo/volume/util.py @@ -0,0 +1,116 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from synnefo.db import models +from snf_django.lib.api import faults +from synnefo.api.util import get_image_dict, get_vm +from synnefo.plankton import backend +from synnefo.cyclades_settings import cyclades_services, BASE_HOST +from synnefo.lib import join_urls +from synnefo.lib.services import get_service_path + + +def get_volume(user_id, volume_id, for_update=False, + non_deleted=False, + exception=faults.ItemNotFound): + volumes = models.Volume.objects + if for_update: + volumes = volumes.select_for_update() + try: + volume_id = int(volume_id) + except (TypeError, ValueError): + raise faults.BadRequest("Invalid volume id: %s" % volume_id) + try: + volume = volumes.get(id=volume_id, userid=user_id) + if non_deleted and volume.deleted: + raise faults.BadRequest("Volume '%s' has been deleted." + % volume_id) + return volume + except models.Volume.DoesNotExist: + raise exception("Volume %s not found" % volume_id) + + +def get_volume_type(volume_type_id, for_update=False, include_deleted=False, + exception=faults.ItemNotFound): + vtypes = models.VolumeType.objects + if not include_deleted: + vtypes = vtypes.filter(deleted=False) + if for_update: + vtypes = vtypes.select_for_update() + try: + vtype_id = int(volume_type_id) + except (TypeError, ValueError): + raise faults.BadRequest("Invalid volume id: %s" % volume_type_id) + try: + return vtypes.get(id=vtype_id) + except models.VolumeType.DoesNotExist: + raise exception("Volume type %s not found" % vtype_id) + + +def get_snapshot(user_id, snapshot_id, exception=faults.ItemNotFound): + try: + with backend.PlanktonBackend(user_id) as b: + return b.get_snapshot(snapshot_id) + except faults.ItemNotFound: + raise exception("Snapshot %s not found" % snapshot_id) + + +def get_image(user_id, image_id, exception=faults.ItemNotFound): + try: + return get_image_dict(image_id, user_id) + except faults.ItemNotFound: + raise exception("Image %s not found" % image_id) + + +def get_server(user_id, server_id, for_update=False, non_deleted=False, + exception=faults.ItemNotFound): + try: + server_id = int(server_id) + except (TypeError, ValueError): + raise faults.BadRequest("Invalid server id: %s" % server_id) + try: + return get_vm(server_id, user_id, for_update=for_update, + non_deleted=non_deleted, non_suspended=True) + except faults.ItemNotFound: + raise exception("Server %s not found" % server_id) + + +VOLUME_URL = \ + join_urls(BASE_HOST, + get_service_path(cyclades_services, "volume", version="v2.0")) + +VOLUMES_URL = join_urls(VOLUME_URL, "volumes/") +SNAPSHOTS_URL = join_urls(VOLUME_URL, "snapshots/") + + +def volume_to_links(volume_id): + href = join_urls(VOLUMES_URL, str(volume_id)) + return [{"rel": rel, "href": href} for rel in ("self", "bookmark")] + + +def snapshot_to_links(snapshot_id): + href = join_urls(SNAPSHOTS_URL, str(snapshot_id)) + return [{"rel": rel, "href": href} for rel in ("self", "bookmark")] + + +def update_snapshot_state(snapshot_id, user_id, state): + """Update the state of a snapshot in Pithos. + + Use PithosBackend in order to update the state of the snapshots in + Pithos DB. + + """ + with backend.PlanktonBackend(user_id) as b: + return b.update_snapshot_state(snapshot_id, state=state) diff --git a/snf-cyclades-app/synnefo/volume/views.py b/snf-cyclades-app/synnefo/volume/views.py new file mode 100644 index 0000000000000000000000000000000000000000..d5bc6c70f822e49b46543b134311857c6b327376 --- /dev/null +++ b/snf-cyclades-app/synnefo/volume/views.py @@ -0,0 +1,501 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from itertools import ifilter +from logging import getLogger +from synnefo.db import transaction +from django.http import HttpResponse +from django.utils import simplejson as json +from django.utils.encoding import smart_unicode +from django.conf import settings + +from dateutil.parser import parse as date_parse + +from snf_django.lib import api +from snf_django.lib.api import faults, utils + +from synnefo.volume import volumes, snapshots, util +from synnefo.db.models import Volume, VolumeType, VolumeMetadata +from synnefo.plankton import backend +from synnefo.plankton.backend import (OBJECT_AVAILABLE, OBJECT_UNAVAILABLE, + OBJECT_ERROR) +from synnefo.logic.utils import check_name_length + +log = getLogger('synnefo.volume') + + +def display_null_field(field): + if field is None: + return None + else: + return smart_unicode(field) + + +def volume_to_dict(volume, detail=True): + data = { + "id": str(volume.id), + "display_name": display_null_field(volume.name), + "links": util.volume_to_links(volume.id), + } + if detail: + details = { + "status": volume.status.lower(), + "size": volume.size, + "display_description": volume.description, + "created_at": utils.isoformat(volume.created), + "metadata": dict((m.key, m.value) for m in volume.metadata.all()), + "snapshot_id": display_null_field(volume.source_snapshot_id), + "source_volid": display_null_field(volume.source_volume_id), + "image_id": display_null_field(volume.source_image_id), + "attachments": get_volume_attachments(volume), + "volume_type": volume.volume_type_id, + "deleted": volume.deleted, + "delete_on_termination": volume.delete_on_termination, + "user_id": volume.userid, + "tenant_id": volume.project, + # "availabilit_zone": None, + # "bootable": None, + # "os-vol-tenant-attr:tenant_id": None, + # "os-vol-host-attr:host": None, + # "os-vol-mig-status-attr:name_id": None, + # "os-vol-mig-status-attr:migstat": None, + } + data.update(details) + return data + + +def get_volume_attachments(volume): + if volume.machine_id is None: + return [] + else: + return [{"server_id": volume.machine_id, + "volume_id": volume.id, + "device_index": volume.index}] + + +@api.api_method(http_method="POST", user_required=True, logger=log) +def create_volume(request): + """Create a new Volume.""" + + req = utils.get_json_body(request) + log.debug("create_volume %s", req) + user_id = request.user_uniq + + vol_dict = utils.get_attribute(req, "volume", attr_type=dict, + required=True) + name = utils.get_attribute(vol_dict, "display_name", + attr_type=basestring, required=True) + + # Get and validate 'size' parameter + size = utils.get_attribute(vol_dict, "size", + attr_type=(basestring, int), required=True) + try: + size = int(size) + if size <= 0: + raise ValueError + except (TypeError, ValueError): + raise faults.BadRequest("Volume 'size' needs to be a positive integer" + " value. '%s' cannot be accepted." % size) + + project = vol_dict.get("project") + if project is None: + project = user_id + + # Optional parameters + volume_type_id = utils.get_attribute(vol_dict, "volume_type", + attr_type=(basestring, int), + required=False) + description = utils.get_attribute(vol_dict, "display_description", + attr_type=basestring, required=False, + default="") + metadata = utils.get_attribute(vol_dict, "metadata", attr_type=dict, + required=False, default={}) + + # Id of the volume to clone from + source_volume_id = utils.get_attribute(vol_dict, "source_volid", + required=False) + + # Id of the snapshot to create the volume from + source_snapshot_id = utils.get_attribute(vol_dict, "snapshot_id", + required=False) + if source_snapshot_id and not settings.CYCLADES_SNAPSHOTS_ENABLED: + raise faults.NotImplemented("Making a clone from a snapshot is not" + " implemented") + + # Reference to an Image stored in Glance + source_image_id = utils.get_attribute(vol_dict, "imageRef", required=False) + + # Get server ID to attach the volume. Since we currently do not supported + # detached volumes, server_id attribute is mandatory. + server_id = utils.get_attribute(vol_dict, "server_id", required=True) + + # Create the volume + volume = volumes.create(user_id=user_id, size=size, name=name, + source_volume_id=source_volume_id, + source_snapshot_id=source_snapshot_id, + source_image_id=source_image_id, + volume_type_id=volume_type_id, + description=description, + metadata=metadata, + server_id=server_id, project=project) + + # Render response + data = json.dumps(dict(volume=volume_to_dict(volume, detail=False))) + return HttpResponse(data, status=202) + + +@api.api_method(http_method="GET", user_required=True, logger=log) +def list_volumes(request, detail=False): + log.debug('list_volumes detail=%s', detail) + volumes = Volume.objects.filter(userid=request.user_uniq)\ + .prefetch_related("metadata")\ + .order_by("id") + + volumes = utils.filter_modified_since(request, objects=volumes) + + volumes = [volume_to_dict(v, detail) for v in volumes] + + data = json.dumps({'volumes': volumes}) + return HttpResponse(data, content_type="application/json", status=200) + + +@api.api_method(http_method="DELETE", user_required=True, logger=log) +def delete_volume(request, volume_id): + log.debug("delete_volume volume_id: %s", volume_id) + + volume = util.get_volume(request.user_uniq, volume_id, for_update=True, + non_deleted=True) + volumes.delete(volume) + + return HttpResponse(status=202) + + +@api.api_method(http_method="GET", user_required=True, logger=log) +def get_volume(request, volume_id): + log.debug('get_volume volume_id: %s', volume_id) + + volume = util.get_volume(request.user_uniq, volume_id, non_deleted=False) + + data = json.dumps({'volume': volume_to_dict(volume, detail=True)}) + return HttpResponse(data, content_type="application/json", status=200) + + +@api.api_method(http_method="PUT", user_required=True, logger=log) +def update_volume(request, volume_id): + req = utils.get_json_body(request) + log.debug('update_volume volume_id: %s, request: %s', volume_id, req) + + volume = util.get_volume(request.user_uniq, volume_id, for_update=True, + non_deleted=True) + + vol_req = utils.get_attribute(req, "volume", attr_type=dict, + required=True) + name = utils.get_attribute(vol_req, "display_name", required=False) + description = utils.get_attribute(vol_req, "display_description", + required=False) + delete_on_termination = utils.get_attribute(vol_req, + "delete_on_termination", + attr_type=bool, + required=False) + + if name is None and description is None and\ + delete_on_termination is None: + raise faults.BadRequest("Nothing to update.") + else: + volume = volumes.update(volume, name, description, + delete_on_termination) + + data = json.dumps({'volume': volume_to_dict(volume, detail=True)}) + return HttpResponse(data, content_type="application/json", status=200) + + +@api.api_method(http_method="GET", user_required=True, logger=log) +def list_volume_metadata(request, volume_id): + log.debug('list_volume_meta volume_id: %s', volume_id) + volume = util.get_volume(request.user_uniq, volume_id, for_update=False, + non_deleted=False) + metadata = volume.metadata.values_list('key', 'value') + data = json.dumps({"metadata": dict(metadata)}) + return HttpResponse(data, content_type="application/json", status=200) + + +@api.api_method(user_required=True, logger=log) +@transaction.commit_on_success +def update_volume_metadata(request, volume_id, reset=False): + req = utils.get_json_body(request) + log.debug('update_volume_meta volume_id: %s, reset: %s request: %s', + volume_id, reset, req) + meta_dict = utils.get_attribute(req, "metadata", required=True, + attr_type=dict) + for key, value in meta_dict.items(): + check_name_length(key, VolumeMetadata.KEY_LENGTH, + "Metadata key is too long.") + check_name_length(value, VolumeMetadata.VALUE_LENGTH, + "Metadata value is too long.") + volume = util.get_volume(request.user_uniq, volume_id, for_update=True, + non_deleted=True) + if reset: + if len(meta_dict) > settings.CYCLADES_VOLUME_MAX_METADATA: + raise faults.BadRequest("Volumes cannot have more than %s metadata" + " items" % + settings.CYCLADES_VOLUME_MAX_METADATA) + + volume.metadata.all().delete() + for key, value in meta_dict.items(): + volume.metadata.create(key=key, value=value) + else: + if len(meta_dict) + len(volume.metadata.all()) - \ + len(volume.metadata.all().filter(key__in=meta_dict.keys())) > \ + settings.CYCLADES_VOLUME_MAX_METADATA: + raise faults.BadRequest("Volumes cannot have more than %s metadata" + " items" % + settings.CYCLADES_VOLUME_MAX_METADATA) + + for key, value in meta_dict.items(): + try: + # Update existing metadata + meta = volume.metadata.get(key=key) + meta.value = value + meta.save() + except VolumeMetadata.DoesNotExist: + # Or create a new one + volume.metadata.create(key=key, value=value) + metadata = volume.metadata.values_list('key', 'value') + data = json.dumps({"metadata": dict(metadata)}) + return HttpResponse(data, content_type="application/json", status=200) + + +@api.api_method(http_method="DELETE", user_required=True, logger=log) +@transaction.commit_on_success +def delete_volume_metadata_item(request, volume_id, key): + log.debug('delete_volume_meta_item volume_id: %s, key: %s', + volume_id, key) + volume = util.get_volume(request.user_uniq, volume_id, for_update=False, + non_deleted=True) + try: + volume.metadata.get(key=key).delete() + except VolumeMetadata.DoesNotExist: + raise faults.BadRequest("Metadata key not found") + return HttpResponse(status=200) + + +@api.api_method(http_method="POST", user_required=True, logger=log) +@transaction.commit_on_success +def reassign_volume(request, volume_id, args): + req = utils.get_json_body(request) + log.debug('reassign_volume volume_id: %s, request: %s', volume_id, req) + project = args.get("project") + if project is None: + raise faults.BadRequest("Missing 'project' attribute.") + volume = util.get_volume(request.user_uniq, volume_id, for_update=True, + non_deleted=True) + volumes.reassign_volume(volume, project) + return HttpResponse(status=200) + + +API_STATUS_FROM_IMAGE_STATUS = { + OBJECT_AVAILABLE: "AVAILABLE", + OBJECT_UNAVAILABLE: "CREATING", + OBJECT_ERROR: "ERROR", + "DELETED": "DELETED"} # Unused status + + +def snapshot_to_dict(snapshot, detail=True): + owner = snapshot['owner'] + status = snapshot.get('status', "unknown").upper() + status = API_STATUS_FROM_IMAGE_STATUS.get(status, "UNKNOWN") + progress = "%s%%" % 100 if status == "AVAILABLE" else 0 + + data = { + "id": snapshot["id"], + "size": int(snapshot["size"]) >> 30, # gigabytes + "display_name": snapshot["name"], + "display_description": snapshot.get("description", ""), + "status": status, + "user_id": owner, + "tenant_id": owner, + "os-extended-snapshot-attribute:progress": progress, + # "os-extended-snapshot-attribute:project_id": project, + "created_at": utils.isoformat(date_parse(snapshot["created_at"])), + "metadata": snapshot.get("metadata", {}), + "volume_id": snapshot.get("volume_id"), + "links": util.snapshot_to_links(snapshot["id"]) + } + return data + + +@api.api_method(http_method="POST", user_required=True, logger=log) +def create_snapshot(request): + """Create a new Snapshot.""" + + req = utils.get_json_body(request) + log.debug("create_snapshot %s", req) + user_id = request.user_uniq + + snap_dict = utils.get_attribute(req, "snapshot", required=True, + attr_type=dict) + volume_id = utils.get_attribute(snap_dict, "volume_id", required=True) + volume = util.get_volume(user_id, volume_id, for_update=True, + non_deleted=True, + exception=faults.BadRequest) + + metadata = utils.get_attribute(snap_dict, "metadata", required=False, + attr_type=dict, default={}) + name = utils.get_attribute(snap_dict, "display_name", required=False, + attr_type=basestring, + default="Snapshot of volume '%s'" % volume_id) + description = utils.get_attribute(snap_dict, "display_description", + required=False, + attr_type=basestring, default="") + + # TODO: What to do with force ? + force = utils.get_attribute(req, "force", required=False, attr_type=bool, + default=False) + + snapshot = snapshots.create(user_id=user_id, volume=volume, name=name, + description=description, metadata=metadata, + force=force) + + # Render response + data = json.dumps(dict(snapshot=snapshot_to_dict(snapshot, detail=False))) + return HttpResponse(data, status=202) + + +@api.api_method(http_method="GET", user_required=True, logger=log) +def list_snapshots(request, detail=False): + log.debug('list_snapshots detail=%s', detail) + since = utils.isoparse(request.GET.get('changes-since')) + with backend.PlanktonBackend(request.user_uniq) as b: + snapshots = b.list_snapshots() + if since: + updated_since = lambda snap:\ + date_parse(snap["updated_at"]) >= since + snapshots = ifilter(updated_since, snapshots) + if not snapshots: + return HttpResponse(status=304) + + snapshots = sorted(snapshots, key=lambda x: x['id']) + snapshots_dict = [snapshot_to_dict(snapshot, detail) + for snapshot in snapshots] + + data = json.dumps(dict(snapshots=snapshots_dict)) + + return HttpResponse(data, status=200) + + +@api.api_method(http_method="DELETE", user_required=True, logger=log) +def delete_snapshot(request, snapshot_id): + log.debug("delete_snapshot snapshot_id: %s", snapshot_id) + + snapshot = util.get_snapshot(request.user_uniq, snapshot_id) + snapshots.delete(snapshot) + + return HttpResponse(status=202) + + +@api.api_method(http_method="GET", user_required=True, logger=log) +def get_snapshot(request, snapshot_id): + log.debug('get_snapshot snapshot_id: %s', snapshot_id) + + snapshot = util.get_snapshot(request.user_uniq, snapshot_id) + data = json.dumps({'snapshot': snapshot_to_dict(snapshot, detail=True)}) + return HttpResponse(data, content_type="application/json", status=200) + + +@api.api_method(http_method="PUT", user_required=True, logger=log) +def update_snapshot(request, snapshot_id): + req = utils.get_json_body(request) + log.debug('update_snapshot snapshot_id: %s, request: %s', snapshot_id, req) + snapshot = util.get_snapshot(request.user_uniq, snapshot_id) + + snap_dict = utils.get_attribute(req, "snapshot", attr_type=dict, + required=True) + new_name = utils.get_attribute(snap_dict, "display_name", required=False, + attr_type=basestring) + new_description = utils.get_attribute(snap_dict, "display_description", + required=False, attr_type=basestring) + + if new_name is None and new_description is None: + raise faults.BadRequest("Nothing to update.") + + snapshot = snapshots.update(snapshot, name=new_name, + description=new_description) + + data = json.dumps({'snapshot': snapshot_to_dict(snapshot, detail=True)}) + return HttpResponse(data, content_type="application/json", status=200) + + +@api.api_method(http_method="GET", user_required=True, logger=log) +def list_snapshot_metadata(request, snapshot_id): + log.debug('list_snapshot_meta snapshot_id: %s', snapshot_id) + snapshot = util.get_snapshot(request.user_uniq, snapshot_id) + metadata = snapshot["properties"] + data = json.dumps({"metadata": dict(metadata)}) + return HttpResponse(data, content_type="application/json", status=200) + + +@api.api_method(user_required=True, logger=log) +@transaction.commit_on_success +def update_snapshot_metadata(request, snapshot_id, reset=False): + req = utils.get_json_body(request) + log.debug('update_snapshot_meta snapshot_id: %s, reset: %s request: %s', + snapshot_id, reset, req) + snapshot = util.get_snapshot(request.user_uniq, snapshot_id) + meta_dict = utils.get_attribute(req, "metadata", required=True, + attr_type=dict) + with backend.PlanktonBackend(request.user_uniq) as b: + b.update_properties(snapshot_id, meta_dict, replace=reset) + snapshot = util.get_snapshot(request.user_uniq, snapshot_id) + metadata = snapshot["properties"] + data = json.dumps({"metadata": dict(metadata)}) + return HttpResponse(data, content_type="application/json", status=200) + + +@api.api_method(http_method="DELETE", user_required=True, logger=log) +@transaction.commit_on_success +def delete_snapshot_metadata_item(request, snapshot_id, key): + log.debug('delete_snapshot_meta_item snapshot_id: %s, key: %s', + snapshot_id, key) + snapshot = util.get_snapshot(request.user_uniq, snapshot_id) + if key in snapshot["properties"]: + with backend.PlanktonBackend(request.user_uniq) as b: + b.remove_property(snapshot_id, key) + return HttpResponse(status=200) + + +def volume_type_to_dict(volume_type): + vtype_info = { + "id": volume_type.id, + "name": volume_type.name, + "deleted": volume_type.deleted, + "SNF:disk_template": volume_type.disk_template} + return vtype_info + + +@api.api_method(http_method="GET", user_required=True, logger=log) +def list_volume_types(request): + log.debug('list_volumes') + vtypes = VolumeType.objects.filter(deleted=False).order_by("id") + vtypes = [volume_type_to_dict(vtype) for vtype in vtypes] + data = json.dumps({'volume_types': vtypes}) + return HttpResponse(data, content_type="application/json", status=200) + + +@api.api_method(http_method="GET", user_required=True, logger=log) +def get_volume_type(request, volume_type_id): + log.debug('get_volume_type volume_type_id: %s', volume_type_id) + volume_type = util.get_volume_type(volume_type_id, include_deleted=True) + data = json.dumps({'volume_type': volume_type_to_dict(volume_type)}) + return HttpResponse(data, content_type="application/json", status=200) diff --git a/snf-cyclades-app/synnefo/volume/volumes.py b/snf-cyclades-app/synnefo/volume/volumes.py new file mode 100644 index 0000000000000000000000000000000000000000..f33829b7b5f76bf489603a1439b93a88122e41eb --- /dev/null +++ b/snf-cyclades-app/synnefo/volume/volumes.py @@ -0,0 +1,294 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import logging + +from synnefo.db import transaction +from django.conf import settings +from snf_django.lib.api import faults +from synnefo.db.models import Volume, VolumeMetadata +from synnefo.volume import util +from synnefo.logic import server_attachments, utils, commands +from synnefo.plankton.backend import OBJECT_AVAILABLE +from synnefo import quotas + +log = logging.getLogger(__name__) + + +@transaction.commit_on_success +def create(user_id, size, server_id, name=None, description=None, + source_volume_id=None, source_snapshot_id=None, + source_image_id=None, volume_type_id=None, metadata=None, + project=None): + + # Currently we cannot create volumes without being attached to a server + if server_id is None: + raise faults.BadRequest("Volume must be attached to server") + server = util.get_server(user_id, server_id, for_update=True, + non_deleted=True, + exception=faults.BadRequest) + + server_vtype = server.flavor.volume_type + if volume_type_id is not None: + volume_type = util.get_volume_type(volume_type_id, + include_deleted=False, + exception=faults.BadRequest) + if volume_type != server_vtype: + raise faults.BadRequest("Cannot create a volume with type '%s' to" + " a server with volume type '%s'." + % (volume_type.id, server_vtype.id)) + else: + volume_type = server_vtype + + # Assert that not more than one source are used + sources = filter(lambda x: x is not None, + [source_volume_id, source_snapshot_id, source_image_id]) + if len(sources) > 1: + raise faults.BadRequest("Volume can not have more than one source!") + + if source_volume_id is not None: + source_type = "volume" + source_uuid = source_volume_id + elif source_snapshot_id is not None: + source_type = "snapshot" + source_uuid = source_snapshot_id + elif source_image_id is not None: + source_type = "image" + source_uuid = source_image_id + else: + source_type = "blank" + source_uuid = None + + if project is None: + project = user_id + + if metadata is not None and \ + len(metadata) > settings.CYCLADES_VOLUME_MAX_METADATA: + raise faults.BadRequest("Volumes cannot have more than %s metadata " + "items" % + settings.CYCLADES_VOLUME_MAX_METADATA) + + volume = _create_volume(server, user_id, project, size, + source_type, source_uuid, + volume_type=volume_type, name=name, + description=description, index=None) + + if metadata is not None: + for meta_key, meta_val in metadata.items(): + utils.check_name_length(meta_key, VolumeMetadata.KEY_LENGTH, + "Metadata key is too long") + utils.check_name_length(meta_val, VolumeMetadata.VALUE_LENGTH, + "Metadata key is too long") + volume.metadata.create(key=meta_key, value=meta_val) + + server_attachments.attach_volume(server, volume) + + return volume + + +def _create_volume(server, user_id, project, size, source_type, source_uuid, + volume_type, name=None, description=None, index=None, + delete_on_termination=True): + + utils.check_name_length(name, Volume.NAME_LENGTH, + "Volume name is too long") + utils.check_name_length(description, Volume.DESCRIPTION_LENGTH, + "Volume description is too long") + validate_volume_termination(volume_type, delete_on_termination) + + if index is None: + # Counting a server's volumes is safe, because we have an + # X-lock on the server. + index = server.volumes.filter(deleted=False).count() + + if size is not None: + try: + size = int(size) + except (TypeError, ValueError): + raise faults.BadRequest("Volume 'size' needs to be a positive" + " integer value.") + if size < 1: + raise faults.BadRequest("Volume size must be a positive integer") + if size > settings.CYCLADES_VOLUME_MAX_SIZE: + raise faults.BadRequest("Maximum volume size is '%sGB'" % + settings.CYCLADES_VOLUME_MAX_SIZE) + + # Only ext_ disk template supports cloning from another source. Otherwise + # is must be the root volume so that 'snf-image' fill the volume + can_have_source = (index == 0 or + volume_type.provider in settings.GANETI_CLONE_PROVIDERS) + if not can_have_source and source_type != "blank": + msg = ("Cannot specify a 'source' attribute for volume type '%s' with" + " disk template '%s'" % + (volume_type.id, volume_type.disk_template)) + raise faults.BadRequest(msg) + + source_version = None + origin_size = None + # TODO: Check Volume/Snapshot Status + if source_type == "snapshot": + source_snapshot = util.get_snapshot(user_id, source_uuid, + exception=faults.BadRequest) + snap_status = source_snapshot.get("status", "").upper() + if snap_status != OBJECT_AVAILABLE: + raise faults.BadRequest("Cannot create volume from snapshot, while" + " snapshot is in '%s' status" % + snap_status) + source = Volume.prefix_source(source_uuid, + source_type="snapshot") + if size is None: + raise faults.BadRequest("Volume size is required") + elif (size << 30) < int(source_snapshot["size"]): + raise faults.BadRequest("Volume size '%s' is smaller than" + " snapshot's size '%s'" + % (size << 30, source_snapshot["size"])) + source_version = source_snapshot["version"] + origin = source_snapshot["mapfile"] + origin_size = source_snapshot["size"] + elif source_type == "image": + source_image = util.get_image(user_id, source_uuid, + exception=faults.BadRequest) + img_status = source_image.get("status", "").upper() + if img_status != OBJECT_AVAILABLE: + raise faults.BadRequest("Cannot create volume from image, while" + " image is in '%s' status" % img_status) + if size is None: + raise faults.BadRequest("Volume size is required") + elif (size << 30) < int(source_image["size"]): + raise faults.BadRequest("Volume size '%s' is smaller than" + " image's size '%s'" + % (size << 30, source_image["size"])) + source = Volume.prefix_source(source_uuid, source_type="image") + source_version = source_image["version"] + origin = source_image["mapfile"] + origin_size = source_image["size"] + elif source_type == "blank": + if size is None: + raise faults.BadRequest("Volume size is required") + source = origin = None + elif source_type == "volume": + # Currently, Archipelago does not support cloning a volume + raise faults.BadRequest("Cloning a volume is not supported") + # source_volume = util.get_volume(user_id, source_uuid, + # for_update=True, non_deleted=True, + # exception=faults.BadRequest) + # if source_volume.status != "IN_USE": + # raise faults.BadRequest("Cannot clone volume while it is in '%s'" + # " status" % source_volume.status) + # # If no size is specified, use the size of the volume + # if size is None: + # size = source_volume.size + # elif size < source_volume.size: + # raise faults.BadRequest("Volume size cannot be smaller than the" + # " source volume") + # source = Volume.prefix_source(source_uuid, source_type="volume") + # origin = source_volume.backend_volume_uuid + else: + raise faults.BadRequest("Unknown source type") + + volume = Volume.objects.create(userid=user_id, + project=project, + size=size, + volume_type=volume_type, + name=name, + machine=server, + description=description, + delete_on_termination=delete_on_termination, + source=source, + source_version=source_version, + origin=origin, + index=index, + status="CREATING") + + # Store the size of the origin in the volume object but not in the DB. + # We will have to change this in order to support detachable volumes. + volume.origin_size = origin_size + + return volume + + +@transaction.commit_on_success +def delete(volume): + """Delete a Volume""" + # A volume is deleted by detaching it from the server that is attached. + # Deleting a detached volume is not implemented. + server_id = volume.machine_id + if server_id is not None: + server = util.get_server(volume.userid, server_id, for_update=True, + non_deleted=True, + exception=faults.BadRequest) + server_attachments.detach_volume(server, volume) + log.info("Detach volume '%s' from server '%s', job: %s", + volume.id, server_id, volume.backendjobid) + else: + raise faults.BadRequest("Cannot delete a detached volume") + + return volume + + +@transaction.commit_on_success +def update(volume, name=None, description=None, delete_on_termination=None): + if name is not None: + utils.check_name_length(name, Volume.NAME_LENGTH, + "Volume name is too long") + volume.name = name + if description is not None: + utils.check_name_length(description, Volume.DESCRIPTION_LENGTH, + "Volume description is too long") + volume.description = description + if delete_on_termination is not None: + validate_volume_termination(volume.volume_type, delete_on_termination) + volume.delete_on_termination = delete_on_termination + + volume.save() + return volume + + +@transaction.commit_on_success +def reassign_volume(volume, project): + if volume.index == 0: + raise faults.Conflict("Cannot reassign: %s is a system volume" % + volume.id) + if volume.machine_id is not None: + server = util.get_server(volume.userid, volume.machine_id, + for_update=True, non_deleted=True, + exception=faults.BadRequest) + commands.validate_server_action(server, "REASSIGN") + action_fields = {"from_project": volume.project, "to_project": project} + log.info("Reassigning volume %s from project %s to %s", + volume.id, volume.project, project) + volume.project = project + volume.save() + quotas.issue_and_accept_commission(volume, action="REASSIGN", + action_fields=action_fields) + + +def validate_volume_termination(volume_type, delete_on_termination): + """Validate volume's termination mode based on volume's type. + + NOTE: Currently, detached volumes are not supported, so all volumes + must be terminated upon instance deletion. + + """ + if delete_on_termination is False: + # Only ext_* volumes can be preserved + if volume_type.template != "ext": + raise faults.BadRequest("Volumes of '%s' disk template cannot have" + " 'delete_on_termination' attribute set" + " to 'False'" % volume_type.disk_template) + # But currently all volumes must be terminated + raise faults.NotImplemented("Volumes with the 'delete_on_termination'" + " attribute set to False are not" + " supported") diff --git a/snf-cyclades-gtools/MANIFEST.in b/snf-cyclades-gtools/MANIFEST.in index 30e7467ed8ae951d633b39b930efcb2653ad6c69..ce7f614828c2d727bb93e8692c231749d45fdd5a 100644 --- a/snf-cyclades-gtools/MANIFEST.in +++ b/snf-cyclades-gtools/MANIFEST.in @@ -1,2 +1 @@ -include kvm-vif-bridge -include distribute_setup.py +include distribute_setup.py README.md diff --git a/snf-cyclades-gtools/README.md b/snf-cyclades-gtools/README.md new file mode 100644 index 0000000000000000000000000000000000000000..52c5aa7c1d9c4c6a82c6bbee86f17acc27318c12 --- /dev/null +++ b/snf-cyclades-gtools/README.md @@ -0,0 +1,27 @@ +snf-cyclades-gtools +=================== + +Overview +-------- + +This is Synnefo's snf-cyclades-gtools component. Please see the [official +Synnefo site](http://www.synnefo.org) for more information. + + +Copyright and license +===================== + +Copyright (C) 2010-2014 GRNET S.A. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. diff --git a/snf-cyclades-gtools/collectd/plugins/ganeti-cpustats.py b/snf-cyclades-gtools/collectd/plugins/ganeti-cpustats.py index b05ad1313f6b322cb27f0ffc23e254592f5e6353..9ef16ce5b0cc15b770a99616f197c80d6eda2b44 100644 --- a/snf-cyclades-gtools/collectd/plugins/ganeti-cpustats.py +++ b/snf-cyclades-gtools/collectd/plugins/ganeti-cpustats.py @@ -20,7 +20,7 @@ def cpustats(data=None): for file in glob("/var/run/ganeti/kvm-hypervisor/pid/*"): instance = os.path.basename(file) try: - pid = int(open(file, "r").read()) + pid = int(open(file, "r").readline()) proc = open("/proc/%d/stat" % pid, "r") cputime = [int(proc.readline().split()[42])] except EnvironmentError: @@ -28,6 +28,9 @@ def cpustats(data=None): vcpus = get_vcpus(pid) proc.close() + if vcpus == 0: + continue + vl = collectd.Values(type="derive") vl.host = instance vl.plugin = "cpu" diff --git a/snf-cyclades-gtools/docs/index.rst b/snf-cyclades-gtools/docs/index.rst index b34d15fab4aa7fc55a89246b37d37881499c8f95..923b30daf1cd368fc86988ed4d7a984ca10cce3f 100644 --- a/snf-cyclades-gtools/docs/index.rst +++ b/snf-cyclades-gtools/docs/index.rst @@ -43,9 +43,7 @@ Package source -------------- The source for component :ref:`snf-cyclades-gtools <snf-cyclades-gtools>` -lives under ``snf-cyclades-gtools/`` at ``git://code.grnet.gr/git/synnefo``, -also accessible at -`code.grnet.gr <https://code.grnet.gr/projects/synnefo/repository/revisions/master/show/snf-cyclades-gtools>`_. +lives under ``snf-cyclades-gtools/`` at ``https://github.com/grnet/synnefo``. Package installation -------------------- @@ -77,7 +75,7 @@ Hook **** The hook needs to be enabled for phases ``post-{add,modify,reboot,start,stop}`` by *symlinking* in -``/etc/ganeti/hooks/instance-{add,modify,reboot,start,stop}-post.d`` +``/etc/ganeti/hooks/instance-{add,modify,reboot,start,stop}-post.d`` on :ref:`GANETI-MASTER <GANETI_MASTER>`, e.g.: .. code-block:: console diff --git a/snf-cyclades-gtools/setup.py b/snf-cyclades-gtools/setup.py index bcd8e3ea3927604d24a341a17f51ec66774e750e..7abf1295a8f313a22129676dfe63c940e16edee2 100644 --- a/snf-cyclades-gtools/setup.py +++ b/snf-cyclades-gtools/setup.py @@ -1,35 +1,17 @@ -# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # import os @@ -51,7 +33,7 @@ setup( maintainer='Synnefo development team', maintainer_email='synnefo-devel@googlegroups.com', - license="BSD", + license="GNU GPLv3", namespace_packages=["synnefo", "synnefo.versions"], packages=["synnefo", "synnefo.ganeti", "synnefo.versions"], dependency_links=['http://www.synnefo.org/packages/pypi'], @@ -60,16 +42,15 @@ setup( 'python-daemon>=1.5.5', 'pyinotify>=0.8.9', 'puka', - 'python-prctl>=1.1.1', 'setproctitle>=1.0.1' ], entry_points={ - 'console_scripts': [ - 'snf-ganeti-eventd = synnefo.ganeti.eventd:main', - 'snf-progress-monitor = synnefo.ganeti.progress_monitor:main' - ], - 'synnefo': [ + 'console_scripts': [ + 'snf-ganeti-eventd = synnefo.ganeti.eventd:main', + 'snf-progress-monitor = synnefo.ganeti.progress_monitor:main' + ], + 'synnefo': [ 'default_settings = synnefo.ganeti.settings' - ] - }, + ] + }, ) diff --git a/snf-cyclades-gtools/synnefo/ganeti/eventd.py b/snf-cyclades-gtools/synnefo/ganeti/eventd.py index b602df20e3ece8265620d1d69a7cc86416184199..972d370719ad60d97f1593a5be5818ec6c23c14d 100644 --- a/snf-cyclades-gtools/synnefo/ganeti/eventd.py +++ b/snf-cyclades-gtools/synnefo/ganeti/eventd.py @@ -1,38 +1,20 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # """Ganeti notification daemon with AMQP support @@ -56,14 +38,14 @@ sys.path.append(path) # /etc/ganeti/share # Favor latest ganeti if found if os.path.exists(NEW_GANETI_PATH): - GANETI_PATH = NEW_GANETI_PATH + GANETI_PATH = NEW_GANETI_PATH else: - GANETI_PATH = OLD_GANETI_PATH + GANETI_PATH = OLD_GANETI_PATH sys.path.insert(0, GANETI_PATH) try: - import ganeti + import ganeti # NOQA except ImportError: raise Exception("Cannot import ganeti module. Please check if installed" " under %s for 2.8 or under %s for 2.10 or later." % @@ -117,41 +99,55 @@ def get_time_from_status(op, job): raise InvalidBackendStatus(status, job) -def get_instance_nics(instance, logger): - """Query Ganeti to a get the instance's NICs. +def get_instance_attachments(instance, logger): + """Query Ganeti to a get the instance's attachments (NICs and Disks) - Get instance's NICs from Ganeti configuration data. If running on master, - query Ganeti via Ganeti CLI client. Otherwise, get the nics from Ganeti - configuration file. + Get instance's attachments from Ganeti configuration data. If running on + master, query Ganeti via Ganeti CLI client. Otherwise, get attachments + straight from Ganeti's configuration file. @type instance: string @param instance: the name of the instance - @rtype: List of dicts - @return: Dictionary containing the instance's NICs. Each dictionary - contains the following keys: 'network', 'ip', 'mac', 'mode', - 'link' and 'firewall' + @rtype: instance's NICs and Disks + @return: Dictionary containing the 'nics' and 'disks' of the instance. """ try: client = cli.GetClient() - fields = ["nic.names", "nic.networks.names", "nic.ips", "nic.macs", - "nic.modes", "nic.links", "tags"] - info = client.QueryInstances([instance], fields, use_locking=False) - names, networks, ips, macs, modes, links, tags = info[0] - nic_keys = ["name", "network", "ip", "mac", "mode", "link"] - nics = zip(names, networks, ips, macs, modes, links) + q_fields = ["nic.names", "nic.networks.names", "nic.ips", "nic.macs", + "nic.modes", "nic.links", "nic.uuids", "tags", + "disk.names", "disk.sizes", "disk.uuids"] + info = client.QueryInstances([instance], q_fields, use_locking=False) + # Parse NICs + names, networks, ips, macs, modes, links, uuids, tags = info[0][:-3] + nic_keys = ["name", "network", "ip", "mac", "mode", "link", "uuid"] + nics = zip(names, networks, ips, macs, modes, links, uuids) nics = map(lambda x: dict(zip(nic_keys, x)), nics) + # Parse Disks + names, sizes, uuids = info[0][-3:] + disk_keys = ["name", "size", "uuid"] + disks = zip(names, sizes, uuids) + disks = map(lambda x: dict(zip(disk_keys, x)), disks) except ganeti_errors.OpPrereqError: # Not running on master! Load the conf file raw_data = utils.ReadFile(pathutils.CLUSTER_CONF_FILE) config = serializer.LoadJson(raw_data) i = config["instances"][instance] + # Parse NICs nics = [] - for nic in i["nics"]: + for index, nic in enumerate(i["nics"]): params = nic.pop("nicparams") nic["mode"] = params["mode"] nic["link"] = params["link"] + nic["index"] = index nics.append(nic) + # Parse Disks + disks = [] + for index, disk in enumerate(i["disks"]): + disks.append({"name": disk.pop("name"), + "size": disk["size"], + "uuid": disk["uuid"], + "index": index}) tags = i.get("tags", []) # Get firewall from instance Tags # Tags are of the form synnefo:network:N:firewall_mode @@ -165,7 +161,9 @@ def get_instance_nics(instance, logger): firewall = t[3] [nic.setdefault("firewall", firewall) for nic in nics if nic["name"] == nic_name] - return nics + attachments = {"nics": nics, + "disks": disks} + return attachments class InvalidBackendStatus(Exception): @@ -208,8 +206,9 @@ class JobFileHandler(pyinotify.ProcessEvent): self.op_handlers = {"INSTANCE": self.process_instance_op, "NETWORK": self.process_network_op, - "CLUSTER": self.process_cluster_op} + "CLUSTER": self.process_cluster_op, # "GROUP": self.process_group_op} + "TAGS": self.process_tag_op} def process_IN_CLOSE_WRITE(self, event): self.process_IN_MOVED_TO(event) @@ -306,6 +305,26 @@ class JobFileHandler(pyinotify.ProcessEvent): job_fields = {"nics": get_field(input, "nics"), "disks": get_field(input, "disks"), "beparams": get_field(input, "beparams")} + elif op_id == "OP_INSTANCE_SNAPSHOT": + # Cyclades store the UUID of the snapshot as the 'reason' attribute + # of the Ganeti job in order to be able to update the status of + # the snapshot based on the result of the Ganeti job. Parse this + # attribute and include it in the msg. + # NOTE: This will fill the 'snapshot_info' attribute only for the + # first disk, but this is ok because Cyclades do not issue jobs to + # create snapshots of many disks. + disks = get_field(input, "disks") + if disks: + reason = get_field(input, "reason") + snapshot_info = None + try: + reason = reason[0] + assert (reason[0] == "gnt:user") + snapshot_info = reason[1] + disks[0][1]["snapshot_info"] = snapshot_info + except: + self.logger.warning("Malformed snapshot job '%s'", job_id) + job_fields = {"disks": disks} msg = {"type": "ganeti-op-status", "instance": instances, @@ -316,8 +335,10 @@ class JobFileHandler(pyinotify.ProcessEvent): op.status == "success") or (op_id == "OP_INSTANCE_SET_PARAMS" and op.status in ["success", "error", "cancelled"])): - nics = get_instance_nics(msg["instance"], self.logger) - msg["instance_nics"] = nics + attachments = get_instance_attachments(msg["instance"], + self.logger) + msg["instance_nics"] = attachments["nics"] + msg["instance_disks"] = attachments["disks"] routekey = "ganeti.%s.event.op" % prefix_from_name(instances) @@ -377,6 +398,30 @@ class JobFileHandler(pyinotify.ProcessEvent): return msg, routekey + def process_tag_op(self, op, job_id): + """ Process OP_TAGS_* opcodes. + + """ + input = op.input + op_id = input.OP_ID + if op_id == "OP_TAGS_SET": + # NOTE: Check 'dry_run' after 'cluster' because networks and groups + # do not support the 'dry_run' option. + if (op.status == "waiting" and input.tags and + input.kind == "cluster" and input.dry_run): + # Special where a prefixed cluster tag operation in dry-run + # mode is used in order to trigger eventd to send a + # heartbeat message. + tag = input.tags[0] + if tag.startswith("snf:eventd:heartbeat"): + self.logger.debug("Received heartbeat tag '%s'." + " Sending response.", tag) + msg = {"type": "eventd-heartbeat", + "cluster": self.cluster_name} + return msg, "eventd.heartbeat" + + return None, None + def find_cluster_name(): global handler_logger @@ -496,7 +541,10 @@ def main(): while True: # loop forever # process the queue of events as explained above - notifier.process_events() + try: + notifier.process_events() + except StandardError: + logger.exception("Unhandled exception") if notifier.check_events(): # read notified events and enqeue them notifier.read_events() diff --git a/snf-cyclades-gtools/synnefo/ganeti/progress_monitor.py b/snf-cyclades-gtools/synnefo/ganeti/progress_monitor.py index bf4ac9a60eafcaef6c109d2e5e506c041a6159c0..305ac9d3e03037938ac2517d81ce92fbc32ef8ae 100755 --- a/snf-cyclades-gtools/synnefo/ganeti/progress_monitor.py +++ b/snf-cyclades-gtools/synnefo/ganeti/progress_monitor.py @@ -1,38 +1,20 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# Copyright 2011, 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # """Utility to monitor the progress of image deployment diff --git a/snf-cyclades-gtools/synnefo/versions/__init__.py b/snf-cyclades-gtools/synnefo/versions/__init__.py index e68793f97a2b2b2232a6ac7007e881e9bf203b66..4ee938c013a307567139bc35cd25d0ff33546fba 100644 --- a/snf-cyclades-gtools/synnefo/versions/__init__.py +++ b/snf-cyclades-gtools/synnefo/versions/__init__.py @@ -1,35 +1,17 @@ -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # this is a namespace package try: diff --git a/snf-cyclades-gtools/test/synnefo.ganeti_unittest.py b/snf-cyclades-gtools/test/synnefo.ganeti_unittest.py index 7bf8525c70aacb8f7adb8e817a3b43468e41cced..b47bd7e0bd27e1e8edc99704f925747fb60bea11 100755 --- a/snf-cyclades-gtools/test/synnefo.ganeti_unittest.py +++ b/snf-cyclades-gtools/test/synnefo.ganeti_unittest.py @@ -1,38 +1,20 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # import sys diff --git a/snf-deploy/MANIFEST.in b/snf-deploy/MANIFEST.in index 6106c5d89680afa76e234fbcf1feb50c52b9a5f6..ce7f614828c2d727bb93e8692c231749d45fdd5a 100644 --- a/snf-deploy/MANIFEST.in +++ b/snf-deploy/MANIFEST.in @@ -1 +1 @@ -include distribute_setup.py +include distribute_setup.py README.md diff --git a/snf-deploy/README.md b/snf-deploy/README.md new file mode 100644 index 0000000000000000000000000000000000000000..53620bacf862dc71f361b572c19bb685b9ac73e3 --- /dev/null +++ b/snf-deploy/README.md @@ -0,0 +1,27 @@ +snf-deploy +========== + +Overview +-------- + +This is Synnefo's snf-deploy component. Please see the [official Synnefo +site](http://www.synnefo.org) for more information. + + +Copyright and license +===================== + +Copyright (C) 2010-2014 GRNET S.A. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. diff --git a/snf-deploy/conf/deploy.conf b/snf-deploy/conf/deploy.conf index 5f7f89054e6e346fac2b5215b6872a61b3ae4a20..c019e00af7eba14c90535650617d3b39c433fc41 100644 --- a/snf-deploy/conf/deploy.conf +++ b/snf-deploy/conf/deploy.conf @@ -1,40 +1,34 @@ -[packages] +[DEFAULT] # whether to use apt-get or local generated package found in packages dir use_local_packages = True # url to obtain latest synnefo packages. -# To use them change USE_LOCAL_PACKAGES setting to yes # To get them run: snf-deploy packages package_url = http://builder.dev.grnet.gr/synnefo/packages/Squeeze/40/ -[dirs] +# dir to store snf-deploy status +state_dir = /var/lib/snf-deploy # dir to find all template files used to customize setup # in case you want to add another setting please modify the corresponding file -templates = /var/lib/snf-deploy/files -# dir to store local images (disk0, disk1 of the virtual cluster) -images = /var/lib/snf-deploy/images +template_dir = /var/lib/snf-deploy/files +# dir to store disks for the virtual cluster) +vcluster_dir = /var/lib/snf-deploy/vcluster # dir to store/find local packages # dir to locally save packages that will be downloaded from package_url # put here any locally created packages (useful for development) -packages = /var/lib/snf-deploy/packages +package_dir = /var/lib/snf-deploy/packages # dir to store pidfiles (dnsmasq, kvm) -run = /var/run/snf-deploy +run_dir = /var/run/snf-deploy # dir to store dnsmasq related files -dns = /var/lib/snf-deploy/dnsmasq +dns_dir = /var/lib/snf-deploy/dnsmasq # dir to lookup fabfile and ifup script -lib = /usr/lib/snf-deploy -# dir to store executed commands (to enforce sequential execution) -cmd = /var/run/snf-deploy/cmd +lib_dir = /usr/lib/snf-deploy # dir to be used by Django for file-based mail backend mail_dir = /var/tmp/synnefo-mails -[keys] -# whether to create new keys -keygen = False # whether to inject ssh keys found in templates/root/.ssh in nodes key_inject = True -[options] # Deploy Synnefo, specially tuned for testing. This option improves the speed # of some operations, but is not safe for all enviroments. (e.g. disable # fsync of postgresql) diff --git a/snf-deploy/conf/ganeti.conf b/snf-deploy/conf/ganeti.conf index 6a0ece10f0c1638b95bb0a25032722924f6a3bdf..f3ca8bddc250f676e6902dfd65414d95c04686de 100644 --- a/snf-deploy/conf/ganeti.conf +++ b/snf-deploy/conf/ganeti.conf @@ -1,18 +1,27 @@ -[ganeti1] -cluster_nodes = node1 -master_node = node1 +[DEFAULT] +vg = ganeti +# Ganeti has hard requiremend for VG larger than 20480M +vg_size = 30G +# whether to add synnefo related packages +synnefo = True -cluster_netdev = eth0 -cluster_name = ganeti1 -cluster_ip = 192.168.0.13 +[ganeti] +name = ganeti +domain = synnefo.live +ip = 10.1.2.101 +netdev = eth0 -vg = autovg -synnefo_public_network_subnet = 10.0.1.0/24 -synnefo_public_network_gateway = 10.0.1.1 -synnefo_public_network_type = CUSTOM +[ganeti-qa] +name = ganeti +domain = qa.synnefo.live +ip = 10.1.2.101 +netdev = eth0 +synnefo = -image_dir = /srv/okeanos -# To add another cluster repeat the above section -# with different header and nodes +[ganeti-vc] +name = ganeti +domain = vcluster.synnefo.live +ip = 10.1.2.101 +netdev = eth0 diff --git a/snf-deploy/conf/nodes.conf b/snf-deploy/conf/nodes.conf index 27f60f0d28a643819cafae19624e941e856073ed..6fb814e10e5e5d34810ae6fd1d2a6948cf161c99 100644 --- a/snf-deploy/conf/nodes.conf +++ b/snf-deploy/conf/nodes.conf @@ -1,42 +1,104 @@ -# please note that currently is only supported deployment -# with nodes (both ganeti and synnefo) residing in the same subnet/domain -[network] +# In this section we define configuration setting common to all nodes +[DEFAULT] +# Currently both ganeti and synnefo must reside in the same domain +# Instances will reside in the .vm.<domain> subdomain domain = synnefo.live -[os] -node1 = wheezy -# node2 = wheezy +# Each node should define: -[hostnames] -node1 = auto1 -# node2 = auto2 - -[ips] -node1 = 192.168.0.1 -# node2 = 192.168.0.2 +# The node's desired hostname. It will be set +hostname = +# The node's primary IP +ip = # This is used only in case of vcluster # needed to pass the correct dhcp responces to the virtual nodes -[macs] -node1 = 52:54:00:00:00:01 -# node2 = 52:54:00:00:00:02 - -[info] -# Here we define which nodes from the predefined ones to use -nodes = node1 - -# login credentials for the nodes -# please note that in case of vcluster these are preconfigured -# and not editable. -# in case of physical nodes all nodes should have the same login account +mac = + +# The node's OS (debian, ubuntu, etc) +# Currently tested only under debian (wheezy) +os = debian + +# The node's administrator account (with root priviledges) user = root -password = 12345 +# The root's password +password = +# The interface with internet access public_iface = eth0 +# The interface for the instances' public traffic vm_public_iface = eth1 +# The interface for the instances' private traffic vm_private_iface = eth2 -# extra disk name inside the nodes -# if defined, snf-deploy will create a VG for ganeti in order to support lvm storage -# if not then only file disk template will be supported +# The extra disk for the Ganeti VG needed for plain and drbd disk templates extra_disk = /dev/vdb + +################### +# synnefo/ci node # +################### + +[node] +name = node +ip = 192.0.2.1 +extra_disk = + +############ +# qa nodes # +############ + +[dev] +name = qa2 +ip = 10.1.2.10 +public_iface = eth1 +domain = qa.synnefo.live + +[qa1] +name = qa1 +ip = 10.1.2.11 +public_iface = eth1 +domain = qa.synnefo.live + +[qa2] +name = qa2 +ip = 10.1.2.12 +public_iface = eth1 +domain = qa.synnefo.live + +############ +# vc nodes # +############ + +[vc1] +mac = 52:54:00:00:00:01 +name = vc1 +ip = 10.1.2.1 +public_iface = eth0 +domain = vcluster.synnefo.live + +[vc2] +mac = 52:54:00:00:00:02 +name = vc2 +ip = 10.1.2.2 +public_iface = eth0 +domain = vcluster.synnefo.live + +[vc3] +mac = 52:54:00:00:00:03 +name = vc3 +ip = 10.1.2.3 +public_iface = eth0 +domain = vcluster.synnefo.live + +[vc4] +mac = 52:54:00:00:00:04 +name = vc4 +ip = 10.1.2.4 +public_iface = eth0 +domain = vcluster.synnefo.live + +[dummy] +name = dummy +ip = 1.2.3.4 +public_iface = eth0 +domain = synnefo.live diff --git a/snf-deploy/conf/packages.conf b/snf-deploy/conf/packages.conf new file mode 100644 index 0000000000000000000000000000000000000000..41b2b22f1d0252f5d5a399adb25841907b492bdb --- /dev/null +++ b/snf-deploy/conf/packages.conf @@ -0,0 +1,9 @@ +[debian] +python-nfqueue = 0.4+physindev-1~wheezy +python-scapy = 2.2.0+rfc6355-1 +snf-ganeti = unstable +ganeti2 = unstable +python-django-eztables = 0.3.3-1~snf~0.2 +qemu-kvm = wheezy-backports + +[ubuntu] diff --git a/snf-deploy/conf/setups.conf b/snf-deploy/conf/setups.conf new file mode 100644 index 0000000000000000000000000000000000000000..7525c58f27d2293c9b4dbca4d7f694f7c3593163 --- /dev/null +++ b/snf-deploy/conf/setups.conf @@ -0,0 +1,79 @@ +[DEFAULT] + +################################# +# snf-deploy synnefo --autoconf # +################################# + +[auto] +ns = node +client = node +ca = node +router = node +nfs = node +db = node +mq = node +astakos = node +cyclades = node +admin = node +vnc = node +pithos = node +cms = node +stats = node +dev = node +clusters = + ganeti + + +[ganeti] +master = node +vmc = + node + +################################### +# snf-deploy ganeti-qa --setup qa # +################################### + +[qa] +ns = dev +client = dev +router = qa1 +nfs = dev +dev = dev +clusters = + ganeti-qa + + +[ganeti-qa] +master = qa1 +vmc = + qa1 + qa2 + +################################## +# snf-deploy vcluster --setup vc # +################################## + +[vc] +ns = vc1 +client = vc4 +router = vc1 +nfs = vc1 +db = vc2 +mq = vc3 +astakos = vc1 +cyclades = vc2 +pithos = vc3 +cms = vc4 +stats = vc1 +dev = vc1 +clusters = + ganeti-vc + + +[ganeti-vc] +master = vc1 +vmc = + vc1 + vc2 + vc3 + vc4 diff --git a/snf-deploy/conf/squeeze.conf b/snf-deploy/conf/squeeze.conf deleted file mode 100644 index 8f81c9a58665d7adda27e640996775e56773426e..0000000000000000000000000000000000000000 --- a/snf-deploy/conf/squeeze.conf +++ /dev/null @@ -1,57 +0,0 @@ -[debian] -rabbitmq-server = squeeze-backports -gunicorn = squeeze-backports -qemu-kvm = squeeze-backports -qemu = squeeze-backports -python-gevent = squeeze-backports -apache2 = -postgresql = -python-psycopg2 = -python-argparse = -nfs-kernel-server = squeeze-backports -nfs-common = squeeze-backports -bind9 = -vlan = -vlan = -lvm2 = -curl = -memcached = -python-memcache = -bridge-utils = -python-progress = -ganeti-instance-debootstrap = -python-django-south = squeeze-backports -python-django = squeeze-backports -drbd8-utils = - - -[synnefo] -snf-astakos-app = squeeze -snf-common = squeeze -snf-cyclades-app = squeeze -snf-cyclades-gtools = squeeze -snf-django-lib = squeeze -python-astakosclient = squeeze -snf-branding = squeeze -snf-webproject = squeeze -snf-pithos-app = squeeze -snf-pithos-backend = squeeze -snf-tools = squeeze - - -[ganeti] -snf-ganeti = 2.6.2+ippool11+hotplug5+extstorage3+rbdfix1+kvmfix2+nolvm+netxen-1~squeeze -ganeti-htools = 2.6.2+ippool11+hotplug5+extstorage3+rbdfix1+kvmfix2+nolvm+netxen-1~squeeze - -[other] -snf-cloudcms = squeeze -snf-vncauthproxy = squeeze -snf-pithos-webclient = squeeze -snf-image = squeeze -snf-network = squeeze -python-objpool = squeeze -nfdhcpd = squeeze -kamaki = squeeze -python-bitarray = squeeze-backports -nfqueue-bindings-python = 0.3+physindev-1 - diff --git a/snf-deploy/conf/synnefo.conf b/snf-deploy/conf/synnefo.conf index bbcb7bb80a5b6ec09e98ecdb39a75a3d696d89db..e5cad0755835b39768ef5f827a9982d670814cef 100644 --- a/snf-deploy/conf/synnefo.conf +++ b/snf-deploy/conf/synnefo.conf @@ -1,39 +1,39 @@ -[cred] +[DEFAULT] +# various credentials synnefo_user = synnefo synnefo_db_passwd = example_passw0rd synnefo_rapi_passwd = example_rapi_passw0rd synnefo_rabbitmq_passwd = example_rabbitmq_passw0rd +synnefo_vnc_passwd = example_vnc_passw0rd +cyclades_secret = example_cyclades_secret +oa2_secret = example_oa2_secret +webproject_secret = example_webproject_secret +stats_secret = example_stats_secret +collectd_secret = example_collectd_secret + user_email = user@synnefo.org user_name = John user_lastname = Doe user_passwd = 12345 -oa2_secret = 12345 - -[roles] -accounts = node1 -compute = node1 -object-store = node1 -cyclades = node1 -pithos = node1 -cms = node1 -db = node1 -mq = node1 -ns = node1 -client = node1 -router = node1 -stats = node1 +# one common shared dir for nfs +shared_dir = /srv - -[synnefo] -pithos_dir = /srv/pithos flavor_cpu = 1,2,4,8 flavor_ram = 128,256,512,1024,2048,4096,8192 flavor_disk = 2,5,10,20,40,60,80,100 -flavor_storage = file +flavor_storage = file,ext_archipelago,ext_shared-filer + +# url to download debian wheezy image +debian_base_url = http://cdn.synnefo.org/debian_base-7.0-x86_64.diskdump + +# archipelago segment +segment_size = 512 +# options related to synnefo networks vm_public_bridge = br0 vm_private_bridge = prv0 common_bridge = br0 - -debian_base_url = http://cdn.synnefo.org/debian_base-7.0-x86_64.diskdump +synnefo_public_network_subnet = 10.2.1.0/24 +synnefo_public_network_gateway = 10.2.1.1 +synnefo_public_network_type = CUSTOM diff --git a/snf-deploy/conf/vcluster.conf b/snf-deploy/conf/vcluster.conf index 377b2a9c7ec3784813a962d9a7035db0bcf29cf1..52995cd4cf899e4a10efdc213fd280837af1bd8a 100644 --- a/snf-deploy/conf/vcluster.conf +++ b/snf-deploy/conf/vcluster.conf @@ -1,31 +1,7 @@ -[image] -# url to get the base image. This is a debian base image with preconfigured -# root password and installed rsa/dsa keys. Plus a NetworkManager hook that -# changes the VM's name based on info provided by dhcp response. -# To create it run: snf-deploy image -squeeze_image_url = https://pithos.okeanos.grnet.gr/public/832xv -ubuntu_image_url = +[DEFAULT] +disk0_size = 10G +disk1_size = 30G -# in order ganeti nodes to support lvm storage (plain disk template) it will -# be needed an extra disk to eventually be able to create a VG. Ganeti requires -# this VG to be at least of 30GB. To this end in order the virtual nodes to have -# this extra disk an image should be created locally. There are three options: -# 1. not create an extra disk (only file storage template will be supported) -# 2. create an image of 30G in image dir (default /var/lib/snf-deploy/images) -# using dd if=/dev/zero of=squeeze.disk1 -# 3. create this image in a local VG using lvgreate -L30G squeeze.disk1 lvg -# and create a symbolic link in /var/lib/snf-deploy/images - -# Whether to create an extra disk or not -create_extra_disk = False -# lvg is the name of the local VG if any -lvg = - -# OS istalled in the virtual cluster -os = squeeze - - -[cluster] # the bridge to use for the virtual cluster # on this bridge we will launch a dnsnmasq and provide # fqdns needed to the cluster. @@ -33,8 +9,7 @@ os = squeeze # iptables -t nat -A POSTROUTING -s 192.0.0.0/28 -j MASQUERADE # ip addr add 192.0.0.14/28 dev auto_nodes_br # To create run: snf-deploy cluster -bridge = auto_nodes_br +bridge = vcluster_bridge -[network] -subnet = 192.168.0.0/28 -gateway = 192.168.0.14 +subnet = 10.1.2.0/24 +gateway = 10.1.2.254 diff --git a/snf-deploy/conf/wheezy.conf b/snf-deploy/conf/wheezy.conf deleted file mode 100644 index 4486a3ca5bcae0dc46e9cf00fc1f1ac8b7155469..0000000000000000000000000000000000000000 --- a/snf-deploy/conf/wheezy.conf +++ /dev/null @@ -1,59 +0,0 @@ -[debian] -rabbitmq-server = -gunicorn = -qemu-kvm = -qemu = -python-gevent = -apache2 = -postgresql = -python-psycopg2 = -python-argparse = -nfs-kernel-server = -nfs-common = -bind9 = -vlan = -vlan = -lvm2 = -curl = -memcached = -python-memcache = -bridge-utils = -python-progress = -ganeti-instance-debootstrap = -python-django-south = -python-django = -drbd8-utils = -collectd = - - -[synnefo] -snf-astakos-app = wheezy -snf-common = wheezy -snf-cyclades-app = wheezy -snf-cyclades-gtools = wheezy -snf-django-lib = wheezy -python-astakosclient = wheezy -snf-branding = wheezy -snf-webproject = wheezy -snf-pithos-app = wheezy -snf-pithos-backend = wheezy -snf-tools = wheezy -snf-stats-app = wheezy - - -[ganeti] -snf-ganeti = wheezy -ganeti-htools = wheezy -ganeti-haskell = wheezy - -[other] -snf-cloudcms = wheezy -snf-vncauthproxy = wheezy -snf-pithos-webclient = wheezy -snf-image = wheezy -snf-network = wheezy -python-objpool = wheezy -nfdhcpd = wheezy -kamaki = wheezy -python-bitarray = wheezy -python-nfqueue = 0.4+physindev-1~wheezy diff --git a/snf-deploy/files/etc/apache2/sites-available/synnefo-ssl b/snf-deploy/files/etc/apache2/sites-available/synnefo-ssl index ac0f7c71a2896d594607082267293e19e0114274..b76b780e6b0cfd96c52a1822b912b122069164d1 100644 --- a/snf-deploy/files/etc/apache2/sites-available/synnefo-ssl +++ b/snf-deploy/files/etc/apache2/sites-available/synnefo-ssl @@ -24,12 +24,14 @@ ProxyPass / http://localhost:8080/ retry=0 ProxyPassReverse / http://localhost:8080/ -# RewriteEngine On + RewriteEngine On + RewriteRule ^/$ /astakos/ui [PT,NE] # RewriteRule ^/login(.*) /im/login/redirect\$1 [PT,NE] SSLEngine on - SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem - SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key + SSLCertificateFile /etc/ssl/certs/synnefo.pem + SSLCertificateKeyFile /etc/ssl/private/synnefo.key + SSLCACertificateFile /etc/ssl/certs/synnefo_ca.pem </VirtualHost> </IfModule> diff --git a/snf-deploy/files/etc/apt/sources.list.d/synnefo.squeeze.list b/snf-deploy/files/etc/apt/sources.list.d/synnefo.squeeze.list index 1bce08f6c14ec092ea2afe289a986ab3ff0cfde9..f1855f3d8ebf261b08adcb5d6be1863e04b0d4a1 100644 --- a/snf-deploy/files/etc/apt/sources.list.d/synnefo.squeeze.list +++ b/snf-deploy/files/etc/apt/sources.list.d/synnefo.squeeze.list @@ -1,4 +1,4 @@ # Backports repository -deb http://backports.debian.org/debian-backports squeeze-backports main contrib non-free +deb http://backports.debian.org/debian-backports squeeze-backports main deb http://apt.dev.grnet.gr squeeze/ diff --git a/snf-deploy/files/etc/apt/sources.list.d/synnefo.wheezy.list b/snf-deploy/files/etc/apt/sources.list.d/synnefo.wheezy.list index 3ab1f852e81a80c0b815f7b44dd3728ce3abb852..f45c25df2c110f3a3857228754925997cb17a117 100644 --- a/snf-deploy/files/etc/apt/sources.list.d/synnefo.wheezy.list +++ b/snf-deploy/files/etc/apt/sources.list.d/synnefo.wheezy.list @@ -1,4 +1,9 @@ -deb http://ftp.de.debian.org/debian wheezy main non-free contrib +deb http://ftp.de.debian.org/debian wheezy main +deb http://ftp.de.debian.org/debian wheezy-backports main deb http://apt.dev.grnet.gr wheezy/ deb http://apt.dev.grnet.gr unstable/ +# This is for archipelago packages +deb http://apt.dev.grnet.gr experimental/ + +deb http://eu.ceph.com/debian-dumpling/ wheezy main diff --git a/snf-deploy/files/etc/archipelago/archipelago.conf b/snf-deploy/files/etc/archipelago/archipelago.conf new file mode 100644 index 0000000000000000000000000000000000000000..44a6f1dac179b114f1b8cc60fadd76b7e792ebc8 --- /dev/null +++ b/snf-deploy/files/etc/archipelago/archipelago.conf @@ -0,0 +1,123 @@ +[ARCHIPELAGO] +# Switch peer processes to run as this user +USER=archipelago +# Switch peer processes to run as this group +GROUP=synnefo +# Enable blktap module. Possible values: True/False +BLKTAP_ENABLED=True + +# xseg +[XSEG] +# Max xseg ports supported by segment +SEGMENT_PORTS = 2048 +SEGMENT_DYNPORTS = 1024 + +# Max segment size +SEGMENT_SIZE = %SEGMENT_SIZE% +# Start port of xsegbd devices +XSEGBD_START=0 +# End port of xsegbd devices +XSEGBD_END=499 + +# Start of port range that can be used by the vlmc tool +VTOOL_START=1003 +# End of port range that can be used by the vlmc tool +VTOOL_END=1022 + +[PEERS] +ROLES=blockerb blockerm mapperd vlmcd +# Order matters. Peers will be started with list order and stopped with reversed +# order. +ORDER=blockerb blockerm mapperd vlmcd + + +# (peer role, peer type) +# Mandatory peer roles: +# blockerb +# blockerm +# mapperd +# vlmcd +# +# Available peer types: +# rados_blocker +# file_blocker +# mapperd +# vlmcd +# + +# Generic peer options +# portno_start: Start of port range that will be used by the peer +# portno_end: End of port range that will be used by the peer +# nr_ops: Max number of flying operations. Must be a power of 2. +# log_level: verbosity levels for each xseg peer +# 0 - Error +# 1 - Warnings +# 2 - Info +# 3 - Debug +# Warning: debug level 3 logs A LOT! +# nr_threads: Number of threads of each peer. Currently only blockers supports +# threads with the following tricks: +# a) Threads in file_blocker are I/O threads that block. +# b) Threads in rados_blocker are processing threads. For lock +# congestion reasons, avoid setting them to a value larger than 4. + + +# file_blocker specific options: +# +# archip_dir: Where archipelago files will reside +# fdcache: Fd cache size + +# rados_blocker specific options: +# +# pool: rados pool where objects will reside + +[blockerb] +type=file_blocker +portno_start=1000 +portno_end=1000 +log_level=3 +nr_ops=64 +nr_threads=64 +archip_dir=%ARCHIP_DIR%/blocks +fdcache=512 +direct=False + +[blockerm] +type=file_blocker +portno_start=1002 +portno_end=1002 +log_level=3 +nr_ops=64 +nr_threads=64 +archip_dir=%ARCHIP_DIR%/maps +lock_dir=%ARCHIP_DIR%/locks +fdcache=512 +direct=False + +# mapperd specific options: +# +# blockerb_port: target port that will be used to communicate with the blockerb +# blockerm_port: target port that will be used to communicate with the blockerm + +[mapperd] +type=mapperd +portno_start=1001 +portno_end=1001 +log_level=3 +nr_ops=512 +blockerb_port=1000 +blockerm_port=1002 + +# vlmcd specific options: +# +# blocker_port: target port that will be used to communicate with the blockerb +# mapper_port: target port that will be used to communicate with the mapper + +[vlmcd] +type=vlmcd +portno_start=500 +portno_end=999 +log_level=3 +nr_ops=512 +blocker_port=1000 +mapper_port=1001 diff --git a/snf-deploy/files/etc/bind/named.conf.local b/snf-deploy/files/etc/bind/named.conf.local index 3b2f3ea1f742768472bad707023bf6d506cfc561..5a200a3194f8ef385e5805ca3586b4fa604ec241 100644 --- a/snf-deploy/files/etc/bind/named.conf.local +++ b/snf-deploy/files/etc/bind/named.conf.local @@ -6,12 +6,37 @@ // organization //include "/etc/bind/zones.rfc1918"; +include "/etc/bind/ddns.key"; + +// all synnefo components share the same domain/zone zone "%DOMAIN%" in { type master; + notify no; file "/etc/bind/zones/%DOMAIN%"; + allow-update { key DDNS_UPDATE; }; +}; + +# domain/zone for the VMs +zone "vm.%DOMAIN%" in { + type master; + notify no; + file "/etc/bind/zones/vm.%DOMAIN%"; + allow-update { key DDNS_UPDATE; }; }; +// reverse dns zone for all IPs zone "in-addr.arpa" in { type master; + notify no; file "/etc/bind/rev/synnefo.in-addr.arpa.zone"; + allow-update { key DDNS_UPDATE; }; }; + +// v6 reverse dns zone for all IPs +zone "ip6.arpa" in { + type master; + notify no; + file "/etc/bind/rev/synnefo.ip6.arpa.zone"; + allow-update { key DDNS_UPDATE; }; +}; + diff --git a/snf-deploy/files/etc/bind/rev/synnefo.ip6.arpa.zone b/snf-deploy/files/etc/bind/rev/synnefo.ip6.arpa.zone new file mode 100644 index 0000000000000000000000000000000000000000..6497a2a6faa463e5f18dc7558920ae9005f9e77d --- /dev/null +++ b/snf-deploy/files/etc/bind/rev/synnefo.ip6.arpa.zone @@ -0,0 +1,11 @@ +$TTL 86400 +$ORIGIN ip6.arpa. +@ IN SOA ns.%DOMAIN%. admin.%DOMAIN%. ( + 2012070900; the Serial Number + 172800; the Refresh Rate + 7200; the Retry Time + 604800; the Expiration Time + 3600) ; the Minimum Time + +@ IN NS ns.%DOMAIN%. + diff --git a/snf-deploy/files/etc/bind/synnefo.ip6.arpa.zone b/snf-deploy/files/etc/bind/synnefo.ip6.arpa.zone new file mode 100644 index 0000000000000000000000000000000000000000..c3d08007ff41fcddb7f8c418b5a2cf918bcb72bc --- /dev/null +++ b/snf-deploy/files/etc/bind/synnefo.ip6.arpa.zone @@ -0,0 +1,10 @@ +$ORIGIN . +$TTL 86400 ; 1 day +ip6.arpa IN SOA ns.vm.qa.live. admin.vm.qa.live. ( + 2012071070 ; serial + 172800 ; refresh (2 days) + 7200 ; retry (2 hours) + 604800 ; expire (1 week) + 3600 ; minimum (1 hour) + ) + NS ns.vm.qa.live. diff --git a/snf-deploy/files/etc/bind/zones/vm.example.com b/snf-deploy/files/etc/bind/zones/vm.example.com new file mode 100644 index 0000000000000000000000000000000000000000..a3da0a85e4e7ffc4dcdd5d70f725b593cc415752 --- /dev/null +++ b/snf-deploy/files/etc/bind/zones/vm.example.com @@ -0,0 +1,13 @@ +$TTL 14400 +$origin vm.%DOMAIN%. +@ IN SOA ns.vm.%DOMAIN%. admin.vm.%DOMAIN%. ( +2012111903; the Serial Number +172800; the Refresh Rate +7200; the Retry Time +604800; the Expiration Time +3600; the Minimum Time +) + +@ IN NS ns.vm.%DOMAIN%. +@ IN A %NS_NODE_IP% +ns IN A %NS_NODE_IP% diff --git a/snf-deploy/files/etc/collectd/synnefo-ganeti.conf b/snf-deploy/files/etc/collectd/synnefo-ganeti.conf index a3d51aaa7fb064e20fd0b8ea65ce6b8e1a29c76a..e9d81186b1f84c4202211997da6bd36e7b0f98e9 100644 --- a/snf-deploy/files/etc/collectd/synnefo-ganeti.conf +++ b/snf-deploy/files/etc/collectd/synnefo-ganeti.conf @@ -14,7 +14,7 @@ LoadPlugin network <Server "%STATS%" "25826"> SecurityLevel "Encrypt" Username "user" - Password "secret" + Password "%COLLECTD_SECRET%" </Server> TimeToLive 128 ReportStats false diff --git a/snf-deploy/files/etc/default/ganeti-instance-debootstrap b/snf-deploy/files/etc/default/ganeti-instance-debootstrap new file mode 100644 index 0000000000000000000000000000000000000000..5326080f3c204cf1a6ac0f95350f818f4eff4cd6 --- /dev/null +++ b/snf-deploy/files/etc/default/ganeti-instance-debootstrap @@ -0,0 +1,78 @@ +# ganeti-instance-debootstrap defaults file + +# if you want to change from the default of installing debian stable +# on the next instance, customize this file before the instance +# installation + +# PROXY: if non-null, use this as an http(s)-proxy in order to speed +# up non-cached installs or provide internet access if not directly +# possible; not that if not set, debootstrap might still use a +# system-wide proxy setting if it is exported in the ganeti-noded +# daemon environment (but the node daemon environment is cleaned up +# and not exported starting with Ganeti 2.5) +# PROXY="http://proxy.example.com:3128/" + +# MIRROR: do not customize MIRROR if you want to be able to install +# both debian and ubuntu, since they have different defaults; or +# customize it before each install +# MIRROR="http://ftp.debian.org/debian" +MIRROR="http://ftp.gr.debian.org/debian" + +# ARCH: define ARCH only if you want a different architecture than the +# current one; the known use case is to install a 32-bit instance on a +# 64-bit node; choose either "i386" or "amd64": +# ARCH="i386" +ARCH="amd64" + +# SUITE: change suite to any of the ones supported by deboostrap; this +# could be unstable, etch, etc.: +# SUITE="wheezy" +SUITE="wheezy" + +# EXTRA_PKGS: depending on the suite and architecture you are using, different +# extra packages are needed for different hypervisors. For example: +# +# Xen, for squeeze i386: +# EXTRA_PKGS="linux-image-xen-686,libc6-xen" +# Xen, for wheezy i386: +# EXTRA_PKGS="libc6-xen" +# Xen, for squeeze amd64: +# EXTRA_PKGS="linux-image-xen-amd64" +# KVM, for squeeze/wheezy i386: +# EXTRA_PKGS="acpi-support-base,console-tools,udev,linux-image-686" +# KVM, for squeeze/wheezy amd64: +# EXTRA_PKGS="acpi-support-base,console-tools,udev,linux-image-amd64" +# +EXTRA_PKGS="acpi-support-base,console-tools,udev,linux-image-amd64" + +# CUSTOMIZE_DIR: a directory containing scripts to customize the installation. +# The scripts are executed using run-parts +# By default /etc/ganeti/instance-debootstrap/hooks +# CUSTOMIZE_DIR="/etc/ganeti/instance-debootstrap/hooks" + +# GENERATE_CACHE: if set to yes (the default), create new cache files; +# any other value will disable the generation of cache files (but they +# will still be used if they exist) +GENERATE_CACHE="yes" + +# CLEAN_CACHE: should be set to the number of days after which to +# clean the cache; the default is 14 (two weeks); to disable cache +# cleaning, set it to an empty value ("") +CLEAN_CACHE="" + +# PARTITION_STYLE: whether and how the target device should be partitioned. +# Allowed values: +# 'none': just format the device, but don't partition it +# 'msdos': install an msdos partition table on the device, with a single +# partition on it +# (more styles may be added in the future) +# The default is "msdos" from ganeti 2.0 onwards, but none if installing under +# Ganeti 1.2 (os api version 5) +# PARTITION_STYLE="none" + +# PARTITION_ALIGNMENT: the alignment of the partitions in sectors +# (512B); this defaults to 1MiB to give grub enough space for +# embedding and for better alignment with modern media (HDDs and +# SSDs), feel free to increase it if your media has even bigger +# allocation blocks +# PARTITION_ALIGNMENT=2048 diff --git a/snf-deploy/files/etc/default/snf-image b/snf-deploy/files/etc/default/snf-image index 38e394e9296d4a7672249b0d0918c0efff508dd6..4760bb76d3d5fd9eeac5119b4ddc9a3b0610e545 100644 --- a/snf-deploy/files/etc/default/snf-image +++ b/snf-deploy/files/etc/default/snf-image @@ -1,90 +1,13 @@ # snf-image defaults file +# +# The original file shipped with the package can be found +# under /etc/default/snf-image.orig +# This file is genereted by snf-deploy and includes only the +# settings that differ from the defaults. -# IMAGE_NAME: Name of the image to use -# Generally you use the name of the image with the version of the OS included. -# Examples include: -# centos-5.4 debian-4.0 fedora-12 -# IMAGE_NAME="" - -# IMAGE_DIR: directory location for disk images -# IMAGE_DIR="/var/lib/snf-image" IMAGE_DIR=%IMAGE_DIR% - -# IMAGE_DEBUG: turn on debugging output for the scripts -# IMAGE_DEBUG=no - -# VERSION_CHECK: Check if host and helper have the -# same version. This is usefull if snf-image-host is -# installed as debian package and not from source. -# VERSION_CHECK="no" - -# HELPER_DIR: Directory hosting the helper files -# HELPER_DIR="/var/lib/snf-image/helper/" - -# HELPER_CACHE_DIR: Directory hosting the helper cache files -# HELPER_CACHE_DIR="/var/cache/snf-image/helper/" - -# HELPER_IMG: Path to the helper VM image -# HELPER_IMG="${HELPER_DIR}/image" - -# HELPER_KERNEL: Path to the helper VM kernel -# HELPER_KERNEL="${HELPER_DIR}/kernel" - -# HELPER_INITRD: Path to the helper VM initial ramdisk -# HELPER_INITRD="${HELPER_DIR}/initrd" - -# HELPER_TIMOUT: Soft and hard timeout limits for helper instance. -# The helper instance will be terminated after a given time if it hasn't exited -# by itself. A TERM signal will be send if the instance is running after -# a HELPER_SOFT_TIMEOUT interval. A KILL signal will be sent, if the instance -# is still running after a HELPER_HARD_TIMEOUT interval since the initial -# signal was sent. The timeout values are integer numbers with an optional -# suffix: `s' for seconds (the default), `m' for minutes, `h' for hours or `d' -# for days. -# HELPER_SOFT_TIMEOUT="20" -# HELPER_HARD_TIMEOUT="5" HELPER_SOFT_TIMEOUT=100 - -# HELPER_USER: For security reasons, it is recommended that the helper VM -# runs as an unprivileged user. KVM drops root privileges and runs as -# HELPER_USER imeddiately before starting execution of the helper VM. -# HELPER_USER="nobody" - -# MULTISTRAP_CONFIG: Configuration file to be used with multistrap to create -# the rootfs of the helper image. -# MULTISTRAP_CONFIG="/etc/snf-image/multistrap.conf" - -# MULTISTRAP_APTPREFDIR: Directory where apt preference files are hosted. Those -# files will be injected to the helper image before multistrap is called. -# MULTISTRAP_APTPREFDIR="/etc/snf-image/apt.pref.d" - -# PITHOS_DB: Pithos database in SQLAlchemy format -# PITHOS_DB="sqlite:////var/lib/pithos/backend.db" PITHOS_DB=postgresql://%SYNNEFO_USER%:%SYNNEFO_DB_PASSWD%@%DB_NODE%:5432/snf_pithos - -# PITHOS_DATA: Directory where pithos data are hosted -# PITHOS_DATA="/var/lib/pithos/data" -PITHOS_DATA=%PITHOS_DIR%/data - -# PROGRESS_MONITOR: External program that monitors the progress of the image -# deployment. The snf-image monitor messages will be redirected to the standard -# input of this program. -# PROGRESS_MONITOR="" PROGRESS_MONITOR=snf-progress-monitor - -# UNATTEND: This variables overwrites the unattend.xml file used when deploying -# a windows image. snf-image-helper will use its own unattend.xml file if this -# variable is empty. Please unless you really know what you are doing, leave -# this empty. -# UNATTEND="" - -# Paths for needed programs. Uncommend and change the variables below if you -# don't want to use the default one. -# LOSETUP="losetup" -# KPARTX="kpartx" -# SFDISK="sfdisk" -# QEMU_IMG="qemu-img" -# INSTALL_MBR="install-mbr" -# TIMELIMIT="timelimit" -# CURL="curl" CURL="curl -k" +KVM="qemu-system-x86_64 -enable-kvm -machine pc-i440fx-2.0,accel=kvm" diff --git a/snf-deploy/files/etc/default/snf-network b/snf-deploy/files/etc/default/snf-network new file mode 100644 index 0000000000000000000000000000000000000000..c8d7e565aab36d97fac9a8217cf4d4d02c241d39 --- /dev/null +++ b/snf-deploy/files/etc/default/snf-network @@ -0,0 +1,32 @@ +STATE_DIR=/var/lib/snf-network +LOGFILE=/var/log/ganeti/snf-network.log +IFUP_EXTRA_SCRIPT=/etc/ganeti/ifup-extra + +MAC_MASK=ff:ff:f0:00:00:00 + +TAP_CONSTANT_MAC=cc:47:52:4e:45:54 # GRNET in hex :-) +MAC2EUI64=/usr/bin/mac2eui64 +NFDHCPD_STATE_DIR=/var/lib/nfdhcpd +GANETI_NIC_DIR=/var/run/ganeti/xen-hypervisor/nic + +MAC_FILTERED_TAG=private-filtered +NFDHCPD_TAG=nfdhcpd +IP_LESS_ROUTED_TAG=ip-less-routed +MASQ_TAG=masq +PUBLIC_TAG=public +DNS_TAG=public + +# Default options for runlocked helper script (uncomment to modify) +#RUNLOCKED_OPTS="--id 10001 --retry-sec 0.5" + +# NS options needed by nsupdate +# A proper bind configuration is a prerequisite +# Please see: https://wiki.debian.org/DDNS +# If one of the following vars are not set dnshook wont do a thing +# Name server IP/FQDN +SERVER=%SERVER% +# zone for the vms +FZONE=vm.%DOMAIN% +# keyfile path to pass to nsupdate with -k option +# see man page for more info +KEYFILE=%KEYFILE% diff --git a/snf-deploy/files/etc/default/vncauthproxy b/snf-deploy/files/etc/default/vncauthproxy new file mode 100644 index 0000000000000000000000000000000000000000..cd6541c7242baa3fdb8392cb6116aa48e565e0bd --- /dev/null +++ b/snf-deploy/files/etc/default/vncauthproxy @@ -0,0 +1,6 @@ +# set uid/gid +#CHUID="vncauthproxy:vncauthproxy" +# Arguments passed to vncauthproxy +#DAEMON_OPTS="--pid-file=$PIDFILE" + +DAEMON_OPTS="--pid-file=$PIDFILE --enable-ssl --listen-address=%VNC% --proxy-listen-address=%VNC%" diff --git a/snf-deploy/files/etc/ganeti/file-storage-paths b/snf-deploy/files/etc/ganeti/file-storage-paths new file mode 100644 index 0000000000000000000000000000000000000000..cd157236f38a00f996b5f38712aee3b0222636e2 --- /dev/null +++ b/snf-deploy/files/etc/ganeti/file-storage-paths @@ -0,0 +1,2 @@ +%SHARED_GANETI_DIR%/file-storage +%SHARED_GANETI_DIR%/shared-file-storage diff --git a/snf-deploy/files/etc/gunicorn.d/synnefo b/snf-deploy/files/etc/gunicorn.d/synnefo index aa2eec8231e074497b02373b2946ad4c1a801a75..29d7745bed0c6fddc41175025ac74685130db512 100644 --- a/snf-deploy/files/etc/gunicorn.d/synnefo +++ b/snf-deploy/files/etc/gunicorn.d/synnefo @@ -4,13 +4,14 @@ CONFIG = { 'DJANGO_SETTINGS_MODULE': 'synnefo.settings', }, 'working_dir': '/etc/synnefo', - 'user': 'www-data', - 'group': 'www-data', + 'user': 'synnefo', + 'group': 'synnefo', 'args': ( '--bind=127.0.0.1:8080', '--workers=8', '--worker-class=gevent', # '--worker-class=sync', '--log-level=debug', + '--log-file=/var/log/synnefo/gunicorn.log', ), } diff --git a/snf-deploy/files/etc/gunicorn.d/synnefo-archip b/snf-deploy/files/etc/gunicorn.d/synnefo-archip new file mode 100644 index 0000000000000000000000000000000000000000..4930ca3c0b0ce486382288169a8cf77a2ad6846a --- /dev/null +++ b/snf-deploy/files/etc/gunicorn.d/synnefo-archip @@ -0,0 +1,18 @@ +CONFIG = { + 'mode': 'django', + 'environment': { + 'DJANGO_SETTINGS_MODULE': 'synnefo.settings', + }, + 'working_dir': '/etc/synnefo', + 'user': 'synnefo', + 'group': 'synnefo', + 'args': ( + '--bind=127.0.0.1:8080', + '--workers=6', + '--worker-class=gevent', + '--config=/etc/synnefo/gunicorn-hooks/gunicorn-archipelago.py', + # '--worker-class=sync', + '--log-level=debug', + '--log-file=/var/log/synnefo/gunicorn.log', + ), +} diff --git a/snf-deploy/files/etc/modprobe.d/drbd.conf b/snf-deploy/files/etc/modprobe.d/drbd.conf new file mode 100644 index 0000000000000000000000000000000000000000..c91b9bac8264bbb6f60f79894d73b82c2b698145 --- /dev/null +++ b/snf-deploy/files/etc/modprobe.d/drbd.conf @@ -0,0 +1 @@ +options drbd minor_count=255 usermode_helper=/bin/true diff --git a/snf-deploy/files/etc/rc.local b/snf-deploy/files/etc/rc.local new file mode 100644 index 0000000000000000000000000000000000000000..666245bc63d980072bf2cd28b22aa0fe65118e23 --- /dev/null +++ b/snf-deploy/files/etc/rc.local @@ -0,0 +1,15 @@ +#!/bin/bash + +brctl addbr %COMMON_BRIDGE% +ip link set %COMMON_BRIDGE% up + +iptables -t mangle -A PREROUTING -i %COMMON_BRIDGE% -p udp -m udp --dport 67 -j NFQUEUE --queue-num 42 + +if [ %ROUTER_IP% == %NODE_IP% ]; then + iptables -t nat -A POSTROUTING -o %PUBLIC_IFACE% -s %SUBNET% -j MASQUERADE + echo 1 > /proc/sys/net/ipv4/ip_forward + ip addr add %GATEWAY% dev %COMMON_BRIDGE% + ip route add %SUBNET% dev %COMMON_BRIDGE% src %GATEWAY% +fi + +exit 0 diff --git a/snf-deploy/files/etc/resolv.conf b/snf-deploy/files/etc/resolv.conf index d2f784fb4df9a694d4c99d1294bf7ca7ace8478f..681008917a64aae51fa27632deeaac4c2520d825 100644 --- a/snf-deploy/files/etc/resolv.conf +++ b/snf-deploy/files/etc/resolv.conf @@ -1,3 +1,7 @@ +# This has been generated automatically by snf-deploy, at %DATE% +# The immutable bit (+i attribute) has been used to avoid it being +# overwritten by software such as NetworkManager or resolvconf. +# Use lsattr/chattr to view or modify its file attributes. domain %DOMAIN% search %DOMAIN% nameserver %NS_NODE_IP% diff --git a/snf-deploy/files/etc/synnefo/admin.conf b/snf-deploy/files/etc/synnefo/admin.conf new file mode 100644 index 0000000000000000000000000000000000000000..e4587c56af21e8d8a520e036a9db7f7014c957a7 --- /dev/null +++ b/snf-deploy/files/etc/synnefo/admin.conf @@ -0,0 +1 @@ +ADMIN_BASE_URL = 'https://%ADMIN%/admin' diff --git a/snf-deploy/files/etc/synnefo/astakos.conf b/snf-deploy/files/etc/synnefo/astakos.conf index 30e74c1716a4ba368f1b989f5e14ed0365f38aa3..604eb6d587e6186754dda9c0bc204536a80a5e3a 100644 --- a/snf-deploy/files/etc/synnefo/astakos.conf +++ b/snf-deploy/files/etc/synnefo/astakos.conf @@ -2,7 +2,7 @@ CLOUDBAR_LOCATION = 'https://%ACCOUNTS%/static/im/cloudbar/' CLOUDBAR_SERVICES_URL = 'https://%ACCOUNTS%/astakos/ui/get_services' CLOUDBAR_MENU_URL = 'https://%ACCOUNTS%/astakos/ui/get_menu' -ASTAKOS_DEFAULT_FROM_EMAIL = 'okeanos feedback@%DOMAIN%>' +ASTAKOS_DEFAULT_FROM_EMAIL = 'synnefo_feedback@%DOMAIN%>' ASTAKOS_DEFAULT_CONTACT_EMAIL = 'feedback@%DOMAIN%' ASTAKOS_DEFAULT_ADMIN_EMAIL = 'feedback@%DOMAIN%' @@ -10,7 +10,7 @@ ASTAKOS_IM_MODULES = ['local'] ASTAKOS_BASE_URL = 'https://%ACCOUNTS%/astakos' -ASTAKOS_SITENAME = '~okeanos' +ASTAKOS_SITENAME = 'Synnefo' ASTAKOS_RECAPTCHA_PUBLIC_KEY = '6LeFidMSAAAAAM7Px7a96YQzsBcKYeXCI_sFz0Gk' ASTAKOS_RECAPTCHA_PRIVATE_KEY = '6LeFidMSAAAAAFv5U5NSayJJJhr0roludAidPd2M' diff --git a/snf-deploy/files/etc/synnefo/backend.conf b/snf-deploy/files/etc/synnefo/backend.conf new file mode 100644 index 0000000000000000000000000000000000000000..37b401504f0bf5993c116de37953d840080c076a --- /dev/null +++ b/snf-deploy/files/etc/synnefo/backend.conf @@ -0,0 +1,2 @@ +PITHOS_BACKEND_DB_CONNECTION = 'postgresql://%SYNNEFO_USER%:%SYNNEFO_DB_PASSWD%@%DB_NODE%:5432/snf_pithos' +PITHOS_BACKEND_QUOTA = 20 * 1024 * 1024 * 1024 diff --git a/snf-deploy/files/etc/synnefo/cyclades.conf b/snf-deploy/files/etc/synnefo/cyclades.conf index a2666964619f51f2e4da00a4b4c7344844caf4ad..e760a63e33bf2366e4afa742266d6cae9f3aa6ae 100644 --- a/snf-deploy/files/etc/synnefo/cyclades.conf +++ b/snf-deploy/files/etc/synnefo/cyclades.conf @@ -2,18 +2,15 @@ MAX_CIDR_BLOCK = 21 PUBLIC_USE_POOL = True DEFAULT_MAC_FILTERED_BRIDGE = '%COMMON_BRIDGE%' -CUSTOM_BRIDGED_BRIDGE = '%COMMON_BRIDGE%' +DEFAULT_BRIDGE = '%COMMON_BRIDGE%' MAX_VMS_PER_USER = 5 VMS_USER_QUOTA = { - 'psomas@grnet.gr': 1000, - 'cstavr@grnet.gr':1000, - 'gmytil@cslab.ntua.gr': 20, - 'ananos@cslab.ece.ntua.gr': 20, - 'vkoukis@grnet.gr': 400 + 'user@synnefo.org': 10, + 'johndoe@synnefo.org': 5 } MAX_NETWORKS_PER_USER = 3 -NETWORKS_USER_QUOTA = { 'psomas@grnet.gr': 1000 } +NETWORKS_USER_QUOTA = { 'user@synnefo.org': 10 } CPU_BAR_GRAPH_URL = 'https://%STATS%/stats/v1.0/cpu-bar/%s' CPU_TIMESERIES_GRAPH_URL = 'https://%STATS%/stats/v1.0/cpu-ts/%s' NET_BAR_GRAPH_URL = 'https://%STATS%/stats/v1.0/net-bar/%s' @@ -22,7 +19,7 @@ GANETI_DISK_TEMPLATES = ('blockdev', 'diskless', 'drbd', 'file', 'plain', 'rbd', 'sharedfile', 'ext') ASTAKOS_AUTH_URL = 'https://%ACCOUNTS%/astakos/identity/v2.0' -SECRET_ENCRYPTION_KEY= "oEs0pt7Di1mkxA0P6FiK" +SECRET_ENCRYPTION_KEY= "%CYCLADES_SECRET%" GANETI_CREATEINSTANCE_KWARGS = { 'os': 'snf-image+default', @@ -38,7 +35,6 @@ CLOUDBAR_ACTIVE_SERVICE = '2' CLOUDBAR_SERVICES_URL = 'https://%ACCOUNTS%/astakos/ui/get_services' CLOUDBAR_MENU_URL = 'https://%ACCOUNTS%/astakos/ui/get_menu' BACKEND_DB_CONNECTION = 'postgresql://%SYNNEFO_USER%:%SYNNEFO_DB_PASSWD%@%DB_NODE%:5432/snf_pithos' -BACKEND_BLOCK_PATH = '%PITHOS_DIR%/data/' AMQP_HOSTS = ["amqp://%SYNNEFO_USER%:%SYNNEFO_RABBITMQ_PASSWD%@%MQ_NODE%:5672"] @@ -82,13 +78,34 @@ UI_SYSTEM_IMAGES_OWNERS = { CYCLADES_BASE_URL = 'https://%CYCLADES%/cyclades' -CYCLADES_VNCAUTHPROXY_OPTS = { - 'auth_user': 'synnefo', - 'auth_password': 'synnefo_vnc_pass', +CYCLADES_VNCAUTHPROXY_OPTS = [ + { + 'auth_user': '%SYNNEFO_USER%', + 'auth_password': '%SYNNEFO_VNC_PASSWD%', + 'server_address': '%VNC%', + 'server_port': 24999, + 'enable_ssl': True, + 'ca_cert': '/etc/ssl/certs/synnefo_ca.pem', + 'strict': True, + } +] + +CYCLADES_STATS_SECRET_KEY = "%STATS_SECRET%" + +GANETI_DISK_PROVIDER_KWARGS = { + 'archipelago_cached': { + 'cache':'writeback', + 'heads':'16', + 'secs':'63', + 'provider':'archipelago' + }, + 'shared-filer': { + 'shared_dir': '%SHARED_GANETI_DIR%/shared-file-storage' + } } -CYCLADES_STATS_SECRET_KEY = "random" - -# IP and not fqdn because java VncViewer class used for machine's console -# has an issue with self-signed certificates -UI_MEDIA_URL = "https://%CYCLADES_NODE_IP%/static/ui/static/snf/" +GANETI_CLONE_PROVIDERS = [ + 'vlmc', + 'archipelago', + 'archipelago_cached' + ] diff --git a/snf-deploy/files/etc/synnefo/gunicorn-hooks/gunicorn-archipelago.py b/snf-deploy/files/etc/synnefo/gunicorn-hooks/gunicorn-archipelago.py new file mode 100644 index 0000000000000000000000000000000000000000..15cf374660147b5b634360a65a6be7fd89cc789c --- /dev/null +++ b/snf-deploy/files/etc/synnefo/gunicorn-hooks/gunicorn-archipelago.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 - +# +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +from pithos.workers import glue +from multiprocessing import Lock +import mmap +import pickle +import os + +SYNNEFO_UMASK=0o007 + +def find_hole(workers, fworkers): + old_key = [] + old_age = [] + for key in fworkers: + if key not in workers.keys(): + old_age.append(fworkers[key]) + old_key.append(key) + break + if len(old_age) and len(old_key): + for key in old_key: + del fworkers[key] + return old_age + return old_age + + +def follow_workers(pid, wid, server): + hole = None + fd = server.state_fd + fd.seek(0) + f = pickle.load(fd) + hole = find_hole(server.WORKERS, f) + if len(hole) > 0: + k = {pid: int(hole[0])} + else: + k = {pid: wid} + f.update(k) + fd.seek(0) + pickle.dump(f, fd) + return hole + + +def allocate_wid(pid, wid, server): + hole = None + hole = follow_workers(pid, wid, server) + return hole + + +def when_ready(server): + server.lock = Lock() + server.state_fd = mmap.mmap(-1, 4096) + pickle.dump({}, server.state_fd) + + +def update_workers(pid, wid, server): + fd = server.state_fd + fd.seek(0) + f = pickle.load(fd) + for k, v in f.items(): + if wid == v: + del f[k] + break + k = {pid: wid} + f.update(k) + fd.seek(0) + pickle.dump(f, fd) + + +def post_fork(server, worker): + # set umask for the gunicorn worker + os.umask(SYNNEFO_UMASK) + server.lock.acquire() + if server.worker_age <= server.num_workers: + update_workers(worker.pid, server.worker_age, server) + glue.WorkerGlue.setmap(worker.pid, server.worker_age) + else: + wid = allocate_wid(worker.pid, server.worker_age, server) + glue.WorkerGlue.setmap(worker.pid, wid[0]) + server.lock.release() + + +def worker_exit(server, worker): + if glue.WorkerGlue.ioctx_pool: + glue.WorkerGlue.ioctx_pool._shutdown_pool() + + +def on_exit(server): + server.state_fd.close() + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/snf-deploy/files/etc/synnefo/pithos.conf b/snf-deploy/files/etc/synnefo/pithos.conf index 4f4825af2bfd33deaaab348216a8d9e9e719aae6..c35e825b28e4a21b9f7fe3651105f5620f6bd61b 100644 --- a/snf-deploy/files/etc/synnefo/pithos.conf +++ b/snf-deploy/files/etc/synnefo/pithos.conf @@ -1,8 +1,5 @@ PITHOS_AUTHENTICATION_URL = 'https://%ACCOUNTS%/im/authenticate' PITHOS_AUTHENTICATION_USERS = None -PITHOS_BACKEND_DB_CONNECTION = 'postgresql://%SYNNEFO_USER%:%SYNNEFO_DB_PASSWD%@%DB_NODE%:5432/snf_pithos' -PITHOS_BACKEND_BLOCK_PATH = '%PITHOS_DIR%/data' -PITHOS_BACKEND_QUOTA = 20 * 1024 * 1024 * 1024 PITHOS_UPDATE_MD5 = False PITHOS_SERVICE_TOKEN = '%PITHOS_SERVICE_TOKEN%' diff --git a/snf-deploy/files/etc/synnefo/stats.conf b/snf-deploy/files/etc/synnefo/stats.conf index 588849fe54e35325abef3ad71feee5f00f6e44dc..6ea829a4312c4302c7af627541ff7adb790458fb 100644 --- a/snf-deploy/files/etc/synnefo/stats.conf +++ b/snf-deploy/files/etc/synnefo/stats.conf @@ -1,2 +1,2 @@ STATS_BASE_URL = "https://%STATS%/stats/" -STATS_SECRET_KEY = "random" +STATS_SECRET_KEY = "%STATS_SECRET%" diff --git a/snf-deploy/files/etc/synnefo/webproject.conf b/snf-deploy/files/etc/synnefo/webproject.conf index f2dfba220c95cc6b7fff6fb5970947e6b135a2c1..a466b22127b9d786dbcabca44bcfc3e2374813d1 100644 --- a/snf-deploy/files/etc/synnefo/webproject.conf +++ b/snf-deploy/files/etc/synnefo/webproject.conf @@ -23,7 +23,7 @@ DATABASES = { } } -SECRET_KEY = 'ly6)mw6a7x%n)-e#zzk4jo6f2=uqu!1o%)2-(7lo+f9yd^k^bg' +SECRET_KEY = '%WEBPROJECT_SECRET%' USE_X_FORWARDED_HOST = True SESSION_COOKIE_DOMAIN = "%DOMAIN%" diff --git a/snf-deploy/files/etc/sysctl.d/disable-ipv6.conf b/snf-deploy/files/etc/sysctl.d/disable-ipv6.conf new file mode 100644 index 0000000000000000000000000000000000000000..edca4b6ca81483363ce13dd7277b5539435d8a07 --- /dev/null +++ b/snf-deploy/files/etc/sysctl.d/disable-ipv6.conf @@ -0,0 +1,4 @@ +# This has been generated automatically by snf-deploy, at %DATE% +net.ipv6.conf.all.disable_ipv6 = 1 +net.ipv6.conf.default.disable_ipv6 = 1 +net.ipv6.conf.lo.disable_ipv6 = 1 diff --git a/snf-deploy/files/root/ca/ca-x509-extensions.cnf b/snf-deploy/files/root/ca/ca-x509-extensions.cnf new file mode 100644 index 0000000000000000000000000000000000000000..6548afa26a23aaf2653f6e55f8746db26c24e961 --- /dev/null +++ b/snf-deploy/files/root/ca/ca-x509-extensions.cnf @@ -0,0 +1,9 @@ +# x509v3 extenstions to add when creating the root CA +# This is a CA's root certificate +basicConstraints = critical, CA:TRUE +# The key of this certificate will be used for signing other certificates +keyUsage = keyCertSign, cRLSign +# Follow the guidelines in RFC3280 +subjectKeyIdentifier = hash +# This certificate will be used for signing certificates with the following CN +nameConstraints = permitted;DNS:%DOMAIN% diff --git a/snf-deploy/files/root/ca/x509-extensions.cnf b/snf-deploy/files/root/ca/x509-extensions.cnf new file mode 100644 index 0000000000000000000000000000000000000000..2c3aa0a4a18ce8f919824365a651b82a19c265e5 --- /dev/null +++ b/snf-deploy/files/root/ca/x509-extensions.cnf @@ -0,0 +1,7 @@ +# x509v3 extenstions to add when creating the synnefo certificate +# This is certificate and not a CA +basicConstraints = CA:FALSE +# The certificate is valid for the CN but also for other alternative names +subjectAltName = DNS:%DOMAIN%,DNS:*.%DOMAIN% +# The certificate will be used for server authentication (e.g. apache2) +extendedKeyUsage = serverAuth diff --git a/snf-deploy/files/root/create_root_ca.sh b/snf-deploy/files/root/create_root_ca.sh new file mode 100644 index 0000000000000000000000000000000000000000..e4de825575f7be39b34755a80b345547bb5ae11e --- /dev/null +++ b/snf-deploy/files/root/create_root_ca.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +# THIS SCRIPT CREATES A CA AND SIGNES A CERTIFICATE TO BE USED +# FOR THE SYNNEFO INSTALLATION. IT FOLLOWS INSTRUCTIONS FROM: +# https://wiki.mozilla.org/SecurityEngineering/x509Certs#Running_your_Own_CA + +DIR=/root/ca + +ROOT_CA_KEY=$DIR/cakey.pem +ROOT_CA_CSR=$DIR/cacert.csr +ROOT_CA_CERT=$DIR/cacert.pem +KEY=$DIR/key.pem +CSR=$DIR/cert.csr +CERT=$DIR/cert.pem +ROOT_CNF=$DIR/ca-x509-extensions.cnf +CNF=$DIR/x509-extensions.cnf + +mkdir -p $DIR + +echo [$ROOT_CA_KEY] Generating private key for root CA... +openssl genpkey -algorithm RSA -out $ROOT_CA_KEY -pkeyopt rsa_keygen_bits:4096 + +echo [$ROOT_CA_CSR] Generating certificate request for root CA... +openssl req -new -key $ROOT_CA_KEY -days 5480 -extensions v3_ca -batch \ + -out $ROOT_CA_CSR -utf8 -subj '/C=GR/O=Synnefo/OU=SynnefoCloudSoftware' + +echo [$ROOT_CA_CERT] Generating certificate for root CA... +openssl x509 -req -sha256 -days 3650 -in $ROOT_CA_CSR -signkey $ROOT_CA_KEY \ + -set_serial 1 -extfile $ROOT_CNF -out $ROOT_CA_CERT + + + +echo [$KEY] Generating private key for services... +openssl genpkey -algorithm RSA -out $KEY -pkeyopt rsa_keygen_bits:2048 + +echo [$CSR] Generating certificate request for services... +openssl req -new -key $KEY -days 1096 -extensions v3_ca -batch \ + -out $CSR -utf8 -subj '/OU=SynnefoCloudServices/CN=synnefo.live' + +echo [$CERT] Generating certificate for services... +openssl x509 -req -sha256 -days 1096 -in $CSR \ + -CAkey $ROOT_CA_KEY -CA $ROOT_CA_CERT -set_serial 100 \ + -out $CERT -extfile $CNF diff --git a/snf-deploy/files/root/firefox_cert_override.py b/snf-deploy/files/root/firefox_cert_override.py new file mode 100755 index 0000000000000000000000000000000000000000..246a822cb9770281ceadb19b5445cb7e606fcf3a --- /dev/null +++ b/snf-deploy/files/root/firefox_cert_override.py @@ -0,0 +1,67 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import OpenSSL +import sys +import base64 + + +def cert_override(cert_contents, domain): + """ + Generate a certificate exception entry. The result can be appended in + `cert_override.txt`. + + https://developer.mozilla.org/en-US/docs/Cert_override.txt + """ + cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, + cert_contents) + + tpl = "%(domain)s\t%(oid)s\t%(cert_hash)s" + \ + "\t%(options)s\t%(serial_hash)s\t%(issuer_hash)s" + + oid = "OID.2.16.840.1.101.3.4.2.1" + cert_hash = cert.digest("sha256") + options = "MU" + + serial = ("%x" % cert.get_serial_number()).decode("hex") + issuer_parts = cert.get_issuer().der().split(".") + issuer_name = "".join(issuer_parts[:-2]) + issuer_domain = ".".join(issuer_parts[-2:]) + + serial_prefix = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\t" + \ + "\x00\x00\x00$\x00" + serial_content = serial_prefix + serial + issuer_name + serial_hash = base64.b64encode(serial_content) + issuer_hash = base64.b64encode(issuer_domain) + + + return tpl % { + 'domain': domain, + 'oid': oid, + 'cert_hash': cert_hash, + 'options': options, + 'serial_hash': serial_hash, + 'issuer_hash': issuer_hash + } + + +if __name__ == "__main__": + try: + cert_path, domain = sys.argv[1], sys.argv[2] + except IndexError: + print "Usage: %s <cert_path> <domain>" % (sys.argv[0]) + exit(1) + + print cert_override(file(cert_path).read(), domain) diff --git a/snf-deploy/files/root/qa-sample.json b/snf-deploy/files/root/qa-sample.json new file mode 100644 index 0000000000000000000000000000000000000000..850767ac9cf3b5cb185cf743ade671102f45b2da --- /dev/null +++ b/snf-deploy/files/root/qa-sample.json @@ -0,0 +1,242 @@ +{ + "# Note:": null, + "# This file is stored in the JSON format and does not support": null, + "# comments. As a work-around, comments are keys starting with a hash": null, + "# sign (#).": null, + + "name": "%CLUSTER_NAME%", + + "# Name used for renaming cluster": null, + "rename": "ganeti1-rename", + + "# Virtual cluster": null, + "#vcluster-master": "xen-vcluster", + "#vcluster-basedir": "/srv/ganeti/vcluster", + + "enabled-hypervisors": "kvm", + "# Dict of hypervisor name and parameters (like on the cmd line)": null, + "hypervisor-parameters": {"kvm": "kernel_path=,vnc_bind_address=0.0.0.0", "xen-pvm": "kernel_path=/boot/vmlinuz-3.2.0-4-amd64", "xen-hvm": "kernel_path=/boot/vmlinuz-3.2.0-4-amd64"}, + "# Backend parameters (like on the cmd line)": null, + "backend-parameters": "", + "# Dict of OS name and parameters (like on the cmd line)": null, + "os-parameters": {}, + "# Dict of OS name and value dict of hypervisor parameters": null, + "os-hvp": {}, + "primary_ip_version": 4, + "# Name of the LVM group for the cluster": null, + "vg-name": "%VG%", + "# Cluster-level value of the exclusive-storage flag": null, + "exclusive-storage": null, + + "# Only enable disk templates that the QA machines can actually use.": null, + "enabled-disk-templates": [ + "plain", + "drbd", + "diskless" + ], + + "# Additional arguments for initializing cluster": null, + "cluster-init-args": [], + + "# Network interface for master role": null, + "master-netdev": "%CLUSTER_NETDEV%", + + "# Default network interface parameters": null, + "default-nicparams": { + "mode": "bridged", + "link": "br0" + }, + + "os": "debootstrap+default", + "maxmem": "1024M", + "minmem": "512M", + + "# Instance policy specs": null, + "#ispec_cpu_count_max": null, + "#ispec_cpu_count_min": null, + "#ispec_cpu_count_std": null, + "#ispec_disk_count_max": null, + "#ispec_disk_count_min": null, + "#ispec_disk_count_std": null, + "#ispec_disk_size_max": null, + "ispec_disk_size_min": 512, + "#ispec_disk_size_std": null, + "ispec_mem_size_max": 1024, + "#ispec_mem_size_min": null, + "#ispec_mem_size_std": null, + "#ispec_nic_count_max": null, + "#ispec_nic_count_min": null, + "#ispec_nic_count_std": null, + + "# Lists of disks": null, + "disks": [ + { + "size": "1G", + "name": "disk0", + "growth": "2G" + }, + { + "size": "512M", + "name": "disk1", + "growth": "768M" + } + ], + + "# Script to check instance status": null, + "instance-check": null, + + "# Regular expression to ignore existing tags": null, + "ignore-tags-re": null, + + "nodes": %NODES%, + + "instances": [ + { + "name": "xen-test-inst1.%DOMAIN%", + + "# Static MAC address": null, + "#nic.mac/0": "AA:00:00:11:11:11" + }, + { + "name": "xen-test-inst2.%DOMAIN%", + + "# Static MAC address": null, + "#nic.mac/0": "AA:00:00:22:22:22" + }, + { + "name": "xen-test-inst3.%DOMAIN%", + + "# Static MAC address": null, + "#nic.mac/0": "AA:00:00:22:22:22" + } + ], + + "groups": { + "group-with-nodes": "default", + "inexistent-groups": [ + "group1", + "group2", + "group3" + ] + }, + + "networks": { + "inexistent-networks": [ + "network1", + "network2", + "network3" + ] + }, + + "tests": { + "# Whether tests are enabled or disabled by default": null, + "default": true, + + "env": true, + "os": true, + "tags": true, + "rapi": true, + "test-jobqueue": true, + "delay": true, + + "create-cluster": false, + "cluster-verify": true, + "cluster-info": true, + "cluster-burnin": true, + "cluster-command": true, + "cluster-copyfile": true, + "cluster-master-failover": true, + "cluster-renew-crypto": true, + "cluster-destroy": true, + "cluster-rename": false, + "cluster-reserved-lvs": true, + "cluster-modify": true, + "cluster-oob": true, + "cluster-epo": true, + "cluster-redist-conf": true, + "cluster-repair-disk-sizes": true, + "cluster-exclusive-storage": true, + "cluster-instance-policy": true, + + "haskell-confd": true, + "htools": true, + + "group-list": true, + "group-rwops": true, + + "network": false, + + "node-list": true, + "node-info": true, + "node-volumes": true, + "node-readd": true, + "node-storage": true, + "node-modify": true, + "node-oob": true, + + "# This test needs at least three nodes": null, + "node-evacuate": false, + + "# This test needs at least two nodes": null, + "node-failover": false, + + "instance-add-plain-disk": true, + "instance-add-file": true, + "instance-add-drbd-disk": true, + "instance-add-diskless": true, + "instance-add-restricted-by-disktemplates": true, + "instance-convert-disk": true, + "instance-plain-rapi-common-tests": true, + "instance-remove-drbd-offline": true, + + "instance-export": true, + "instance-failover": true, + "instance-grow-disk": true, + "instance-import": true, + "instance-info": true, + "instance-list": true, + "instance-migrate": true, + "instance-modify": true, + "instance-modify-primary": true, + "instance-modify-disks": false, + "instance-reboot": true, + "instance-reinstall": true, + "instance-rename": false, + "instance-shutdown": true, + "instance-device-names": true, + + "job-list": true, + + "# cron/ganeti-watcher should be disabled for these tests": null, + "instance-automatic-restart": false, + "instance-consecutive-failures": false, + + "# This test might fail with certain hypervisor types, depending": null, + "# on whether they support the `gnt-instance console' command.": null, + "instance-console": false, + + "# Disabled by default because they take rather long": null, + "instance-replace-disks": false, + "instance-recreate-disks": false, + + "# Whether to test the tools/move-instance utility": null, + "inter-cluster-instance-move": false, + + "# Run instance tests with different cluster configurations": null, + "default-instance-tests": true, + "exclusive-storage-instance-tests": false + }, + + "options": { + "burnin-instances": 2, + "burnin-disk-template": "drbd", + "burnin-in-parallel": false, + "burnin-check-instances": false, + "burnin-rename": "ganeti1-rename", + "burnin-reboot": true, + "reboot-types": ["soft", "hard", "full"], + "use-iallocators": false + }, + + "# vim: set syntax=javascript :": null +} diff --git a/snf-deploy/files/tmp/configure-2.10 b/snf-deploy/files/tmp/configure-2.10 new file mode 100755 index 0000000000000000000000000000000000000000..6379dae32f984bd8223c9ed695c1227d02ce732c --- /dev/null +++ b/snf-deploy/files/tmp/configure-2.10 @@ -0,0 +1,12 @@ +./configure \ + --prefix=/usr \ + --localstatedir=/var \ + --sysconfdir=/etc \ + --with-export-dir=/var/lib/ganeti/export \ + --with-iallocator-search-path=/usr/local/lib/ganeti/iallocators,/usr/lib/ganeti/iallocators \ + --with-os-search-path=/srv/ganeti/os,/usr/local/lib/ganeti/os,/usr/lib/ganeti/os,/usr/share/ganeti/os \ + --with-extstorage-search-path=/srv/ganeti/extstorage,/usr/local/lib/ganeti/extstorage,/usr/lib/ganeti/extstorage,/usr/share/ganeti/extstorage +\ + --docdir=/usr/share/doc/ganeti \ + --disable-symlinks + diff --git a/snf-deploy/files/tmp/configure-2.8 b/snf-deploy/files/tmp/configure-2.8 new file mode 100755 index 0000000000000000000000000000000000000000..ab2141b82f3fa9d25153cd20538abfffa305ccc3 --- /dev/null +++ b/snf-deploy/files/tmp/configure-2.8 @@ -0,0 +1,12 @@ +./configure \ + --prefix=/usr \ + --localstatedir=/var \ + --sysconfdir=/etc \ + --with-export-dir=/var/lib/ganeti/export \ + --with-iallocator-search-path=/usr/local/lib/ganeti/iallocators,/usr/lib/ganeti/iallocators \ + --with-os-search-path=/srv/ganeti/os,/usr/local/lib/ganeti/os,/usr/lib/ganeti/os,/usr/share/ganeti/os \ + --with-extstorage-search-path=/srv/ganeti/extstorage,/usr/local/lib/ganeti/extstorage,/usr/lib/ganeti/extstorage,/usr/share/ganeti/extstorage +\ + --docdir=/usr/share/doc/ganeti \ + --enable-htools-rapi + diff --git a/snf-deploy/files/tmp/exports b/snf-deploy/files/tmp/exports deleted file mode 100644 index c2328a64630b93d30751df42c12d990a49442d09..0000000000000000000000000000000000000000 --- a/snf-deploy/files/tmp/exports +++ /dev/null @@ -1,3 +0,0 @@ -%PITHOS_DIR% %IP%(rw,async,no_subtree_check,no_root_squash) -%IMAGE_DIR% %IP%(rw,async,no_subtree_check,no_root_squash) - diff --git a/snf-deploy/files/tmp/page.json b/snf-deploy/files/tmp/page.json index 8cc794d090ea281ea766cc12db9119b2be06d476..f460fb43df17ec3098437ea5cf9dac85101035ad 100644 --- a/snf-deploy/files/tmp/page.json +++ b/snf-deploy/files/tmp/page.json @@ -1,7 +1,7 @@ [ { "fields": { - "_cached_url": "/", + "_cached_url": "/home/", "_content_title": "", "_page_title": "", "active": true, @@ -14,17 +14,17 @@ "meta_keywords": "", "modification_date": "2012-11-16 14:52:19", "navigation_extension": null, - "override_url": "/", + "override_url": "/home/", "parent": null, "publication_date": "2012-11-16 14:50:00", "publication_end_date": null, "redirect_to": "", "rght": 2, "site": 1, - "slug": "okeanos", + "slug": "synnefo", "symlinked_page": null, "template_key": "twocolwide", - "title": "Okeanos", + "title": "Synnefo", "translation_of": null, "tree_id": 1 }, @@ -36,7 +36,7 @@ "ordering": 0, "parent": 1, "region": "main", - "text": "Welcome to Okeanos!!\r\n\r\n" + "text": "Welcome to Synnefo!!\r\n\r\n" }, "model": "page.rawcontent", "pk": 1 diff --git a/snf-deploy/files/tmp/sites.json b/snf-deploy/files/tmp/sites.json index 11d18d327288c26a24b218cea8e4f2f4b4b12a3b..a62fc0bf5e81f227d5dccaf168de1c810998037f 100644 --- a/snf-deploy/files/tmp/sites.json +++ b/snf-deploy/files/tmp/sites.json @@ -3,8 +3,8 @@ "pk": 1, "model": "sites.site", "fields": { - "domain": "okeanos.grnet.gr", - "name": "okeanos.grnet.gr" + "domain": "%DOMAIN%", + "name": "%DOMAIN%" } } ] diff --git a/snf-deploy/scripts/dhclient-hostname b/snf-deploy/scripts/dhclient-hostname new file mode 100755 index 0000000000000000000000000000000000000000..9a97dc4d8f299660c684b44f047d0acd44d70fd7 --- /dev/null +++ b/snf-deploy/scripts/dhclient-hostname @@ -0,0 +1,29 @@ +#!/bin/sh + +# Filename: /etc/dhcp3/dhclient-exit-hooks.d/hostname +# Purpose: Used by dhclient-script to set the hostname of the system +# to match the DNS information for the host as provided by +# DHCP. +# Depends: dhcp3-client (should be in the base install) +# hostname (for hostname, again, should be in the base) +# bind9-host (for host) +# coreutils (for cut and echo) +# + +if [ "$reason" != BOUND ] && [ "$reason" != RENEW ] \ + && [ "$reason" != REBIND ] && [ "$reason" != REBOOT ] +then + return +fi + +echo dhclient-exit-hooks.d/hostname: Dynamic IP address = $new_ip_address + +hostname=$(host $new_ip_address | sed 's/.$//' | cut -d ' ' -f 5) + +echo $hostname > /etc/hostname + +hostname $hostname + +echo dhclient-exit-hooks.d/hostname: Dynamic Hostname = $hostname + +# And that _should_ just about do it... diff --git a/snf-deploy/scripts/mkimage.sh b/snf-deploy/scripts/mkimage.sh new file mode 100755 index 0000000000000000000000000000000000000000..14527627450b3c86a1dd0dcc8534ec3a241b50a7 --- /dev/null +++ b/snf-deploy/scripts/mkimage.sh @@ -0,0 +1,96 @@ +#!/bin/bash -x + +DISTRO=wheezy +MIRROR=http://ftp.gr.debian.org/debian +EXTRA_PACKAGES=acpi-support-base,console-tools,udev,linux-image-amd64,network-manager,openssh-server,locales + +: ${DISK0:=/var/lib/snf-deploy/vcluster/disk0} +: ${DISK1:=/var/lib/snf-deploy/vcluster/disk1} +: ${HOSTNAME:=vc} +: ${DISK0_SIZE:=10G} +: ${DISK1_SIZE:=30G} +: ${DISTRO:=wheezy} +: ${MIRROR:=http://ftp.gr.debian.org/debian} +: ${EXTRA_PACKAGES:=acpi-support-base,console-tools,udev,linux-image-amd64,network-manager} +: ${NMHOOK:=etc/NetworkManager/dispatcher.d/02hostname} + +set -e + +truncate -s $DISK0_SIZE $DISK0 +truncate -s $DISK1_SIZE $DISK1 + +# sfdisk -H 255 -S 63 -u S --quiet --Linux $DISK0 <<EOF +# 2048,,L,* +# EOF + +/sbin/parted -s $DISK0 mklabel msdos +/sbin/parted -s $DISK0 mkpart primary ext3 2048s 100% +/sbin/parted -s $DISK0 set 1 boot on + +BLOCKDEV=$(losetup -f --show $DISK0) + +PARTITION=$(kpartx -l $BLOCKDEV | awk '{print $1}') + +kpartx -a $BLOCKDEV + +FSDEV=/dev/mapper/$PARTITION + +mkfs.ext3 $FSDEV + +TEMP=$(mktemp -d) + +mount $FSDEV $TEMP + +debootstrap --include $EXTRA_PACKAGES $DISTRO $TEMP $MIRROR + +BLKID=$(blkid -o value -s UUID $FSDEV) + +cat >> $TEMP/etc/fstab <<EOF +proc /proc proc defaults 0 0 +UUID=$BLKID / ext3 defaults 0 1 +EOF + +echo $HOSTNAME > $TEMP/etc/hostname + +echo "T0:23:respawn:/sbin/getty ttyS0 38400" >> $TEMP/etc/inittab + +mkdir -p $TEMP/$(dirname $NMHOOK) + +cat >> $TEMP/$NMHOOK <<EOF +#!/bin/bash + +if [ -n "\$DHCP4_HOST_NAME" ]; then + echo \$DHCP4_HOST_NAME > /etc/hostname + hostname \$DHCP4_HOST_NAME +fi +EOF + +chmod +x $TEMP/$NMHOOK + +chroot $TEMP passwd -d root + +sed -i 's/^PermitEmptyPasswords.*/PermitEmptyPasswords yes/' $TEMP/etc/ssh/sshd_config +sed -i 's/^UsePAM.*/UsePAM no/' $TEMP/etc/ssh/sshd_config + +cat >> $TEMP/etc/pam.d/sshd <<EOF +auth required pam_unix.so shadow nullok +EOF + +cat >> $TEMP/etc/securetty <<EOF +ssh +EOF + +echo en_US.UTF-8 UTF-8 >> $TEMP/etc/locale.gen +chroot $TEMP locale-gen + +cat > $TEMP/etc/default/locale <<EOF +# File generated by update-locale +LANG=en_US.UTF-8 +LANGUAGE="en_US:en" +EOF + +umount $TEMP + +kpartx -d $BLOCKDEV + +losetup -d $BLOCKDEV diff --git a/snf-deploy/scripts/nm-hostname b/snf-deploy/scripts/nm-hostname new file mode 100755 index 0000000000000000000000000000000000000000..5e13ae43e8fe6a22ec126336eb7e35932987372b --- /dev/null +++ b/snf-deploy/scripts/nm-hostname @@ -0,0 +1,6 @@ +#!/bin/bash + +if [ -n "$DHCP4_HOST_NAME" ]; then + echo $DHCP4_HOST_NAME > /etc/hostname + hostname $DHCP4_HOST_NAME +fi diff --git a/snf-deploy/setup.py b/snf-deploy/setup.py index 3d133c35e4983a66c058661032f12d2beeedd79c..c806c7d51571b0a2a9183c5133e9b9489bf3756d 100644 --- a/snf-deploy/setup.py +++ b/snf-deploy/setup.py @@ -1,35 +1,17 @@ -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # import distribute_setup @@ -58,6 +40,7 @@ CLASSIFIERS = [] # Package requirements INSTALL_REQUIRES = [ 'argparse', + 'simplejson', 'ipaddr', 'fabric>=1.3', ] @@ -65,8 +48,8 @@ INSTALL_REQUIRES = [ setup( name='snf-deploy', version=VERSION, - license='BSD', - url='http://code.grnet.gr/', + license='GNU GPLv3', + url='https://www.synnefo.org/', description=SHORT_DESCRIPTION, long_description=SHORT_DESCRIPTION, classifiers=CLASSIFIERS, diff --git a/snf-deploy/snfdeploy/__init__.py b/snf-deploy/snfdeploy/__init__.py index 103413113e3684a0f31ea62efecb49a1d579c5b6..9d64d21685215c48675a44c7c8bbd32b092c9181 100644 --- a/snf-deploy/snfdeploy/__init__.py +++ b/snf-deploy/snfdeploy/__init__.py @@ -1,19 +1,37 @@ -import time +#!/usr/bin/python + +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + import os import argparse import sys -import re -import random -import ast -from snfdeploy.lib import check_pidfile, create_dir, get_default_route, \ - random_mac, Conf, Env -from snfdeploy import fabfile +import glob from fabric.api import hide, settings, execute, show +from snfdeploy import config +from snfdeploy import context +from snfdeploy import status +from snfdeploy import fabfile +from snfdeploy import vcluster +from snfdeploy import constants +from snfdeploy.lib import create_dir -def print_available_actions(command): +def print_help_msg(cmds): - if command == "keygen": + if "keygen" in cmds: print """ Usage: snf-deploy keygen [--force] @@ -21,7 +39,7 @@ Usage: snf-deploy keygen [--force] """ - if command == "vcluster": + elif "vcluster" in cmds: print """ Usage: snf-deploy vcluster @@ -35,211 +53,61 @@ Usage: snf-deploy vcluster """ - if command == "prepare": + elif "setup" in cmds: print """ -Usage: snf-deploy prepare +Usage: setup --node NODE [--role ROLE | --method METHOD --component COMPONENT] - Run the following actions concerning deployment preparation: + Setup a specific component on the requested context - - Setup an internal Domain Name Server - - Tweak hosts and add ssh keys - - Check network setup - - Setup apt repository and apt-get update - - Setup the nfs server and clients among all nodes + --node NODE (overriden if --autoconf is passed) + --role ROLE (one of the end roles) + --cluster CLUSTER (one of the registered cluster) + --setup SETUP (one of the registered setups) + --component COMPONENT (one of the subcomponents) """ - if command == "backend": + elif "run" in cmds: print """ -Usage: snf-deploy backend [update] - - Run the following actions concerning a ganeti backend: - - - Create and add a backend to cyclades - - Does all the net-infra specific actions in backend nodes - (create/connect bridges, iptables..) - - Does all the storage-infra specific actions in backend nodes - depending on the --extra-disk option \ -(create VG, enable lvm/drbd storage..) - - or +Usage: setup --setup SETUP | --target-nodes NODE1,NODE2... --cmd "some cmd" - - Update packages in an already registered backend in cyclades. + Run a specific bash command on the requested nodes + --target-nodes NODES Comma separated nodes definded in nodes.conf + --setup SETUP Target all nodes in SETUP defined in setups.conf + --cmd CMD The bash command to be executed """ - if command == "run": + else: print """ -Usage: snf-deploy run <action> [<action>...] - - Run any of the following fabric commands: - - - Setup commands: Init commands: Admin commands: - setup_apache add_pools activate_user - setup_apt add_rapi_user add_backend - setup_astakos add_nodes add_image_locally - setup_cms astakos_loaddata add_network - setup_collectd - setup_common astakos_register_components add_ns - setup_cyclades cms_loaddata add_user - setup_db cyclades_loaddata connect_bridges - setup_ganeti enable_drbd create_bridges - setup_ganeti_collectd - setup_gtools init_cluster create_vlans - setup_gunicorn setup_nfs_clients destroy_db - setup_hosts setup_nfs_server \ -get_auth_token_from_db - setup_image_helper update_ns_for_ganeti get_service_details - setup_image_host astakos_register_pithos_view gnt_instance_add - setup_iptables gnt_network_add - setup_kamaki Test commands: register_image - setup_lvm test restart_services - setup_mq setup_drbd_dparams - setup_net_infra - setup_network - setup_ns - setup_pithos - setup_pithos_dir - setup_router - setup_stats - setup_stats_collectd - setup_vncauthproxy - setup_webproject +Usage: snf-deploy [-h] [-c CONFDIR] [-t TEMPLATE_DIR] [-s STATE_DIR] + [--dry-run] [-v] [-d] [--autoconf] [--mem MEM] [--smp SMP] + [--vnc] [--force] [-i SSH_KEY] [--no-key-inject] + [--cluster CLUSTER] [--component COMPONENT] + [--method METHOD] [--role ROLE] [--node NODE] + [--setup SETUP] [--disable-colors] + command [cmd] + + The command can be either of: + + packages Download synnefo packages and stores them locally + image Create a debian base image for vcluster + vcluster Create a local virtual cluster with KVM, dnsmasq, and NAT + cleanup Cleanup the local virtual cluster + test Print the configuration + synnefo Deploy synnefo on the requested setup + keygen Create ssh and ddns keys + ganeti Deploy a Ganeti cluster on the requested setup + ganeti-qa Deploy a Ganeti QA cluster on the requested cluster + run Run a specific bash command on the target nodes + help Display a help message for the following command """ - sys.exit(1) - - -def create_dnsmasq_files(args, env): - - print("Customize dnsmasq..") - out = env.dns - - hostsfile = open(out + "/dhcp-hostsfile", "w") - optsfile = open(out + "/dhcp-optsfile", "w") - conffile = open(out + "/conf-file", "w") - - for node, info in env.nodes_info.iteritems(): - # serve ip and hostname to nodes - hostsfile.write("%s,%s,%s,2m\n" % (info.mac, info.ip, info.hostname)) + sys.exit(0) - hostsfile.write("52:54:56:*:*:*,ignore\n") - # Netmask - optsfile.write("1,%s\n" % env.net.netmask) - # Gateway - optsfile.write("3,%s\n" % env.gateway) - # Namesevers - optsfile.write("6,%s\n" % "8.8.8.8") - - dnsconf = """ -user=dnsmasq -bogus-priv -no-poll -no-negcache -leasefile-ro -bind-interfaces -except-interface=lo -dhcp-fqdn -no-resolv -# disable DNS -port=0 -""".format(env.ns.ip) - - dnsconf += """ -# serve domain and search domain for resolv.conf -domain={5} -interface={0} -dhcp-hostsfile={1} -dhcp-optsfile={2} -dhcp-range={0},{4},static,2m -""".format(env.bridge, hostsfile.name, optsfile.name, - env.domain, env.net.network, env.domain) - - conffile.write(dnsconf) - - hostsfile.close() - optsfile.close() - conffile.close() - - -def cleanup(args, env): - print("Cleaning up bridge, NAT, resolv.conf...") - - for f in os.listdir(env.run): - if re.search(".pid$", f): - check_pidfile(os.path.join(env.run, f)) - - create_dir(env.run, True) - # create_dir(env.cmd, True) - cmd = """ - iptables -t nat -D POSTROUTING -s {0} -o {1} -j MASQUERADE - echo 0 > /proc/sys/net/ipv4/ip_forward - iptables -D INPUT -i {2} -j ACCEPT - iptables -D FORWARD -i {2} -j ACCEPT - iptables -D OUTPUT -o {2} -j ACCEPT - """.format(env.subnet, get_default_route()[1], env.bridge) - os.system(cmd) - - cmd = """ - ip link show {0} && ip addr del {1}/{2} dev {0} - sleep 1 - ip link set {0} down - sleep 1 - brctl delbr {0} - """.format(env.bridge, env.gateway, env.net.prefixlen) - os.system(cmd) - - -def network(args, env): - print("Create bridge..Add gateway IP..Activate NAT.." - "Append NS options to resolv.conf") - - cmd = """ - ! ip link show {0} && brctl addbr {0} && ip link set {0} up - sleep 1 - ip link set promisc on dev {0} - ip addr add {1}/{2} dev {0} - """.format(env.bridge, env.gateway, env.net.prefixlen) - os.system(cmd) - - cmd = """ - iptables -t nat -A POSTROUTING -s {0} -o {1} -j MASQUERADE - echo 1 > /proc/sys/net/ipv4/ip_forward - iptables -I INPUT 1 -i {2} -j ACCEPT - iptables -I FORWARD 1 -i {2} -j ACCEPT - iptables -I OUTPUT 1 -o {2} -j ACCEPT - """.format(env.subnet, get_default_route()[1], env.bridge) - os.system(cmd) - - -def image(args, env): - if env.os == "ubuntu": - url = env.ubuntu_image_url - else: - url = env.squeeze_image_url - - disk0 = "{0}/{1}.disk0".format(env.images, env.os) - disk1 = "{0}/{1}.disk1".format(env.images, env.os) - - if url and not os.path.exists(disk0): - cmd = "wget {0} -O {1}".format(url, disk0) - os.system(cmd) - - if ast.literal_eval(env.create_extra_disk) and not os.path.exists(disk1): - if env.lvg: - cmd = "lvcreate -L30G -n{0}.disk1 {1}".format(env.os, env.lvg) - os.system(cmd) - cmd = "ln -s /dev/{0}/{1}.disk1 {2}".format(env.lvg, env.os, disk1) - os.system(cmd) - else: - cmd = "dd if=/dev/zero of={0} bs=10M count=3000".format(disk1) - os.system(cmd) - - -def fabcommand(args, env, actions, nodes=[]): +def fabcommand(args, actions): levels = ["status", "aborts", "warnings", "running", "stdout", "stderr", "user", "debug"] @@ -267,83 +135,17 @@ def fabcommand(args, env, actions, nodes=[]): # ".format(args.confdir, env.packages, env.templates, args.cluster_name, # env.lib, args.autoconf, args.disable_colors, args.key_inject) - if nodes: - ips = [env.nodes_info[n].ip for n in nodes] - - fabfile.setup_env(args) with settings(hide(*lhide), show(*lshow)): - print " ".join(actions) for a in actions: fn = getattr(fabfile, a) - if not args.dry_run: - if nodes: - execute(fn, hosts=ips) - else: - execute(fn) - - -def cluster(args, env): - for hostname, mac in env.node2mac.iteritems(): - launch_vm(args, env, hostname, mac) - - time.sleep(30) - os.system("reset") + execute(fn) -def launch_vm(args, env, hostname, mac): - check_pidfile("%s/%s.pid" % (env.run, hostname)) - - print("Launching cluster node {0}..".format(hostname)) - os.environ["BRIDGE"] = env.bridge - if args.vnc: - graphics = "-vnc :{0}".format(random.randint(1, 1000)) - else: - graphics = "-nographic" - - disks = """ \ --drive file={0}/{1}.disk0,format=raw,if=none,id=drive0,snapshot=on \ --device virtio-blk-pci,drive=drive0,id=virtio-blk-pci.0 \ -""".format(env.images, env.os) - - if ast.literal_eval(env.create_extra_disk): - disks += """ \ --drive file={0}/{1}.disk1,format=raw,if=none,id=drive1,snapshot=on \ --device virtio-blk-pci,drive=drive1,id=virtio-blk-pci.1 \ -""".format(env.images, env.os) - - ifup = env.lib + "/ifup" - nics = """ \ --netdev tap,id=netdev0,script={0},downscript=no \ --device virtio-net-pci,mac={1},netdev=netdev0,id=virtio-net-pci.0 \ --netdev tap,id=netdev1,script={0},downscript=no \ --device virtio-net-pci,mac={2},netdev=netdev1,id=virtio-net-pci.1 \ --netdev tap,id=netdev2,script={0},downscript=no \ --device virtio-net-pci,mac={3},netdev=netdev2,id=virtio-net-pci.2 \ -""".format(ifup, mac, random_mac(), random_mac()) - - cmd = """ -/usr/bin/kvm -name {0} -pidfile {1}/{0}.pid -balloon virtio -daemonize \ --monitor unix:{1}/{0}.monitor,server,nowait -usbdevice tablet -boot c \ -{2} \ -{3} \ --m {4} -smp {5} {6} \ -""".format(hostname, env.run, disks, nics, args.mem, args.smp, graphics) - print cmd - os.system(cmd) - - -def dnsmasq(args, env): - check_pidfile(env.run + "/dnsmasq.pid") - cmd = "dnsmasq --pid-file={0}/dnsmasq.pid --conf-file={1}/conf-file"\ - .format(env.run, env.dns) - os.system(cmd) - - -def get_packages(args, env): - if env.package_url: - os.system("rm {0}/*.deb".format(env.packages)) +def get_packages(): + if config.package_url: + os.system("rm {0}/*.deb".format(config.package_dir)) os.system("wget -r --level=1 -nH --no-parent --cut-dirs=4 {0} -P {1}" - .format(env.package_url, env.packages)) + .format(config.package_url, config.package_dir)) def parse_options(): @@ -353,6 +155,14 @@ def parse_options(): parser.add_argument("-c", dest="confdir", default="/etc/snf-deploy", help="Directory to find default configuration") + parser.add_argument("-t", "--templates-dir", dest="template_dir", + default=None, + help="Directory to find templates. Overrides" + " the one found in the deploy.conf file") + parser.add_argument("-s", "--state-dir", dest="state_dir", + default=None, + help="Directory to store current state. Overrides" + " the one found in the deploy.conf") parser.add_argument("--dry-run", dest="dry_run", default=False, action="store_true", help="Do not execute or write anything.") @@ -379,7 +189,8 @@ def parse_options(): "console or not") parser.add_argument("--force", dest="force", default=False, action="store_true", - help="Force the creation of new ssh key pairs") + help="Force things (creation of key pairs" + " do not abort execution if something fails") parser.add_argument("-i", "--ssh-key", dest="ssh_key", default=None, @@ -389,27 +200,54 @@ def parse_options(): default=True, action="store_false", help="Whether to inject ssh key pairs to hosts") + parser.add_argument("--pass-gen", dest="passgen", + default=False, action="store_true", + help="Whether to create random passwords") + # backend related options - parser.add_argument("--cluster-name", dest="cluster_name", - default="ganeti1", + parser.add_argument("--cluster", dest="cluster", + default=constants.DEFAULT_CLUSTER, help="The cluster name in ganeti.conf") - # backend related options - parser.add_argument("--cluster-node", dest="cluster_node", + # options related to custom setup + parser.add_argument("--component", dest="component", + default=None, + help="The component class") + + parser.add_argument("--method", dest="method", + default=None, + help="The component method") + + parser.add_argument("--role", dest="role", + default=None, + help="The target node's role") + + parser.add_argument("--node", dest="node", + default=constants.DEFAULT_NODE, + help="The target node") + + parser.add_argument("--setup", dest="setup", + default=constants.DEFAULT_SETUP, + help="The target setup") + + parser.add_argument("--cmd", dest="cmd", + default="date", + help="The command to run on target nodes") + + parser.add_argument("--target-nodes", dest="target_nodes", default=None, - help="The node to add to the existing cluster") + help="The target nodes to run cmd") # available commands parser.add_argument("command", type=str, - choices=["packages", "vcluster", "prepare", - "synnefo", "backend", "ganeti", - "run", "cleanup", "test", - "all", "add", "keygen"], + choices=["packages", "vcluster", "cleanup", "image", + "setup", "test", "synnefo", "keygen", + "ganeti", "ganeti-qa", "help", "run"], help="Run on of the supported deployment commands") # available actions for the run command - parser.add_argument("actions", type=str, nargs="*", - help="Run one or more of the supported subcommands") + parser.add_argument("cmds", type=str, nargs="*", + help="Specific commands to display help for") # disable colors in terminal parser.add_argument("--disable-colors", dest="disable_colors", @@ -421,76 +259,22 @@ def parse_options(): def get_actions(*args): actions = { - # prepare actions - "ns": ["setup_ns", "setup_resolv_conf"], - "hosts": ["setup_hosts", "add_keys"], - "check": ["check_dhcp", "check_dns", - "check_connectivity", "check_ssh"], - "apt": ["apt_get_update", "setup_apt"], - "nfs": ["setup_nfs_server", "setup_nfs_clients"], - "prepare": [ - "setup_hosts", "add_keys", - "setup_ns", "setup_resolv_conf", - "check_dhcp", "check_dns", "check_connectivity", "check_ssh", - "apt_get_update", "setup_apt", - "setup_nfs_server", "setup_nfs_clients" - ], - # synnefo actions - "synnefo": [ - "setup_mq", "setup_db", - "setup_astakos", - #TODO: astakos-quota fails if no user is added. - # add_user fails if no groups found - "astakos_loaddata", "add_user", "activate_user", - "astakos_register_components", - "astakos_register_pithos_view", - "setup_cms", "cms_loaddata", - "setup_pithos", - "setup_vncauthproxy", - "setup_cyclades", "cyclades_loaddata", "add_pools", - "export_services", "import_services", "set_user_quota", - "setup_kamaki", "upload_image", "register_image", - "setup_burnin", - "setup_stats" - ], - "supdate": [ - "apt_get_update", "setup_astakos", - "setup_cms", "setup_pithos", "setup_cyclades" + "ganeti": [ + "setup_ganeti" ], - # backend actions - "backend": [ - "setup_hosts", - "update_ns_for_ganeti", - "setup_ganeti", "init_cluster", - "add_rapi_user", "add_nodes", - "setup_image_host", "setup_image_helper", - "setup_network", - "setup_gtools", "add_backend", "add_network", - "setup_lvm", "enable_lvm", - "enable_drbd", "setup_drbd_dparams", - "setup_net_infra", "setup_iptables", "setup_router", - "setup_ganeti_collectd" + "ganeti-qa": [ + "setup_qa", ], - "bstorage": [ - "setup_lvm", "enable_lvm", - "enable_drbd", "setup_drbd_dparams" + "synnefo": [ + "setup_synnefo", ], - "bnetwork": ["setup_net_infra", "setup_iptables", "setup_router"], - "bupdate": [ - "apt_get_update", "setup_ganeti", "setup_image_host", - "setup_image_helper", "setup_network", "setup_gtools" + "setup": [ + "setup", ], - # ganeti actions - "ganeti": [ - "update_ns_for_ganeti", - "setup_ganeti", "init_cluster", "add_nodes", - "setup_image_host", "setup_image_helper", "add_image_locally", - "debootstrap", "setup_net_infra", - "setup_lvm", "enable_lvm", "enable_drbd", "setup_drbd_dparams", - "setup_ganeti_collectd" + "run": [ + "run", ], - "gupdate": ["setup_apt", "setup_ganeti"], - "gdestroy": ["destroy_cluster"], + } ret = [] @@ -500,16 +284,11 @@ def get_actions(*args): return ret -def must_create_keys(force, env): - """Check if we need to create ssh keys - - If force is true we are going to overide the old keys. - Else if there are already generated keys to use, don't create new ones. +def must_create_keys(): + """Check if the ssh keys already exist """ - if force: - return True - d = os.path.join(env.templates, "root/.ssh") + d = os.path.join(config.template_dir, "root/.ssh") auth_keys_exists = os.path.exists(os.path.join(d, "authorized_keys")) dsa_exists = os.path.exists(os.path.join(d, "id_dsa")) dsa_pub_exists = os.path.exists(os.path.join(d, "id_dsa.pub")) @@ -521,8 +300,11 @@ def must_create_keys(force, env): and auth_keys_exists) -def do_create_keys(args, env): - d = os.path.join(env.templates, "root/.ssh") +def do_create_keys(): + d = os.path.join(config.template_dir, "root/.ssh") + # Create dir if it does not exist + if not os.path.exists(d): + os.makedirs(d) a = os.path.join(d, "authorized_keys") # Delete old keys for filename in os.listdir(d): @@ -536,116 +318,103 @@ def do_create_keys(args, env): os.system(cmd) -def add_node(args, env): - actions = [ - "update_ns_for_node:" + args.cluster_node, - ] - fabcommand(args, env, actions) - actions = [ - "setup_resolv_conf", - "apt_get_update", - "setup_apt", - "setup_hosts", - "add_keys", - ] - fabcommand(args, env, actions, [args.cluster_node]) - - actions = get_actions("check") - fabcommand(args, env, actions) - - actions = [ - "setup_nfs_clients", - "setup_ganeti", - "setup_image_host", "setup_image_helper", - "setup_network", "setup_gtools", - ] - fabcommand(args, env, actions, [args.cluster_node]) - - actions = [ - "add_node:" + args.cluster_node, - ] - fabcommand(args, env, actions) - - actions = [ - "setup_lvm", "enable_drbd", - "setup_net_infra", "setup_iptables", - ] - fabcommand(args, env, actions, [args.cluster_node]) +def must_create_ddns_keys(): + d = os.path.join(config.template_dir, "root/ddns") + # Create dir if it does not exist + if not os.path.exists(d): + os.makedirs(d) + key_exists = glob.glob(os.path.join(d, "Kddns*key")) + private_exists = glob.glob(os.path.join(d, "Kddns*private")) + bind_key_exists = os.path.exists(os.path.join(d, "ddns.key")) + return not (key_exists and private_exists and bind_key_exists) + + +def find_ddns_key_files(): + d = os.path.join(config.template_dir, "root/ddns") + keys = glob.glob(os.path.join(d, "Kddns*")) + # Here we must have a key! + return map(os.path.basename, keys) + + +def do_create_ddns_keys(): + d = os.path.join(config.template_dir, "root/ddns") + if not os.path.exists(d): + os.mkdir(d) + for filename in os.listdir(d): + os.remove(os.path.join(d, filename)) + cmd = """ +dnssec-keygen -a HMAC-MD5 -b 128 -K {0} -r /dev/urandom -n USER DDNS_UPDATE +key=$(cat {0}/Kddns_update*.key | awk '{{ print $7 }}') +cat > {0}/ddns.key <<EOF +key DDNS_UPDATE {{ + algorithm HMAC-MD5.SIG-ALG.REG.INT; + secret "$key"; +}}; +EOF +""".format(d) + os.system(cmd) def main(): args = parse_options() - conf = Conf(args) - env = Env(conf) + config.init(args) + status.init() + context.init(args) - create_dir(env.run, False) - create_dir(env.dns, False) + create_dir(config.run_dir, False) + create_dir(config.dns_dir, False) # Check if there are keys to use if args.command == "keygen": - if must_create_keys(args.force, env): - do_create_keys(args, env) - return 0 + if must_create_keys() or args.force: + do_create_keys() else: - print "Keys already existed.. aborting" - return 1 + print "ssh keys found. To re-create them use --force" + if must_create_ddns_keys() or args.force: + do_create_ddns_keys() + else: + print "ddns keys found. To re-create them use --force" + return 0 else: - if (args.key_inject and (args.ssh_key is None) - and must_create_keys(False, env)): - print "No ssh keys to use. Run `snf-deploy keygen' first." + if ((args.key_inject and not args.ssh_key and + must_create_keys()) or must_create_ddns_keys()): + print "No ssh/ddns keys to use. Run `snf-deploy keygen' first." return 1 + config.ddns_keys = find_ddns_key_files() + config.ddns_private_key = "/root/ddns/" + config.ddns_keys[0] if args.command == "test": - conf.print_config() + config.print_config() + return 0 + + if args.command == "image": + vcluster.image() + return 0 if args.command == "cleanup": - cleanup(args, env) + vcluster.cleanup() + return 0 if args.command == "packages": - create_dir(env.packages, True) - get_packages(args, env) + create_dir(config.package_dir, True) + get_packages() + return 0 if args.command == "vcluster": - image(args, env) - network(args, env) - create_dnsmasq_files(args, env) - dnsmasq(args, env) - cluster(args, env) - - if args.command == "prepare": - actions = get_actions("prepare") - fabcommand(args, env, actions) - - if args.command == "synnefo": - actions = get_actions("synnefo") - fabcommand(args, env, actions) - - if args.command == "backend": - actions = get_actions("backend") - fabcommand(args, env, actions) - - if args.command == "ganeti": - actions = get_actions("ganeti") - fabcommand(args, env, actions) - - if args.command == "all": - actions = get_actions("prepare", "synnefo", "backend") - fabcommand(args, env, actions) - - if args.command == "add": - if args.cluster_node: - add_node(args, env) - else: - actions = get_actions("backend") - fabcommand(args, env, actions) + status.reset() + vcluster.cleanup() + vcluster.launch() + return 0 - if args.command == "run": - if not args.actions: - print_available_actions(args.command) - else: - fabcommand(args, env, args.actions) + if args.command == "help": + print_help_msg(args.cmds) + return 0 + + actions = get_actions(args.command) + fabcommand(args, actions) + return 0 if __name__ == "__main__": sys.exit(main()) diff --git a/snf-deploy/snfdeploy/base.py b/snf-deploy/snfdeploy/base.py new file mode 100644 index 0000000000000000000000000000000000000000..d41b0240d88e424108b944192148a3d58a366b8a --- /dev/null +++ b/snf-deploy/snfdeploy/base.py @@ -0,0 +1,335 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from fabric.api import env, local +from fabric.operations import run, put, get +from fabric.context_managers import quiet +import fabric +import os +import shutil +import tempfile +import glob +import time +import copy +from snfdeploy.lib import debug +from snfdeploy import massedit +from snfdeploy import config +from snfdeploy import status +from snfdeploy import context +from snfdeploy import constants + + +# +# Decorators to use on FabricRunner's methods (run, get, put) +# +def _try_and_abort(fn): + """Do nothing is case of dry-run otherwise execute and abort""" + def wrapper(*args, **kwargs): + assert args + cl = args[0] + if config.dry_run: + cl._debug(args[1]) + return + try: + return fn(*args, **kwargs) + except BaseException as e: + if not cl.abort or config.force: + cl._debug("WARNING: command failed. Continuing anyway...") + else: + fabric.utils.abort(e) + return wrapper + + +def _setup_fabric_env(fn): + """Update fabric specific vars related to ssh""" + def wrapper(*args, **kwargs): + assert args + cl = args[0] + env.host = cl.node.name + env.host_string = cl.node.ip + env.password = cl.node.password + env.user = cl.node.user + env.shell = "/bin/bash -c" + env.key_filename = config.ssh_key + return fn(*args, **kwargs) + return wrapper + + +def log(fn): + def wrapper(*args, **kwargs): + assert args + cl = args[0] + cl._debug(fn.__name__) + return fn(*args, **kwargs) + return wrapper + + +def run_cmds(fn): + def wrapper(*args, **kwargs): + """If used as decorator of a class method first argument is self.""" + cl = args[0] + # do something before fn + ret = str() + for c in fn(*args, **kwargs): + output = cl.run(c) + if output: + ret += output + return ret + return wrapper + + +def check_if_testing(fn): + def wrapper(*args, **kwargs): + assert args + if config.testing_vm: + return [] + else: + return fn(*args, **kwargs) + return wrapper + + +def _customize_settings_from_tmpl(tmpl, replace): + local = config.template_dir + tmpl + _, custom = tempfile.mkstemp() + shutil.copyfile(local, custom) + for k, v in replace.iteritems(): + regex = "re.sub('%{0}%', '{1}', line)".format(k.upper(), v) + editor = massedit.Editor(dry_run=False) + editor.set_code_expr([regex]) + editor.edit_file(custom) + + return custom + + +class FabricRunner(object): + + @_try_and_abort + @_setup_fabric_env + def put(self, local, remote, mode=0644): + self._debug("Uploading %s .." % remote) + if config.autoconf: + shutil.copyfile(local, remote) + os.chmod(remote, mode) + else: + put(local_path=local, remote_path=remote, mode=mode) + + @_try_and_abort + @_setup_fabric_env + def get(self, remote, local): + self._debug("Downloading %s .." % remote) + if config.autoconf: + shutil.copyfile(remote, local) + else: + get(remote_path=remote, local_path=local) + + @_try_and_abort + @_setup_fabric_env + def run(self, cmd): + self._debug("RunCmd %s .." % cmd) + if config.autoconf: + return local(cmd, capture=True, shell="/bin/bash") + else: + return run(cmd) + + +class ComponentRunner(FabricRunner): + """Fabric wrapper for SynnefoComponent""" + + ORDER = [ + "check", + "install", + "prepare", + "configure", + "restart", + "initialize", + "test", + ] + + def _check_conflicts(self): + for c in self.conflicts: + if status.check(c(self.ctx)): + raise BaseException("Conflicting Component: %s " % + c.__name__) + + def _check_status(self): + if status.check(self): + raise BaseException("Component already installed: %s " % + self.__class__.__name__) + + def _update_status(self): + status.update(self) + self._debug(constants.VALUE_OK) + + def _debug(self, msg): + debug(str(self.ctx), "[%s]" % self.__class__.__name__, msg) + + def _install_package(self, package): + self._debug(" * Installing package %s... " % package) + apt_get = "export DEBIAN_FRONTEND=noninteractive ;" + \ + "apt-get install -y --force-yes " + + if config.use_local_packages: + with quiet(): + debs = glob.glob("%s/%s*.deb" % (config.package_dir, package)) + if debs: + deb = debs[0] + f = os.path.basename(deb) + self._debug(" * Package %s found in %s..." + % (package, config.package_dir)) + self.put(deb, "/tmp/%s" % f) + cmd = """ +dpkg -i /tmp/{0} +{2} -f +apt-mark hold {1} +""".format(f, package, apt_get) + self.run(cmd) + self.run("rm /tmp/%s" % f) + return + + info = config.get_package(package, self.node.os) + if info in \ + ["squeeze-backports", "squeeze", "stable", + "testing", "unstable", "wheezy", "wheezy-backports"]: + apt_get += " -t %s %s " % (info, package) + elif info: + apt_get += " %s=%s " % (package, info) + else: + apt_get += package + + self.run(apt_get) + + return + + def install(self): + for p in self._install(): + self._install_package(p) + + def configure(self): + for tmpl, replace, opts in self._configure(): + self._debug(" * Customizing template %s..." % tmpl) + mode = opts.get("mode", 0644) + remote = opts.get("remote", tmpl) + custom = _customize_settings_from_tmpl(tmpl, replace) + self.put(custom, remote, mode) + os.remove(custom) + + def _setup(self): + self.admin_pre() + self.check() + self.install() + self.prepare() + self.configure() + self.restart() + self.initialize() + self.test() + self.admin_post() + + def _check_and_install_required(self): + ctx = self.ctx + for c in self.required_components(): + c(ctx=ctx).setup() + + def setup(self): + self._check_and_install_required() + try: + self._check_status() + self._check_conflicts() + except BaseException as e: + self._debug(str(e)) + return + + self._setup() + self._update_status() + time.sleep(1) + + +class Component(ComponentRunner): + + REQUIRED_PACKAGES = [] + + alias = None + service = None + + def __init__(self, ctx=None, node=None): + if not ctx: + self.ctx = context.Context() + else: + self.ctx = copy.deepcopy(ctx) + if node: + self.ctx.node = node + self.abort = True + + def required_components(self): + return [] + + @property + def fqdn(self): + return self.node + + @property + def node(self): + info = self.ctx.node_info + info.alias = self.alias + return info + + @property + def cluster(self): + return self.ctx.cluster_info + + @property + def conflicts(self): + return [] + + def admin_pre(self): + pass + + @run_cmds + def check(self): + """ Returns a list of bash commands that check prerequisites """ + return [] + + def _install(self): + """ Returns a list of debian packages to install """ + return self.REQUIRED_PACKAGES + + @run_cmds + def prepare(self): + """ Returs a list of bash commands that prepares the component """ + return [] + + def _configure(self): + """ Must return a list of tuples (tmpl_path, replace_dict, mode) """ + return [] + + @run_cmds + def initialize(self): + """ Returs a list of bash commands that initialize the component """ + return [] + + @run_cmds + def test(self): + """ Returs a list of bash commands that test existing installation """ + return [] + + @run_cmds + def restart(self): + return [] + + @run_cmds + def clean(self): + return [] + + def admin_post(self): + pass diff --git a/snf-deploy/snfdeploy/components.py b/snf-deploy/snfdeploy/components.py new file mode 100644 index 0000000000000000000000000000000000000000..119b891f93a02a0a00796ccc2ec31ab5b457c7f9 --- /dev/null +++ b/snf-deploy/snfdeploy/components.py @@ -0,0 +1,2090 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import re +import datetime +import simplejson +import copy +import os +from snfdeploy import base +from snfdeploy import config +from snfdeploy import constants +from snfdeploy import context +from snfdeploy.lib import FQDN, evaluate + + +_USER_INFO_RE = lambda x: \ + re.compile(r"(\d+)[ |]*(\S+)[ |]*(\S+)[ |]*%s.*" % x, re.M) +_USER_INFO = ["user_id", "user_auth_token", "user_uuid"] + +_SERVICE_INFO_RE = lambda x: re.compile(r"(\d+)[ ]*%s[ ]*(\S+)" % x, re.M) +_SERVICE_INFO = ["service_id", "service_token"] + +_BACKEND_INFO_RE = lambda x: re.compile(r"(\d+)[ ]*%s.*" % x, re.M) +_BACKEND_INFO = ["backend_id"] + +_VOLUME_INFO_RE = lambda x: re.compile(r"(\d+)[ ]*%s.*" % x, re.M) +_VOLUME_INFO = ["volume_type_id"] + + +# Helper decorator that wraps get_* methods of certain Components +# Those methods take one argument; the identity (mail, service, backend) +# to look for. It parses the output of those methods and updates the +# context's keys with the matched groups. +def parse(regex, keys): + def wrap(f): + def wrapped_f(cl, what): + result = f(cl, what) + match = regex(what).search(result) + if config.dry_run: + evaluate(context, **dict(zip(keys, ["dummy"] * len(keys)))) + elif match: + evaluate(context, **dict(zip(keys, match.groups()))) + else: + raise BaseException("Cannot parse info for %s" % what) + return wrapped_f + return wrap + + +def update_admin(fn): + """ Initializes the admin roles for each component + + Initialize the admin roles (NS, Astakos, Cyclades, etc.) and make them + available under self.NS, self.ASTAKOS, etc. These have the same execution + context of the current components besides the target node which gets + derived from the corresponding config. + + """ + def wrapper(*args, **kwargs): + """If used as decorator of a class method first argument is self.""" + cl = args[0] + ctx = copy.deepcopy(cl.ctx) + ctx.admin_service = cl.service + ctx.admin_cluster = cl.cluster + ctx.admin_node = cl.node + ctx.admin_fqdn = cl.fqdn + cl.NS = NS(node=ctx.ns.node, ctx=ctx) + cl.CA = CA(node=ctx.ca.node, ctx=ctx) + cl.NFS = NFS(node=ctx.nfs.node, ctx=ctx) + cl.DB = DB(node=ctx.db.node, ctx=ctx) + cl.ASTAKOS = Astakos(node=ctx.astakos.node, ctx=ctx) + cl.CYCLADES = Cyclades(node=ctx.cyclades.node, ctx=ctx) + cl.ADMIN = Admin(node=ctx.admin.node, ctx=ctx) + cl.CLIENT = Client(node=ctx.client.node, ctx=ctx) + return fn(*args, **kwargs) + return wrapper + + +def update_cluster_admin(fn): + """ Initializes the cluster admin roles for each component + + Finds the master role for the corresponding cluster + + """ + def wrapper(*args, **kwargs): + """If used as decorator of a class method first argument is self.""" + cl = args[0] + ctx = copy.deepcopy(cl.ctx) + ctx.admin_cluster = cl.cluster + cl.MASTER = Master(node=ctx.master.node, ctx=ctx) + return fn(*args, **kwargs) + return wrapper + + +def export_and_import_service(fn): + """ Export and import synnefo service + + Used in Astakos, Pithos, and Cyclades service admin_post method + + """ + def wrapper(*args, **kwargs): + cl = args[0] + f = config.jsonfile + cl.export_service() + cl.get(f, f + ".local") + cl.ASTAKOS.put(f + ".local", f) + cl.ASTAKOS.import_service() + return fn(*args, **kwargs) + return wrapper + + +# ########################## Components ############################ + +# A Component() gets initialized with an execution context that is a +# configuration snapshot of the target setup, cluster and node. A +# component implements the following helper methods: check, install, +# prepare, configure, restart, initialize, and test. All those methods +# will be executed on the target node with this order during setup. +# +# Additionally each Component class implements admin_pre, and +# admin_post methods which invoke actions on different components on +# the same execution context before and after installation. For +# example before a backend gets installed, its FQDN must resolve to +# the master floating IP, so we have to run some actions on the ns +# node and after installation we must add it to cyclades (snf-manage +# backend-add on the cyclades node). +# +# Component() inherits ComponentRunner() which practically exports the +# setup() method. This will first check if the required components are +# installed, will install them if not and update the status of target +# node. +# +# ComponentRunner() inherits FabricRunner() which practically wraps +# basic fabric commands (put, get, run) with the correct execution +# environment. +# +# Each component gets initialized with an execution context and uses +# the config module for accessing global wide options. The context +# provides node, cluster, and setup related info. + +class HW(base.Component): + + @base.run_cmds + def prepare(self): + return [ + # NOTE: This is needed because the NFS dir is owned by + # archipelago:synnefo and IDs must be common across nodes + "addgroup --system --gid 200 synnefo", + "adduser --system --uid 200 --gid 200 --no-create-home \ + --gecos Synnefo synnefo", + "addgroup --system --gid 300 archipelago", + "adduser --system --uid 300 --gid 300 --no-create-home \ + --gecos Archipelago archipelago", + ] + + @base.check_if_testing + def _configure(self): + r1 = { + "date": str(datetime.datetime.today()), + } + return [ + ("/etc/sysctl.d/disable-ipv6.conf", r1, {}) + ] + + @base.run_cmds + @base.check_if_testing + def initialize(self): + return [ + "sysctl -f /etc/sysctl.d/disable-ipv6.conf", + ] + + @base.run_cmds + def test(self): + return [ + "ping -c 1 %s" % self.node.ip, + "ping -c 1 www.google.com", + "apt-get update", + ] + + +class SSH(base.Component): + @base.run_cmds + def prepare(self): + return [ + "mkdir -p /root/.ssh", + "for f in $(ls /root/.ssh/*); do cp $f $f.bak ; done", + "echo StrictHostKeyChecking no >> /etc/ssh/ssh_config", + ] + + def _configure(self): + files = [ + "authorized_keys", "id_dsa", "id_dsa.pub", "id_rsa", "id_rsa.pub" + ] + ssh = [("/root/.ssh/%s" % f, {}, {"mode": 0600}) for f in files] + return ssh + + @base.run_cmds + def initialize(self): + f = "/root/.ssh/authorized_keys" + return [ + "test -e {0}.bak && cat {0}.bak >> {0} || true".format(f) + ] + + @base.run_cmds + def test(self): + return ["ssh %s date" % self.node.ip] + + +class DNS(base.Component): + @update_admin + def admin_pre(self): + self.NS.update_ns() + + @base.run_cmds + def prepare(self): + return [ + "chattr -i /etc/resolv.conf", + "sed -i 's/^127.*$/127.0.0.1 localhost/g' /etc/hosts", + "echo %s > /etc/hostname" % self.node.hostname, + "hostname %s" % self.node.hostname + ] + + def _configure(self): + r1 = { + "date": str(datetime.datetime.today()), + "domain": self.node.domain, + "ns_node_ip": self.ctx.ns.ip, + } + resolv = [ + ("/etc/resolv.conf", r1, {}) + ] + return resolv + + @base.run_cmds + def initialize(self): + return ["chattr +i /etc/resolv.conf"] + + +class DDNS(base.Component): + REQUIRED_PACKAGES = [ + "dnsutils", + ] + + @base.run_cmds + def prepare(self): + return [ + "mkdir -p /root/ddns/" + ] + + def _configure(self): + return [ + ("/root/ddns/" + k, {}, {}) for k in config.ddns_keys + ] + + +class NS(base.Component): + REQUIRED_PACKAGES = [ + "bind9", + ] + + alias = constants.NS + + def required_components(self): + return [HW, SSH, DDNS] + + def _nsupdate(self, cmd): + ret = """ +nsupdate -k {0} > /dev/null <<EOF || true +server {1} +{2} +send +EOF +""".format(config.ddns_private_key, self.ctx.ns.ip, cmd) + return ret + + @base.run_cmds + def update_ns(self, info=None): + if not info: + info = self.ctx.admin_fqdn + return [ + self._nsupdate("update add %s" % info.arecord), + self._nsupdate("update add %s" % info.ptrrecord), + self._nsupdate("update add %s" % info.cnamerecord), + ] + + def add_qa_instances(self): + instances = [ + ("xen-test-inst1", "1.2.3.4"), + ("xen-test-inst2", "1.2.3.5"), + ("xen-test-inst3", "1.2.3.6"), + ("xen-test-inst4", "1.2.3.7"), + ] + for name, ip in instances: + info = { + "name": name, + "ip": ip, + "domain": self.node.domain + } + node_info = FQDN(**info) + self.update_ns(node_info) + + @base.run_cmds + def prepare(self): + return [ + "mkdir -p /etc/bind/zones", + "chmod g+w /etc/bind/zones", + "mkdir -p /etc/bind/rev", + "chmod g+w /etc/bind/rev", + ] + + def _configure(self): + d = self.node.domain + ip = self.node.ip + return [ + ("/etc/bind/named.conf.local", {"domain": d}, {}), + ("/etc/bind/zones/example.com", + {"domain": d, "ns_node_ip": ip}, + {"remote": "/etc/bind/zones/%s" % d}), + ("/etc/bind/zones/vm.example.com", + {"domain": d, "ns_node_ip": ip}, + {"remote": "/etc/bind/zones/vm.%s" % d}), + ("/etc/bind/rev/synnefo.in-addr.arpa.zone", {"domain": d}, {}), + ("/etc/bind/rev/synnefo.ip6.arpa.zone", {"domain": d}, {}), + ("/etc/bind/named.conf.options", + {"node_ips": ";".join(self.ctx.all_ips)}, {}), + ("/root/ddns/ddns.key", {}, {"remote": "/etc/bind/ddns.key"}), + ] + + @base.run_cmds + def restart(self): + return ["/etc/init.d/bind9 restart"] + + +class APT(base.Component): + """ Setup apt repos and check fqdns """ + REQUIRED_PACKAGES = ["curl"] + + @base.run_cmds + def prepare(self): + return [ + "echo 'APT::Install-Suggests \"false\";' >> /etc/apt/apt.conf", + "curl -k https://dev.grnet.gr/files/apt-grnetdev.pub | \ + apt-key add -", + ] + + def _configure(self): + return [ + ("/etc/apt/sources.list.d/synnefo.wheezy.list", {}, {}) + ] + + @base.run_cmds + def initialize(self): + return [ + "apt-get update", + ] + + +class MQ(base.Component): + REQUIRED_PACKAGES = ["rabbitmq-server"] + + alias = constants.MQ + + def required_components(self): + return [HW, SSH, DNS, APT] + + @update_admin + def admin_pre(self): + self.NS.update_ns() + + @base.run_cmds + def check(self): + return ["ping -c 1 %s" % self.node.cname] + + @base.run_cmds + def initialize(self): + u = config.synnefo_user + p = config.synnefo_rabbitmq_passwd + return [ + "rabbitmqctl add_user %s %s" % (u, p), + "rabbitmqctl set_permissions %s \".*\" \".*\" \".*\"" % u, + "rabbitmqctl delete_user guest", + "rabbitmqctl set_user_tags %s administrator" % u, + ] + + +class DB(base.Component): + REQUIRED_PACKAGES = ["postgresql"] + + alias = constants.DB + + def required_components(self): + return [HW, SSH, DNS, APT] + + @update_admin + def admin_pre(self): + self.NS.update_ns() + + @base.run_cmds + def check(self): + return ["ping -c 1 %s" % self.node.cname] + + @parse(_USER_INFO_RE, _USER_INFO) + @base.run_cmds + def get_user_info_from_db(self, user_email): + cmd = """ +cat > /tmp/psqlcmd <<EOF +select id, auth_token, uuid, email from auth_user, im_astakosuser \ +where auth_user.id = im_astakosuser.user_ptr_id and auth_user.email = '{0}'; +EOF + +su - postgres -c "psql -w -d snf_apps -f /tmp/psqlcmd" +""".format(user_email) + + return [cmd] + + @base.run_cmds + def allow_db_access(self): + user = "all" + method = "md5" + ip = self.ctx.admin_node.ip + f = "/etc/postgresql/*/main/pg_hba.conf" + cmd1 = "echo host all %s %s/32 %s >> %s" % \ + (user, ip, method, f) + cmd2 = "sed -i 's/\(host.*127.0.0.1.*\)md5/\\1trust/' %s" % f + return [cmd1, cmd2] + + def _configure(self): + u = config.synnefo_user + p = config.synnefo_db_passwd + replace = {"synnefo_user": u, "synnefo_db_passwd": p} + return [ + ("/tmp/db-init.psql", replace, {}), + ] + + @base.check_if_testing + def make_db_fast(self): + f = "/etc/postgresql/*/main/postgresql.conf" + opts = "fsync=off\nsynchronous_commit=off\nfull_page_writes=off\n" + return ["""echo -e "%s" >> %s""" % (opts, f)] + + @base.run_cmds + def prepare(self): + f = "/etc/postgresql/*/main/postgresql.conf" + ret = ["""echo "listen_addresses = '*'" >> %s""" % f] + return ret + self.make_db_fast() + + @base.run_cmds + def initialize(self): + script = "/tmp/db-init.psql" + cmd = "su - postgres -c \"psql -w -f %s\" " % script + return [cmd] + + @base.run_cmds + def restart(self): + return ["/etc/init.d/postgresql restart"] + + @base.run_cmds + def destroy_db(self): + return [ + """su - postgres -c ' psql -w -c "drop database snf_apps" '""", + """su - postgres -c ' psql -w -c "drop database snf_pithos" '""" + ] + + +class VMC(base.Component): + + def extra_components(self): + if self.cluster.synnefo: + return [ + Image, GTools, GanetiCollectd, + PithosBackend, ExtStorage, Archip, ArchipGaneti + ] + else: + return [ExtStorage, Archip, ArchipGaneti] + + def required_components(self): + return [ + HW, SSH, DNS, DDNS, APT, Mount, LVM, DRBD, Ganeti, Network, + ] + self.extra_components() + + @update_cluster_admin + def admin_post(self): + self.MASTER.add_node(self.node) + self.MASTER.enable_lvm() + self.MASTER.enable_drbd() + + +class LVM(base.Component): + REQUIRED_PACKAGES = [ + "lvm2", + ] + + @base.run_cmds + def initialize(self): + extra_disk_dev = self.node.extra_disk + extra_disk_file = "/disk" + # If extra disk found use it + # else create a raw file and losetup it + cmd = """ +if [ -b "{0}" ]; then + pvcreate {0} && vgcreate {2} {0} +else + truncate -s {3} {1} + loop_dev=$(losetup -f --show {1}) + pvcreate $loop_dev + vgcreate {2} $loop_dev +fi +""".format(extra_disk_dev, extra_disk_file, + self.cluster.vg, self.cluster.vg_size) + + return [cmd] + + +class DRBD(base.Component): + REQUIRED_PACKAGES = [ + "drbd8-utils", + ] + + def _configure(self): + return [ + ("/etc/modprobe.d/drbd.conf", {}, {}), + ] + + def prepare(self): + return [ + "echo drbd >> /etc/modules", + ] + + @base.run_cmds + def initialize(self): + return [ + "modprobe -rv drbd || true", + "modprobe -v drbd", + ] + + +class CA(base.Component): + REQUIRED_PACKAGES = [ + "openssl" + ] + + alias = constants.CA + service = constants.CA + + def required_components(self): + return [ + HW, SSH, DNS, APT, + ] + + @update_admin + def admin_pre(self): + self.NS.update_ns() + + @base.run_cmds + def prepare(self): + return [ + "mkdir -p /root/ca" + ] + + def _configure(self): + r1 = { + "domain": self.node.domain, + } + return [ + ("/root/create_root_ca.sh", {}, {"mode": 0755}), + ("/root/ca/ca-x509-extensions.cnf", r1, {}), + ("/root/ca/x509-extensions.cnf", r1, {}), + ] + + @base.run_cmds + def initialize(self): + return [ + "/root/create_root_ca.sh" + ] + + +class Ganeti(base.Component): + REQUIRED_PACKAGES = [ + "qemu-kvm", + "python-bitarray", + "bridge-utils", + "snf-ganeti", + "ganeti2", + "ganeti-instance-debootstrap" + ] + + @update_admin + def admin_pre(self): + self.NS.update_ns() + + @base.run_cmds + def check(self): + commands = [ + "getent hosts %s | grep -v ^127" % self.node.hostname, + "hostname -f | grep %s" % self.node.fqdn, + ] + return commands + + def _configure(self): + r = { + "SHARED_GANETI_DIR": config.ganeti_dir, + } + return [ + ("/etc/ganeti/file-storage-paths", r, {}), + ("/etc/default/ganeti-instance-debootstrap", {}, {}), + ] + + def _prepare_net_infra(self): + br = config.common_bridge + return [ + "brctl addbr {0}; ip link set {0} up".format(br) + ] + + @base.run_cmds + def prepare(self): + return [ + "mkdir -p %s/file-storage/" % config.ganeti_dir, + "mkdir -p %s/shared-file-storage/" % config.ganeti_dir, + "sed -i 's/^127.*$/127.0.0.1 localhost/g' /etc/hosts", + ] + self._prepare_net_infra() + + @base.run_cmds + def restart(self): + return ["/etc/init.d/ganeti restart"] + + +class Master(base.Component): + + @property + def fqdn(self): + return self.cluster + + def required_components(self): + return [ + HW, SSH, DNS, DDNS, APT, Mount, Ganeti + ] + + @update_admin + def admin_pre(self): + self.NS.update_ns() + + @base.run_cmds + def check(self): + commands = [ + "host %s" % self.cluster.fqdn, + ] + return commands + + @base.run_cmds + def add_qa_rapi_user(self): + cmd = """ +echo ganeti-qa qa_example_passwd write >> /var/lib/ganeti/rapi/users +""" + return [cmd] + + def _add_rapi_user(self): + user = config.synnefo_user + passwd = config.synnefo_rapi_passwd + x = "%s:Ganeti Remote API:%s" % (user, passwd) + + cmd = """ +cat >> /var/lib/ganeti/rapi/users <<EOF +%s {HA1}$(echo -n %s | openssl md5 | sed 's/^.* //') write +EOF +""" % (user, x) + + return [cmd] + + @base.run_cmds + def add_node(self, info): + add = """ +gnt-node list {0} || gnt-node add --no-ssh-key-check {0} +""".format(info.fqdn) + + mod_vm = """ +gnt-node modify --vm-capable=yes {0} +""".format(info.fqdn) + + mod_master = """ +gnt-node modify --master-capable=yes {0} +""".format(info.fqdn) + + return [add, mod_vm, mod_master] + + @base.run_cmds + def enable_lvm(self): + vg = self.cluster.vg + return [ + # This is needed because MIN_VG_SIZE is constant and set to 20G + # and cluster modify --vg-name may result to: + # volume group 'ganeti' too small + # But this check is made only ff a vm-capable node is found + "gnt-cluster modify --enabled-disk-templates file,ext,plain \ + --vg-name=%s" % vg, + "gnt-cluster modify --ipolicy-disk-template file,ext,plain", + ] + + @base.run_cmds + def enable_drbd(self): + vg = self.cluster.vg + return [ + "gnt-cluster modify --enabled-disk-templates file,ext,plain,drbd \ + --drbd-usermode-helper=/bin/true", + "gnt-cluster modify --ipolicy-disk-template file,ext,plain,drbd", + "gnt-cluster modify --disk-parameters=drbd:metavg=%s" % vg, + "gnt-group modify --disk-parameters=drbd:metavg=%s default" % vg, + ] + + @base.run_cmds + def initialize(self): + std = "cpu-count=1,disk-count=1,disk-size=1024" + std += ",memory-size=128,nic-count=1,spindle-use=1" + + bound_min = "cpu-count=1,disk-count=1,disk-size=512" + bound_min += ",memory-size=128,nic-count=0,spindle-use=1" + + bound_max = "cpu-count=8,disk-count=16,disk-size=1048576" + bound_max += ",memory-size=32768,nic-count=8,spindle-use=12" + + init = """ +gnt-cluster init --enabled-hypervisors=kvm \ + --nic-parameters link={0},mode=bridged \ + --master-netdev {1} \ + --default-iallocator hail \ + --hypervisor-parameters kvm:kernel_path=,vnc_bind_address=0.0.0.0 \ + --no-ssh-init --no-etc-hosts \ + --ipolicy-std-specs {2} \ + --ipolicy-bounds-specs min:{3}/max:{4} \ + --enabled-disk-templates file,ext \ + {5} + """.format(config.common_bridge, self.cluster.netdev, + std, bound_min, bound_max, self.cluster.fqdn) + + modify = "gnt-node modify --vm-capable=no %s" % self.node.fqdn + + return [init, modify] + self._add_rapi_user() + + @base.run_cmds + def restart(self): + return ["/etc/init.d/ganeti restart"] + + @update_admin + @update_cluster_admin + def admin_post(self): + if self.cluster.synnefo: + self.CYCLADES._debug("Adding backend: %s" % self.cluster.fqdn) + self.CYCLADES.add_backend() + self.CYCLADES.list_backends(self.cluster.fqdn) + self.CYCLADES.undrain_backend() + + +class Image(base.Component): + REQUIRED_PACKAGES = [ + "snf-image", + ] + + @base.run_cmds + def check(self): + return ["mkdir -p %s" % config.images_dir] + + @base.run_cmds + def prepare(self): + url = config.debian_base_url + d = config.images_dir + image = "debian_base.diskdump" + return [ + "test -e /tmp/%s || wget -4 %s -O /tmp/%s" % (image, url, image), + "cp /tmp/%s %s/%s" % (image, d, image), + "mv /etc/default/snf-image /etc/default/snf-image.orig", + ] + + def _configure(self): + tmpl = "/etc/default/snf-image" + replace = { + "synnefo_user": config.synnefo_user, + "synnefo_db_passwd": config.synnefo_db_passwd, + "db_node": self.ctx.db.cname, + "image_dir": config.images_dir, + } + return [(tmpl, replace, {})] + + @base.run_cmds + def initialize(self): + # This is done during postinstall phase + # snf-image-update-helper -y + return [] + + +class GTools(base.Component): + REQUIRED_PACKAGES = [ + "snf-cyclades-gtools", + ] + + @base.run_cmds + def check(self): + return ["ping -c1 %s" % self.ctx.mq.cname] + + @base.run_cmds + def prepare(self): + return [ + "sed -i 's/false/true/' /etc/default/snf-ganeti-eventd", + ] + + def _configure(self): + tmpl = "/etc/synnefo/gtools.conf" + replace = { + "synnefo_user": config.synnefo_user, + "synnefo_rabbitmq_passwd": config.synnefo_rabbitmq_passwd, + "mq_node": self.ctx.mq.cname, + } + return [(tmpl, replace, {})] + + @base.run_cmds + def restart(self): + return ["/etc/init.d/snf-ganeti-eventd restart"] + + +class Network(base.Component): + REQUIRED_PACKAGES = [ + "python-nfqueue", + "snf-network", + "nfdhcpd", + ] + + def _configure(self): + r1 = { + "ns_node_ip": self.ctx.ns.ip + } + r2 = { + "common_bridge": config.common_bridge, + "public_iface": self.node.public_iface, + "subnet": config.synnefo_public_network_subnet, + "gateway": config.synnefo_public_network_gateway, + "router_ip": self.ctx.router.ip, + "node_ip": self.node.ip, + } + r3 = { + "domain": self.node.domain, + "server": self.ctx.ns.ip, + "keyfile": config.ddns_private_key, + } + + return [ + ("/etc/nfdhcpd/nfdhcpd.conf", r1, {}), + ("/etc/rc.local", r2, {"mode": 0755}), + ("/etc/default/snf-network", r3, {}), + ] + + @base.run_cmds + def initialize(self): + return ["/etc/init.d/rc.local start"] + + @base.run_cmds + def restart(self): + return ["/etc/init.d/nfdhcpd restart"] + + +class Apache(base.Component): + REQUIRED_PACKAGES = [ + "apache2", + "python-openssl", + ] + + @update_admin + def admin_pre(self): + self.CA.get("/root/ca/cert.pem", "/tmp/cert.pem") + self.put("/tmp/cert.pem", "/etc/ssl/certs/synnefo.pem") + self.CA.get("/root/ca/key.pem", "/tmp/key.pem") + self.put("/tmp/key.pem", "/etc/ssl/private/synnefo.key") + self.CA.get("/root/ca/cacert.pem", "/tmp/cacert.pem") + self.put("/tmp/cacert.pem", "/etc/ssl/certs/synnefo_ca.pem") + + @base.run_cmds + def prepare(self): + return [ + "a2enmod ssl", "a2enmod rewrite", "a2dissite default", + "a2enmod headers", + "a2enmod proxy_http", "a2dismod autoindex", + ] + + def _configure(self): + r1 = {"HOST": self.node.fqdn} + return [ + ("/etc/apache2/sites-available/synnefo", r1, {}), + ("/etc/apache2/sites-available/synnefo-ssl", r1, {}), + ("/root/firefox_cert_override.py", {}, {}) + ] + + @base.run_cmds + def initialize(self): + return [ + "a2ensite synnefo", "a2ensite synnefo-ssl", + ] + + @base.run_cmds + def restart(self): + return [ + "/etc/init.d/apache2 restart", + ] + + +class Gunicorn(base.Component): + REQUIRED_PACKAGES = [ + "python-gevent", + "gunicorn", + ] + + def _configure(self): + r1 = {"HOST": self.node.fqdn} + return [ + ("/etc/gunicorn.d/synnefo", r1, {}), + ] + + @base.run_cmds + def restart(self): + return [ + "/etc/init.d/gunicorn restart", + ] + + +class Common(base.Component): + REQUIRED_PACKAGES = [ + "ntp", + "snf-common", + "snf-branding", + ] + + @base.run_cmds + def prepare(self): + return [ + "mkdir -p %s" % config.mail_dir, + "chmod 777 %s" % config.mail_dir, + ] + + def _configure(self): + r1 = { + "EMAIL_SUBJECT_PREFIX": self.node.hostname, + "domain": self.node.domain, + "HOST": self.node.fqdn, + "MAIL_DIR": config.mail_dir, + } + return [ + ("/etc/synnefo/common.conf", r1, {}), + ] + + @base.run_cmds + def restart(self): + return [ + "/etc/init.d/gunicorn restart", + ] + + +class Webproject(base.Component): + REQUIRED_PACKAGES = [ + "python-psycopg2", + "python-astakosclient", + "snf-django-lib", + "snf-webproject", + ] + + @base.run_cmds + def check(self): + return ["ping -c1 %s" % self.ctx.db.cname] + + def _configure(self): + r1 = { + "synnefo_user": config.synnefo_user, + "synnefo_db_passwd": config.synnefo_db_passwd, + "db_node": self.ctx.db.cname, + "domain": self.node.domain, + "webproject_secret": config.webproject_secret, + } + return [ + ("/etc/synnefo/webproject.conf", r1, {}), + ] + + @base.run_cmds + def restart(self): + return [ + "/etc/init.d/gunicorn restart", + ] + + +class Astakos(base.Component): + REQUIRED_PACKAGES = [ + "snf-astakos-app", + ] + + alias = constants.ASTAKOS + service = constants.ASTAKOS + + def required_components(self): + return [HW, SSH, DNS, APT, Apache, Gunicorn, Common, Webproject] + + @base.run_cmds + def setup_user(self): + self._debug("Setting up user") + return self._set_default_quota() + \ + self._add_user() + self._activate_user() + + @update_admin + def admin_pre(self): + self.NS.update_ns() + self.DB.allow_db_access() + self.DB.restart() + + @property + def conflicts(self): + return [CMS] + + @base.run_cmds + def export_service(self): + f = config.jsonfile + return [ + "snf-manage service-export-astakos > %s" % f + ] + + @base.run_cmds + def import_service(self): + f = config.jsonfile + return [ + "snf-manage service-import --json=%s" % f + ] + + @base.run_cmds + def set_astakos_default_quota(self): + cmd = "snf-manage resource-modify" + return [ + "%s --system-default 2 astakos.pending_app" % cmd, + "%s --project-default 0 astakos.pending_app" % cmd, + ] + + @base.run_cmds + def set_cyclades_default_quota(self): + cmd = "snf-manage resource-modify" + return [ + "%s --system-default 4 cyclades.vm" % cmd, + "%s --system-default 40G cyclades.disk" % cmd, + "%s --system-default 16G cyclades.total_ram" % cmd, + "%s --system-default 8G cyclades.ram" % cmd, + "%s --system-default 32 cyclades.total_cpu" % cmd, + "%s --system-default 16 cyclades.cpu" % cmd, + "%s --system-default 4 cyclades.network.private" % cmd, + "%s --system-default 4 cyclades.floating_ip" % cmd, + "%s --project-default 0 cyclades.vm" % cmd, + "%s --project-default 0 cyclades.disk" % cmd, + "%s --project-default inf cyclades.total_ram" % cmd, + "%s --project-default 0 cyclades.ram" % cmd, + "%s --project-default inf cyclades.total_cpu" % cmd, + "%s --project-default 0 cyclades.cpu" % cmd, + "%s --project-default 0 cyclades.network.private" % cmd, + "%s --project-default 0 cyclades.floating_ip" % cmd, + ] + + @base.run_cmds + def set_pithos_default_quota(self): + cmd = "snf-manage resource-modify" + return [ + "%s --system-default 40G pithos.diskspace" % cmd, + "%s --project-default 0 pithos.diskspace" % cmd, + ] + + @base.run_cmds + def modify_all_quota(self): + cmd = "snf-manage project-modify --all-system-projects --limit" + return [ + "%s pithos.diskspace 40G 40G" % cmd, + "%s astakos.pending_app 2 2" % cmd, + "%s cyclades.vm 4 4" % cmd, + "%s cyclades.disk 40G 40G" % cmd, + "%s cyclades.total_ram 16G 16G" % cmd, + "%s cyclades.ram 8G 8G" % cmd, + "%s cyclades.total_cpu 32 32" % cmd, + "%s cyclades.cpu 16 16" % cmd, + "%s cyclades.network.private 4 4" % cmd, + "%s cyclades.floating_ip 4 4" % cmd, + ] + + @parse(_SERVICE_INFO_RE, _SERVICE_INFO) + @base.run_cmds + def get_services(self, service): + return [ + "snf-manage component-list -o id,name,token" + ] + + def _configure(self): + r1 = { + "ACCOUNTS": self.ctx.astakos.cname, + "domain": self.node.domain, + "CYCLADES": self.ctx.cyclades.cname, + "PITHOS": self.ctx.pithos.cname, + } + return [ + ("/etc/synnefo/astakos.conf", r1, {}), + ] + + @base.run_cmds + def initialize(self): + return [ + "snf-manage syncdb --noinput", + "snf-manage migrate im --delete-ghost-migrations", + "snf-manage migrate quotaholder_app", + "snf-manage migrate oa2", + "snf-manage loaddata groups", + ] + self._astakos_oa2() + self._astakos_register_components() + + def _astakos_oa2(self): + secret = config.oa2_secret + view = "https://%s/pithos/ui/view" % self.ctx.pithos.cname + cmd = "snf-manage oauth2-client-add pithos-view \ + --secret=%s --is-trusted --url %s || true" % (secret, view) + return [cmd] + + def _astakos_register_components(self): + # base urls + cbu = "https://%s/cyclades" % self.ctx.cyclades.cname + pbu = "https://%s/pithos" % self.ctx.pithos.cname + abu = "https://%s/astakos" % self.ctx.astakos.cname + cmsurl = "https://%s/home" % self.ctx.cms.cname + + cmd = "snf-manage component-add" + h = "%s home --base-url %s --ui-url %s" % (cmd, cmsurl, cmsurl) + c = "%s cyclades --base-url %s --ui-url %s/ui" % (cmd, cbu, cbu) + p = "%s pithos --base-url %s --ui-url %s/ui" % (cmd, pbu, pbu) + a = "%s astakos --base-url %s --ui-url %s/ui" % (cmd, abu, abu) + + return [h, c, p, a] + + @base.run_cmds + def add_user(self): + info = ( + config.user_passwd, + config.user_email, + config.user_name, + config.user_lastname, + ) + cmd = "snf-manage user-add --password %s %s %s %s" % info + return [cmd] + + @update_admin + @base.run_cmds + def activate_user(self): + self.DB.get_user_info_from_db(config.user_email) + user_id = context.user_id + return [ + "snf-manage user-modify --verify %s" % user_id, + "snf-manage user-modify --accept %s" % user_id, + ] + + @update_admin + @export_and_import_service + def admin_post(self): + self.set_astakos_default_quota() + + +class CMS(base.Component): + REQUIRED_PACKAGES = [ + "snf-cloudcms" + ] + + alias = constants.CMS + service = constants.CMS + + def required_components(self): + return [HW, SSH, DNS, APT, Apache, Gunicorn, Common, Webproject] + + @update_admin + def admin_pre(self): + self.NS.update_ns() + self.DB.allow_db_access() + self.DB.restart() + + @property + def conflicts(self): + return [Astakos, Pithos, Cyclades] + + def _configure(self): + r1 = { + "ACCOUNTS": self.ctx.astakos.cname + } + r2 = { + "DOMAIN": self.node.domain + } + return [ + ("/etc/synnefo/cms.conf", r1, {}), + ("/tmp/sites.json", r2, {}), + ("/tmp/page.json", {}, {}), + ] + + @base.run_cmds + def initialize(self): + return [ + "snf-manage syncdb", + "snf-manage migrate --delete-ghost-migrations", + "snf-manage loaddata /tmp/sites.json", + "snf-manage loaddata /tmp/page.json", + "snf-manage createsuperuser --username=admin \ + --email=admin@%s --noinput" % self.node.domain, + ] + + @base.run_cmds + def restart(self): + return ["/etc/init.d/gunicorn restart"] + + +class Mount(base.Component): + REQUIRED_PACKAGES = [ + "nfs-common", + ] + + @update_admin + def admin_pre(self): + self.NFS.update_exports() + self.NFS.restart() + + @property + def conflicts(self): + return [NFS] + + @base.run_cmds + def prepare(self): + fstab = """ +cat >> /etc/fstab <<EOF +{0}:{1} {1} nfs defaults,rw,noatime,rsize=131072,wsize=131072 0 0 +EOF +""".format(self.ctx.nfs.cname, config.shared_dir) + + return [ + "mkdir -p %s" % config.shared_dir, + fstab, + ] + + @base.run_cmds + def initialize(self): + return [ + "mount %s" % config.shared_dir + ] + + +class NFS(base.Component): + REQUIRED_PACKAGES = [ + "rpcbind", + "nfs-kernel-server", + ] + + alias = constants.NFS + + def required_components(self): + return [HW, SSH, DNS, APT] + + @property + def conflicts(self): + return [Mount] + + @update_admin + def admin_pre(self): + self.NS.update_ns() + + @base.run_cmds + def prepare(self): + return [ + "mkdir -p %s" % config.shared_dir, + "mkdir -p %s" % config.images_dir, + "mkdir -p %s" % config.ganeti_dir, + "mkdir -p %s" % config.archip_dir, + "cd %s && mkdir {maps,blocks,locks}" % config.archip_dir, + "cd %s && chown archipelago:synnefo {maps,blocks,locks}" % \ + config.archip_dir, + "cd %s && chmod 770 {maps,blocks,locks}" % config.archip_dir, + "cd %s && chmod g+s {maps,blocks,locks}" % config.archip_dir, + ] + + @base.run_cmds + def update_exports(self): + fqdn = self.ctx.admin_node.fqdn + cmd = """ +grep {1} /etc/exports || cat >> /etc/exports <<EOF +{0} {1}(rw,async,no_subtree_check,no_root_squash) +EOF +""".format(config.shared_dir, fqdn) + return [cmd] + + @base.run_cmds + def restart(self): + return [ + "/etc/init.d/nfs-kernel-server restart", + ] + + +class Pithos(base.Component): + REQUIRED_PACKAGES = [ + "snf-pithos-app", + "snf-pithos-webclient", + ] + + alias = constants.PITHOS + service = constants.PITHOS + + def required_components(self): + return [ + HW, SSH, DNS, APT, Apache, Gunicorn, Common, Webproject, + PithosBackend, Archip, ArchipSynnefo + ] + + @update_admin + def admin_pre(self): + self.NS.update_ns() + self.ASTAKOS.get_services(self.service) + self.DB.allow_db_access() + self.DB.restart() + + @property + def conflicts(self): + return [CMS] + + @base.run_cmds + def export_service(self): + f = config.jsonfile + return [ + "snf-manage service-export-pithos > %s" % f + ] + + @base.run_cmds + def prepare(self): + return [ + #FIXME: Workaround until snf-pithos-webclient creates conf + # files properly with root:synnefo + "chown root:synnefo /etc/synnefo/*snf-pithos-webclient*conf", + ] + + def _configure(self): + r1 = { + "ACCOUNTS": self.ctx.astakos.cname, + "PITHOS": self.ctx.pithos.cname, + "db_node": self.ctx.db.cname, + "synnefo_user": config.synnefo_user, + "synnefo_db_passwd": config.synnefo_db_passwd, + "PITHOS_SERVICE_TOKEN": context.service_token, + "oa2_secret": config.oa2_secret, + } + r2 = { + "ACCOUNTS": self.ctx.astakos.cname, + "PITHOS_UI_CLOUDBAR_ACTIVE_SERVICE": context.service_id, + } + + return [ + ("/etc/synnefo/pithos.conf", r1, {}), + ("/etc/synnefo/webclient.conf", r2, {}), + ] + + @base.run_cmds + def initialize(self): + return ["pithos-migrate stamp head"] + + @base.run_cmds + def restart(self): + return [ + "/etc/init.d/gunicorn restart", + ] + + @update_admin + @export_and_import_service + def admin_post(self): + self.ASTAKOS.set_pithos_default_quota() + + +class PithosBackend(base.Component): + REQUIRED_PACKAGES = [ + "snf-pithos-backend", + "python-psycopg2", + ] + + def _configure(self): + r1 = { + "db_node": self.ctx.db.cname, + "synnefo_user": config.synnefo_user, + "synnefo_db_passwd": config.synnefo_db_passwd, + } + + return [ + ("/etc/synnefo/backend.conf", r1, {}), + ] + + +class Cyclades(base.Component): + REQUIRED_PACKAGES = [ + "memcached", + "python-memcache", + "snf-cyclades-app", + ] + + alias = constants.CYCLADES + service = constants.CYCLADES + + def required_components(self): + return [ + HW, SSH, DNS, APT, + Apache, Gunicorn, Common, Webproject, VNC, PithosBackend, + Archip, ArchipSynnefo + ] + + @update_admin + def admin_pre(self): + self.NS.update_ns() + self.ASTAKOS.get_services(self.service) + self.DB.allow_db_access() + self.DB.restart() + + @property + def conflicts(self): + return [CMS] + + def _add_network(self): + subnet = config.synnefo_public_network_subnet + gw = config.synnefo_public_network_gateway + ntype = config.synnefo_public_network_type + link = config.common_bridge + + cmd = """ +snf-manage network-create --subnet={0} --gateway={1} --public \ + --dhcp=True --flavor={2} --mode=bridged --link={3} --name=Internet \ + --floating-ip-pool=True +""".format(subnet, gw, ntype, link) + + return [cmd] + + @base.check_if_testing + def _add_network6(self): + subnet = "babe::/64" + gw = "babe::1" + ntype = config.synnefo_public_network_type + link = config.common_bridge + + cmd = """ +snf-manage network-create --subnet6={0} \ + --gateway6={1} --public --dhcp=True --flavor={2} --mode=bridged \ + --link={3} --name=IPv6PublicNetwork +""".format(subnet, gw, ntype, link) + + return [cmd] + + @base.run_cmds + def export_service(self): + f = config.jsonfile + return [ + "snf-manage service-export-cyclades > %s" % f + ] + + @parse(_BACKEND_INFO_RE, _BACKEND_INFO) + @base.run_cmds + def list_backends(self, cluster): + return [ + "snf-manage backend-list" + ] + + @base.run_cmds + def add_backend(self): + cluster = self.ctx.admin_cluster.fqdn + user = config.synnefo_user + passwd = config.synnefo_rapi_passwd + return [ + "snf-manage backend-add --clustername=%s --user=%s --pass=%s" % + (cluster, user, passwd) + ] + + @base.run_cmds + def undrain_backend(self): + backend_id = context.backend_id + return [ + "snf-manage backend-modify --drained=False %s" % str(backend_id) + ] + + @base.run_cmds + def prepare(self): + return [ + "sed -i 's/false/true/' /etc/default/snf-dispatcher", + ] + + def _configure(self): + r1 = { + "ACCOUNTS": self.ctx.astakos.cname, + "CYCLADES": self.ctx.cyclades.cname, + "mq_node": self.ctx.mq.cname, + "db_node": self.ctx.db.cname, + "synnefo_user": config.synnefo_user, + "synnefo_db_passwd": config.synnefo_db_passwd, + "synnefo_rabbitmq_passwd": config.synnefo_rabbitmq_passwd, + "common_bridge": config.common_bridge, + "domain": self.node.domain, + "CYCLADES_SERVICE_TOKEN": context.service_token, + "STATS": self.ctx.stats.cname, + "STATS_SECRET": config.stats_secret, + "SYNNEFO_VNC_PASSWD": config.synnefo_vnc_passwd, + "CYCLADES_SECRET": config.cyclades_secret, + "SHARED_GANETI_DIR": config.ganeti_dir, + "VNC": self.ctx.vnc.cname, + } + return [ + ("/etc/synnefo/cyclades.conf", r1, {}), + ] + + @base.run_cmds + def initialize(self): + return [ + "snf-manage syncdb", + "snf-manage migrate --delete-ghost-migrations", + "snf-manage pool-create --type=mac-prefix \ + --base=aa:00:0 --size=65536", + "snf-manage pool-create --type=bridge --base=prv --size=20", + ] + self._add_network() + self._add_network6() + + @base.run_cmds + def _create_flavor(self): + cpu = config.flavor_cpu + ram = config.flavor_ram + disk = config.flavor_disk + volume = context.volume_type_id + return [ + "snf-manage flavor-create %s %s %s %s" % (cpu, ram, disk, volume), + ] + + @base.run_cmds + def _create_volume_type(self, template): + cmd = """ +snf-manage volume-type-create --name {0} --disk-template {0} +""".format(template) + return [cmd] + + @parse(_VOLUME_INFO_RE, _VOLUME_INFO) + @base.run_cmds + def list_volume_types(self, template): + return [ + "snf-manage volume-type-list -o id,disk_template --no-headers" + ] + + @base.run_cmds + def restart(self): + return [ + "/etc/init.d/gunicorn restart", + "/etc/init.d/snf-dispatcher restart", + ] + + def create_flavors(self): + templates = config.flavor_storage.split(",") + for t in templates: + self._create_volume_type(t) + self.list_volume_types(t) + self._create_flavor() + + @update_admin + @export_and_import_service + def admin_post(self): + self.create_flavors() + self.ASTAKOS.set_cyclades_default_quota() + + +class VNC(base.Component): + REQUIRED_PACKAGES = [ + "snf-vncauthproxy" + ] + + alias = constants.VNC + service = constants.VNC + + def required_components(self): + return [ + HW, SSH, DNS, APT, + ] + + @update_admin + def admin_pre(self): + self.NS.update_ns() + self.run("mkdir -p /var/lib/vncauthproxy") + self.CA.get("/root/ca/cert.pem", "/tmp/cert.pem") + self.put("/tmp/cert.pem", "/var/lib/vncauthproxy/cert.pem") + self.CA.get("/root/ca/key.pem", "/tmp/key.pem") + self.put("/tmp/key.pem", "/var/lib/vncauthproxy/key.pem") + + @base.run_cmds + def prepare(self): + user = config.synnefo_user + passwd = config.synnefo_vnc_passwd + outdir = "/var/lib/vncauthproxy" + users_file = "%s/users" % outdir + return [ + "mkdir -p %s" % outdir, + "chown vncauthproxy:vncauthproxy %s/*.pem" % outdir, + "vncauthproxy-passwd -p %s %s %s" % (passwd, users_file, user) + ] + + def _configure(self): + r1 = { + "vnc": self.ctx.vnc.cname, + } + return [ + ("/etc/default/vncauthproxy", r1, {}) + ] + + @base.run_cmds + def restart(self): + return [ + "/etc/init.d/vncauthproxy restart" + ] + + +class Admin(base.Component): + REQUIRED_PACKAGES = [ + "python-django-eztables", + "snf-admin-app" + ] + + alias = constants.ADMIN + service = constants.ADMIN + + def required_components(self): + return [ + HW, SSH, DNS, APT, + Apache, Gunicorn, Common, Webproject, + ] + + @update_admin + def admin_pre(self): + self.NS.update_ns() + self.DB.allow_db_access() + self.DB.restart() + f = "/etc/synnefo/astakos.conf" + self.ASTAKOS.get(f, "/tmp/astakos.conf") + self.put("/tmp/astakos.conf", f) + f = "/etc/synnefo/cyclades.conf" + self.CYCLADES.get(f, "/tmp/cyclades.conf") + self.put("/tmp/cyclades.conf", f) + + def _configure(self): + r1 = { + "ADMIN": self.ctx.admin.cname, + } + return [ + ("/etc/synnefo/admin.conf", r1, {}) + ] + + @base.run_cmds + def initialize(self): + return [ + "snf-manage group-add admin" + ] + + @base.run_cmds + def restart(self): + return [ + "/etc/init.d/gunicorn restart" + ] + + @base.run_cmds + def make_user_admin_user(self): + user_id = context.user_id + return [ + "snf-manage user-modify %s --add-group=admin" % user_id + ] + +class Kamaki(base.Component): + REQUIRED_PACKAGES = [ + "python-progress", + "kamaki", + ] + + @update_admin + def admin_pre(self): + self.ASTAKOS.add_user() + self.ASTAKOS.activate_user() + self.DB.get_user_info_from_db(config.user_email) + self.ADMIN.make_user_admin_user() + self.CA.get("/root/ca/cacert.pem", "/tmp/cacert.pem") + self.put("/tmp/cacert.pem", + "/usr/local/share/ca-certificates/Synnefo_Root_CA.crt") + + @base.run_cmds + def prepare(self): + return [ + "update-ca-certificates", + ] + + @base.run_cmds + def initialize(self): + url = "https://%s/astakos/identity/v2.0" % self.ctx.astakos.cname + token = context.user_auth_token + return [ + "kamaki config set cloud.default.url %s" % url, + "kamaki config set cloud.default.token %s" % token, + "kamaki container create images", + ] + + def _fetch_image(self): + url = config.debian_base_url + image = "debian_base.diskdump" + return [ + "test -e /tmp/%s || wget -4 %s -O /tmp/%s" % (image, url, image) + ] + + def _upload_image(self): + image = "debian_base.diskdump" + return [ + "kamaki file upload --container images /tmp/%s %s" % (image, image) + ] + + def _register_image(self): + image = "debian_base.diskdump" + image_location = "/images/%s" % image + cmd = """ + kamaki image register --name "Debian Base" --location {0} \ + --public --disk-format=diskdump \ + --property OSFAMILY=linux --property ROOT_PARTITION=1 \ + --property description="Debian Squeeze Base System" \ + --property size=450M --property kernel=2.6.32 \ + --property GUI="No GUI" --property sortorder=1 \ + --property USERS=root --property OS=debian + """.format(image_location) + return [ + "sleep 5", + cmd + ] + + @base.run_cmds + def test(self): + return self._fetch_image() + self._upload_image() + \ + self._register_image() + + +class Burnin(base.Component): + REQUIRED_PACKAGES = [ + "snf-tools", + ] + + +class Collectd(base.Component): + REQUIRED_PACKAGES = [ + "collectd", + ] + + def _configure(self): + return [ + ("/etc/collectd/collectd.conf", {}, {}), + ] + + @base.run_cmds + def restart(self): + return [ + "/etc/init.d/collectd restart", + ] + + +class Stats(base.Component): + REQUIRED_PACKAGES = [ + "snf-stats-app", + ] + + alias = constants.STATS + + def required_components(self): + return [ + HW, SSH, DNS, APT, + Apache, Gunicorn, Common, Webproject, Collectd + ] + + @update_admin + def admin_pre(self): + self.NS.update_ns() + + @base.run_cmds + def prepare(self): + return [ + "mkdir -p /var/cache/snf-stats-app", + "chmod g+ws /var/cache/snf-stats-app", + "chown synnefo:synnefo /var/cache/snf-stats-app", + ] + + def _configure(self): + r1 = { + "STATS": self.ctx.stats.cname, + "STATS_SECRET": config.stats_secret, + } + return [ + ("/etc/synnefo/stats.conf", r1, {}), + ("/etc/collectd/synnefo-stats.conf", r1, {}), + ] + + @base.run_cmds + def restart(self): + return [ + "/etc/init.d/gunicorn restart", + "/etc/init.d/apache2 restart", + ] + + +class GanetiCollectd(base.Component): + def _configure(self): + r1 = { + "STATS": self.ctx.stats.cname, + "COLLECTD_SECRET": config.collectd_secret, + } + return [ + ("/etc/collectd/passwd", {}, {}), + ("/etc/collectd/synnefo-ganeti.conf", r1, {}), + ] + + +class Archip(base.Component): + REQUIRED_PACKAGES = [ + "librados2", + "blktap-dkms", + "blktap-archipelago-utils", + "archipelago", + "archipelago-dbg", + "archipelago-rados", + "archipelago-rados-dbg", + "libxseg0", + "libxseg0-dbg", + "python-archipelago", + "python-xseg", + ] + + def required_components(self): + return [Mount] + + @base.run_cmds + def prepare(self): + return ["mkdir -p /etc/archipelago"] + + def _configure(self): + r1 = { + "SEGMENT_SIZE": config.segment_size, + "ARCHIP_DIR": config.archip_dir, + } + return [ + ("/etc/archipelago/archipelago.conf", r1, {}) + ] + + @base.run_cmds + def restart(self): + return [ + #FIXME: See https://github.com/grnet/archipelago/pull/44 + "mkdir -p /dev/shm/posixfd", + "chown -R synnefo:synnefo /dev/shm/posixfd", + "archipelago restart", + ] + + +class ArchipSynnefo(base.Component): + REQUIRED_PACKAGES = [] + + @base.run_cmds + def prepare(self): + return [ + "mkdir -p /etc/synnefo/gunicorn-hooks", + "chmod 750 /etc/synnefo/gunicorn-hooks", + "chmod g+s /etc/synnefo/gunicorn-hooks", + ] + + def _configure(self): + r1 = {"HOST": self.node.fqdn} + return [ + ("/etc/gunicorn.d/synnefo-archip", r1, + {"remote": "/etc/gunicorn.d/synnefo"}), + ("/etc/synnefo/gunicorn-hooks/gunicorn-archipelago.py", {}, {}), + ] + + @base.run_cmds + def restart(self): + return [ + "/etc/init.d/gunicorn restart", + ] + + +class ArchipGaneti(base.Component): + REQUIRED_PACKAGES = [ + "archipelago-ganeti", + ] + + +class ExtStorage(base.Component): + @base.run_cmds + def prepare(self): + return ["mkdir -p /usr/local/lib/ganeti/"] + + @base.run_cmds + def initialize(self): + url = "http://code.grnet.gr/git/extstorage" + extdir = "/usr/local/lib/ganeti/extstorage" + return [ + "git clone %s %s" % (url, extdir) + ] + + +class Client(base.Component): + REQUIRED_PACKAGES = [ + "iceweasel" + ] + + alias = constants.CLIENT + + def required_components(self): + return [HW, SSH, DNS, APT, Kamaki, Burnin, Firefox] + + +class GanetiDev(base.Component): + REQUIRED_PACKAGES = [ + "automake", + "bridge-utils", + "cabal-install", + "fakeroot", + "fping", + "ghc", + "ghc-haddock", + "git", + "graphviz", + "hlint", + "hscolour", + "iproute", + "iputils-arping", + "libcurl4-openssl-dev", + "libghc-attoparsec-dev", + "libghc-crypto-dev", + "libghc-curl-dev", + "libghc-haddock-dev", + "libghc-hinotify-dev", + "libghc-hslogger-dev", + "libghc-hunit-dev", + "libghc-json-dev", + "libghc-network-dev", + "libghc-parallel-dev", + "libghc-quickcheck2-dev", + "libghc-regex-pcre-dev", + "libghc-snap-server-dev", + "libghc-temporary-dev", + "libghc-test-framework-dev", + "libghc-test-framework-hunit-dev", + "libghc-test-framework-quickcheck2-dev", + "libghc-base64-bytestring-dev", + "libghc-text-dev", + "libghc-utf8-string-dev", + "libghc-vector-dev", + "libghc-comonad-transformers-dev", + "libpcre3-dev", + "libghc6-zlib-dev", + "libghc-lifted-base-dev", + "libcurl4-openssl-dev", + "shelltestrunner", + "lvm2", + "make", + "ndisc6", + "openssl", + "pandoc", + "pep8", + "pylint", + "python", + "python-bitarray", + "python-coverage", + "python-epydoc", + "python-ipaddr", + "python-openssl", + "python-pip", + "python-pycurl", + "python-pyinotify", + "python-pyparsing", + "python-setuptools", + "python-simplejson", + "python-sphinx", + "python-yaml", + "qemu-kvm", + "socat", + "ssh", + "vim" + ] + + CABAL = [ + "json", + "network", + "parallel", + "utf8-string", + "curl", + "hslogger", + "Crypto", + "hinotify==0.3.2", + "regex-pcre", + "vector", + "lifted-base==0.2.0.3", + "lens==3.10", + "base64-bytestring==1.0.0.1", + ] + + def _cabal(self): + ret = ["cabal update"] + for p in self.CABAL: + ret.append("cabal install %s" % p) + return ret + + @base.run_cmds + def prepare(self): + src = config.src_dir + url1 = "git://git.ganeti.org/ganeti.git" + url2 = "https://code.grnet.gr/git/ganeti-local" + return self._cabal() + [ + "git clone %s %s/ganeti" % (url1, src), + "git clone %s %s/snf-ganeti" % (url2, src) + ] + + def _configure(self): + sample_nodes = [] + for node in self.ctx.cluster_nodes: + n = config.get_info(node=node) + sample_nodes.append({ + "primary": n.fqdn, + "secondary": n.ip, + }) + + repl = { + "CLUSTER_NAME": self.cluster.name, + "VG": self.cluster.vg, + "CLUSTER_NETDEV": self.cluster.netdev, + "NODES": simplejson.dumps(sample_nodes), + "DOMAIN": self.cluster.domain + } + c8 = os.path.join(config.src_dir, "ganeti", "configure-2.8") + c10 = os.path.join(config.src_dir, "ganeti", "configure-2.10") + return [ + ("/root/qa-sample.json", repl, {}), + ("/tmp/configure-2.8", {}, {"remote": c8, "mode": 0755}), + ("/tmp/configure-2.10", {}, {"remote": c10, "mode": 0755}), + ] + + @base.run_cmds + def initialize(self): + d = os.path.join(config.src_dir, "ganeti") + return [ + "cd %s; ./autogen.sh" % d, + "cd %s; ./configure" % d, + ] + + @base.run_cmds + def test(self): + ret = [] + for n in self.ctx.cluster_nodes: + info = config.get_info(node=n) + ret.append("ssh %s date" % info.name) + ret.append("ssh %s date" % info.ip) + ret.append("ssh %s date" % info.fqdn) + return ret + + @update_admin + @update_cluster_admin + def admin_post(self): + self.MASTER.add_qa_rapi_user() + self.NS.add_qa_instances() + + +class Router(base.Component): + REQUIRED_PACKAGES = [ + "iptables" + ] + + +class Firefox(base.Component): + REQUIRED_PACKAGES = [ + "iceweasel", + "libnss3-tools", + ] + + @update_admin + def admin_pre(self): + self.CA.get("/root/ca/cacert.pem", "/tmp/cacert.pem") + self.put("/tmp/cacert.pem", "/tmp/Synnefo_Root_CA.crt") + + @base.run_cmds + def initialize(self): + return [ + "echo 12345678 > /tmp/iceweasel_db_pass", + "certutil -N -d /etc/iceweasel/profile/ -f /tmp/iceweasel_db_pass", + "certutil -A -n synnefo -t TCu -d /etc/iceweasel/profile/ \ + -i /tmp/Synnefo_Root_CA.crt", + ] diff --git a/snf-deploy/snfdeploy/config.py b/snf-deploy/snfdeploy/config.py new file mode 100644 index 0000000000000000000000000000000000000000..98670cd2e62167f23648649c824745797977ee9f --- /dev/null +++ b/snf-deploy/snfdeploy/config.py @@ -0,0 +1,159 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import ConfigParser +import sys +import os +import ipaddr +from snfdeploy.lib import evaluate, get_hostname, get_netinfo, \ + get_default_route, getlist, getbool, disable_color, FQDN + + +config = sys.modules[__name__] + +CONF_FILES = [ + "nodes", "setups", + "synnefo", "ganeti", + "packages", "deploy", "vcluster", + ] + + +def _autoconf(): + return { + "name": get_hostname(), + "ip": get_netinfo()[0], + "public_iface": get_default_route()[1], + } + + +def _read_config(f): + cfg = ConfigParser.ConfigParser() + cfg.optionxform = str + filename = os.path.join(config.confdir, f) + ".conf" + cfg.read(filename) + return cfg + + +def get(setup_or_cluster, role): + assert setup_or_cluster and role + value = config.setups.get(setup_or_cluster, role) + return getlist(value) + + +def get_info(cluster=None, node=None): + if cluster: + return get_cluster_info(cluster) + if node: + return get_node_info(node) + + +def get_single_node_role_info(setup, role): + assert setup and role + nodes = get(setup, role) + assert len(nodes) == 1 + info = get_node_info(nodes[0]) + info.alias = role + return info + + +def get_package(package, os="debian"): + try: + return config.packages.get(os, package) + except ConfigParser.NoOptionError: + return None + + +def print_config(): + for f in CONF_FILES: + getattr(config, f).write(sys.stdout) + + +def get_cluster_info(cluster): + options = dict(config.ganeti.items(cluster)) + info = FQDN(**options) + return info + + +def _get_node_info(node): + info = dict(config.nodes.items(node)) + if config.autoconf: + info.update(_autoconf()) + info.update({ + "node": node + }) + return info + + +def get_node_info(node): + info = _get_node_info(node) + return FQDN(**info) + + +def find_all_nodes(setup): + ret = [] + for op in config.setups.options(setup): + for tgt in get(setup, op): + if config.nodes.has_section(tgt): + ret.append(tgt) + elif config.ganeti.has_section(tgt): + ret += find_all_nodes(tgt) + + return list(set(ret)) + + +def init(args): + config.confdir = args.confdir + config.autoconf = args.autoconf + # Import all .conf files + for f in CONF_FILES: + setattr(config, f, _read_config(f)) + + # This is done here in order to have easy access + # to configuration options + evaluate(config, **config.deploy.defaults()) + evaluate(config, **config.vcluster.defaults()) + evaluate(config, **config.synnefo.defaults()) + + # Override conf file settings if + # --templates-dir and --state-dir args are passed + if args.template_dir: + config.template_dir = args.template_dir + if args.state_dir: + config.state_dir = args.state_dir + + config.dry_run = args.dry_run + config.force = args.force + config.ssh_key = args.ssh_key + config.mem = args.mem + config.vnc = args.vnc + config.smp = args.smp + config.passgen = args.passgen + + config.jsonfile = "/tmp/service.json" + config.ganeti_dir = os.path.join(config.shared_dir, "ganeti") + config.images_dir = os.path.join(config.shared_dir, "images") + config.archip_dir = os.path.join(config.shared_dir, "archip") + config.src_dir = os.path.join(config.shared_dir, "src") + + if args.disable_colors: + disable_color() + + config.testing_vm = getbool(config.testing_vm) + config.use_local_packages = getbool(config.use_local_packages) + + config.net = ipaddr.IPNetwork(config.subnet) + config.all_nodes = config.nodes.sections() + config.all_ips = [get_node_info(node).ip + for node in config.all_nodes] diff --git a/snf-deploy/snfdeploy/constants.py b/snf-deploy/snfdeploy/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..ed8b74d4c9d1655c3b122b7c269c8b5caf8d4d16 --- /dev/null +++ b/snf-deploy/snfdeploy/constants.py @@ -0,0 +1,63 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +NS = "ns" +CA = "ca" +DB = "db" +MQ = "mq" +ASTAKOS = "astakos" +CYCLADES = "cyclades" +ADMIN = "admin" +VNC = "vnc" +PITHOS = "pithos" +CLIENT = "client" +ROUTER = "router" +NFS = "nfs" +CMS = "cms" +STATS = "stats" +CLUSTERS = "clusters" +MASTER = "master" +VMC = "vmc" +DEV = "dev" +BACKEND = "backend" + +VALUE_OK = "ok" +STATUS_FILE = "snf_deploy_status" + +DEFAULT_NODE = None +DEFAULT_CLUSTER = None +DEFAULT_SETUP = "auto" +DUMMY_NODE = "dummy" + +DEFAULT_PASSWD_LENGTH = 10 + +DB_PASSWD = "synnefo_db_passwd" +RAPI_PASSWD = "synnefo_rapi_passwd" +MQ_PASSWD = "synnefo_rabbitmq_passwd" +VNC_PASSWD = "synnefo_vnc_passwd" +CYCLADES_SECRET = "cyclades_secret" +OA2_SECRET = "oa2_secret" +WEBPROJECT_SECRET = "webproject_secret" +STATS_SECRET = "stats_secret" +COLLECTD_SECRET = "collectd_secret" + +# This is used for generating random passwords +ALL_PASSWDS_AND_SECRETS = frozenset([ + DB_PASSWD, RAPI_PASSWD, MQ_PASSWD, + CYCLADES_SECRET, OA2_SECRET, WEBPROJECT_SECRET, STATS_SECRET, + COLLECTD_SECRET, VNC_PASSWD + ]) + +EXTERNAL_PUBLIC_DNS = "8.8.8.8" diff --git a/snf-deploy/snfdeploy/context.py b/snf-deploy/snfdeploy/context.py new file mode 100644 index 0000000000000000000000000000000000000000..98645e493619b987493ecc1ccc89f185e01e7805 --- /dev/null +++ b/snf-deploy/snfdeploy/context.py @@ -0,0 +1,167 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys +import datetime +import ConfigParser +from snfdeploy import constants +from snfdeploy import config +from snfdeploy import status + +context = sys.modules[__name__] + + +class Context(object): + + def __repr__(self): + ret = "[%s]" % datetime.datetime.now().strftime("%H:%M:%S") + ret += " [%s %s]" % (self.node_info.ip, self.node_info.name) + ret += " [%s %s %s %s]" % \ + (self.node, self.role, self.cluster, self.setup) + return ret + + def __init__(self, node=None, role=None, cluster=None, setup=None): + if not node: + node = context.node + if not role: + role = context.role + if not setup: + setup = context.setup + if not cluster: + cluster = context.cluster + self.node = node + self.role = role + self.cluster = cluster + self.setup = setup + self.update_info() + + def update(self, node=None, role=None, cluster=None, setup=None): + if node: + context.node = self.node = node + if role: + context.role = self.role = role + if cluster: + context.cluster = self.cluster = cluster + if setup: + context.setup = self.setup = setup + self.update_info() + + def update_info(self): + self.ns = self._get(constants.NS) + self.ca = self._get(constants.CA) + self.nfs = self._get(constants.NFS) + self.mq = self._get(constants.MQ) + self.db = self._get(constants.DB) + self.astakos = self._get(constants.ASTAKOS) + self.cyclades = self._get(constants.CYCLADES) + self.admin = self._get(constants.ADMIN) + self.vnc = self._get(constants.VNC) + self.pithos = self._get(constants.PITHOS) + self.stats = self._get(constants.STATS) + self.cms = self._get(constants.CMS) + self.router = self._get(constants.ROUTER) + self.client = self._get(constants.CLIENT) + + def _get(self, role): + try: + return config.get_single_node_role_info(self.setup, role) + except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): + return config.get_node_info(constants.DUMMY_NODE) + + @property + def node_info(self): + return config.get_info(node=self.node) + + @property + def cluster_info(self): + return config.get_info(cluster=self.cluster) + + @property + def clusters(self): + return config.get(self.setup, constants.CLUSTERS) + + @property + def masters(self): + return config.get(self.cluster, constants.MASTER) + + @property + def master(self): + info = config.get_single_node_role_info(self.cluster, constants.MASTER) + info.alias = None + return info + + @property + def vmcs(self): + return config.get(self.cluster, constants.VMC) + + @property + def cluster_nodes(self): + return list(set(self.masters + self.vmcs)) + + @property + def all_nodes(self): + return config.find_all_nodes(self.setup) + + @property + def all_ips(self): + l = lambda x: config.get_node_info(x).ip + return [l(n) for n in self.all_nodes] + + def get(self, role): + try: + return config.get(self.setup, role) + except: + return config.get(self.cluster, role) + + +def backup(): + context.node_backup = context.node + context.role_backup = context.role + context.cluster_backup = context.cluster + context.setup_backup = context.setup + + +def restore(): + context.node = context.node_backup + context.role = context.role_backup + context.cluster = context.cluster_backup + context.setup = context.setup_backup + + +def get_passwd(target): + if not config.passgen: + return getattr(config, target) + return status.get_passwd(context.setup, target) + + +def update_passwords(): + if config.passgen: + for p in constants.ALL_PASSWRD_AND_SECRETS: + passwd = status.get_passwd(context.setup, p) + setattr(config, p, passwd) + else: + print "Using passwords found in configuration files" + + +def init(args): + context.node = args.node + context.role = args.role + context.cluster = args.cluster + context.setup = args.setup + context.method = args.method + context.component = args.component + context.target_nodes = args.target_nodes + context.cmd = args.cmd + update_passwords() diff --git a/snf-deploy/snfdeploy/fabfile.py b/snf-deploy/snfdeploy/fabfile.py index d80642daa5de1c74d3533df9297f57a7ac9fca46..19e6722f06376cc771978bf165ba20fb25c1073c 100644 --- a/snf-deploy/snfdeploy/fabfile.py +++ b/snf-deploy/snfdeploy/fabfile.py @@ -1,1506 +1,168 @@ # Too many lines in module pylint: disable-msg=C0302 # Too many arguments (7/5) pylint: disable-msg=R0913 + +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + """ Fabric file for snf-deploy """ from __future__ import with_statement -from fabric.api import hide, env, settings, local, roles -from fabric.operations import run, put, get -import fabric -import re -import os -import shutil -import tempfile -import ast -from snfdeploy.lib import debug, Conf, Env, disable_color -from snfdeploy import massedit - - -def setup_env(args): - """Setup environment""" - print("Loading configuration for synnefo...") - - conf = Conf(args) - env.env = Env(conf) - - env.local = args.autoconf - env.key_inject = args.key_inject - env.password = env.env.password - env.user = env.env.user - env.shell = "/bin/bash -c" - env.key_filename = args.ssh_key - - if args.disable_colors: - disable_color() - - if env.env.cms.hostname in \ - [env.env.accounts.hostname, env.env.cyclades.hostname, - env.env.pithos.hostname]: - env.cms_pass = True - else: - env.cms_pass = False - - if env.env.accounts.hostname in \ - [env.env.cyclades.hostname, env.env.pithos.hostname]: - env.csrf_disable = True - else: - env.csrf_disable = False - - env.roledefs = { - "nodes": env.env.ips, - "ips": env.env.ips, - "accounts": [env.env.accounts.ip], - "cyclades": [env.env.cyclades.ip], - "pithos": [env.env.pithos.ip], - "cms": [env.env.cms.ip], - "mq": [env.env.mq.ip], - "db": [env.env.db.ip], - "mq": [env.env.mq.ip], - "db": [env.env.db.ip], - "ns": [env.env.ns.ip], - "client": [env.env.client.ip], - "router": [env.env.router.ip], - "stats": [env.env.stats.ip], - } - - env.enable_lvm = False - env.enable_drbd = False - if ast.literal_eval(env.env.create_extra_disk) and env.env.extra_disk: - env.enable_lvm = True - env.enable_drbd = True - - env.roledefs.update({ - "ganeti": env.env.cluster_ips, - "master": [env.env.master.ip], - }) - - -def install_package(package): - debug(env.host, " * Installing package %s..." % package) - apt_get = "export DEBIAN_FRONTEND=noninteractive ;" + \ - "apt-get install -y --force-yes " - - host_info = env.env.ips_info[env.host] - env.env.update_packages(host_info.os) - if ast.literal_eval(env.env.use_local_packages): - with settings(warn_only=True): - deb = local("ls %s/%s*%s_*.deb" - % (env.env.packages, package, host_info.os), - capture=True) - if deb: - debug(env.host, - " * Package %s found in %s..." - % (package, env.env.packages)) - try_put(deb, "/tmp/") - try_run("dpkg -i /tmp/%s || " - % os.path.basename(deb) + apt_get + "-f") - try_run("rm /tmp/%s" % os.path.basename(deb)) - return - - info = getattr(env.env, package) - if info in \ - ["squeeze-backports", "squeeze", "stable", - "testing", "unstable", "wheezy"]: - apt_get += " -t %s %s " % (info, package) - elif info: - apt_get += " %s=%s " % (package, info) - else: - apt_get += package - - try_run(apt_get) - - return - - -@roles("ns") -def update_ns_for_ganeti(): - debug(env.host, - "Updating name server entries for backend %s..." - % env.env.cluster.fqdn) - update_arecord(env.env.cluster) - update_ptrrecord(env.env.cluster) - try_run("/etc/init.d/bind9 restart") - - -@roles("ns") -def update_ns_for_node(node): - info = env.env.nodes_info.get(node) - update_arecord(info) - update_ptrrecord(info) - try_run("/etc/init.d/bind9 restart") - - -@roles("ns") -def update_arecord(host): - filename = "/etc/bind/zones/" + env.env.domain - cmd = """ - echo '{0}' >> {1} - """.format(host.arecord, filename) - try_run(cmd) +from fabric.api import env, execute, parallel +from snfdeploy import context +from snfdeploy import constants +from snfdeploy import roles +import copy +import logging -@roles("ns") -def update_cnamerecord(host): - filename = "/etc/bind/zones/" + env.env.domain - cmd = """ - echo '{0}' >> {1} - """.format(host.cnamerecord, filename) - try_run(cmd) +FORMAT = "%(name)s %(funcName)s:%(lineno)d %(message)s" +# Needed to avoid: +# No handlers could be found for logger "paramiko.transport" +logging.basicConfig(format=FORMAT, level=logging.INFO) -@roles("ns") -def update_ptrrecord(host): - filename = "/etc/bind/rev/synnefo.in-addr.arpa.zone" - cmd = """ - echo '{0}' >> {1} - """.format(host.ptrrecord, filename) - try_run(cmd) +def with_ctx(fn): + def wrapper(*args): + ctx = context.Context() + return fn(*args, ctx=ctx) + return wrapper -@roles("nodes") -def apt_get_update(): - debug(env.host, "apt-get update....") - try_run("apt-get update") +def with_cluster(fn): + def wrapper(old_ctx, *args): + ctx = copy.deepcopy(old_ctx) + ctx.update(cluster=env.host) + return fn(ctx, *args) + return wrapper -@roles("ns") -def setup_ns(): - debug(env.host, "Setting up name server..") - #WARNING: this should be remove after we are done - # because gevent does pick randomly nameservers and google does - # not know our setup!!!!! - apt_get_update() - install_package("bind9") - tmpl = "/etc/bind/named.conf.local" - replace = { - "domain": env.env.domain, - } - custom = customize_settings_from_tmpl(tmpl, replace) - try_put(custom, tmpl) +def with_node(fn): + def wrapper(old_ctx, *args): + ctx = copy.deepcopy(old_ctx) + ctx.update(node=env.host) + return fn(ctx, *args) + return wrapper - try_run("mkdir -p /etc/bind/zones") - tmpl = "/etc/bind/zones/example.com" - replace = { - "domain": env.env.domain, - "ns_node_ip": env.env.ns.ip, - } - custom = customize_settings_from_tmpl(tmpl, replace) - remote = "/etc/bind/zones/" + env.env.domain - try_put(custom, remote) - try_run("mkdir -p /etc/bind/rev") - tmpl = "/etc/bind/rev/synnefo.in-addr.arpa.zone" - replace = { - "domain": env.env.domain, - } - custom = customize_settings_from_tmpl(tmpl, replace) - try_put(custom, tmpl) +# Helper methods that are invoked via fabric's execute - tmpl = "/etc/bind/named.conf.options" - replace = { - "NODE_IPS": ";".join(env.env.ips), - } - custom = customize_settings_from_tmpl(tmpl, replace) - try_put(custom, tmpl, mode=0644) +@with_node +def _setup_vmc(ctx): + VMC = roles.get(constants.VMC, ctx) + VMC.setup() - for role, info in env.env.roles.iteritems(): - if role == "ns": - continue - update_cnamerecord(info) - for node, info in env.env.nodes_info.iteritems(): - update_arecord(info) - update_ptrrecord(info) - try_run("/etc/init.d/bind9 restart") +@with_node +def _setup_master(ctx): + MASTER = roles.get(constants.MASTER, ctx) + MASTER.setup() -@roles("nodes") -def check_dhcp(): - debug(env.host, "Checking IPs for synnefo..") - for n, info in env.env.nodes_info.iteritems(): - try_run("ping -c 1 " + info.ip) - - -@roles("nodes") -def check_dns(): - debug(env.host, "Checking fqdns for synnefo..") - for n, info in env.env.nodes_info.iteritems(): - try_run("ping -c 1 " + info.fqdn) - - for n, info in env.env.roles.iteritems(): - try_run("ping -c 1 " + info.fqdn) - - -@roles("nodes") -def check_connectivity(): - debug(env.host, "Checking internet connectivity..") - try_run("ping -c 1 www.google.com") - - -@roles("nodes") -def check_ssh(): - debug(env.host, "Checking password-less ssh..") - for n, info in env.env.nodes_info.iteritems(): - try_run("ssh " + info.fqdn + " date") - - -@roles("ips") -def add_keys(): - if not env.key_inject: - debug(env.host, "Skipping ssh keys injection..") - return - else: - debug(env.host, "Adding rsa/dsa keys..") - try_run("mkdir -p /root/.ssh") - cmd = """ -for f in $(ls /root/.ssh/*); do - cp $f $f.bak -done - """ - try_run(cmd) - files = ["authorized_keys", "id_dsa", "id_dsa.pub", - "id_rsa", "id_rsa.pub"] - for f in files: - tmpl = "/root/.ssh/" + f - replace = {} - custom = customize_settings_from_tmpl(tmpl, replace) - try_put(custom, tmpl, mode=0600) - - cmd = """ -if [ -e /root/.ssh/authorized_keys.bak ]; then - cat /root/.ssh/authorized_keys.bak >> /root/.ssh/authorized_keys -fi - """ - debug(env.host, "Updating exising authorized keys..") - try_run(cmd) - - -@roles("ips") -def setup_resolv_conf(): - debug(env.host, "Tweak /etc/resolv.conf...") - try_run("/etc/init.d/network-manager stop", abort=False) - tmpl = "/etc/dhcp/dhclient-enter-hooks.d/nodnsupdate" - replace = {} - custom = customize_settings_from_tmpl(tmpl, replace) - try_put(custom, tmpl, mode=0644) - try_run("cp /etc/resolv.conf /etc/resolv.conf.bak") - tmpl = "/etc/resolv.conf" - replace = { - "domain": env.env.domain, - "ns_node_ip": env.env.ns.ip, - } - custom = customize_settings_from_tmpl(tmpl, replace) - try: - try_put(custom, tmpl) - cmd = """ - echo "\ -# This has been generated automatically by snf-deploy, at -# $(date). -# The immutable bit (+i attribute) has been used to avoid it being -# overwritten by software such as NetworkManager or resolvconf. -# Use lsattr/chattr to view or modify its file attributes. - - -$(cat {0})" > {0} -""".format(tmpl) - try_run(cmd) - except: - pass - try_run("chattr +i /etc/resolv.conf") - - -@roles("ips") -def setup_hosts(): - debug(env.host, "Tweaking /etc/hosts and ssh_config files...") - try_run("echo StrictHostKeyChecking no >> /etc/ssh/ssh_config") - cmd = "sed -i 's/^127.*$/127.0.0.1 localhost/g' /etc/hosts " - try_run(cmd) - host_info = env.env.ips_info[env.host] - cmd = "hostname %s" % host_info.hostname - try_run(cmd) - cmd = "echo %s > /etc/hostname" % host_info.hostname - try_run(cmd) - - -def try_run(cmd, abort=True): - try: - if env.local: - return local(cmd, capture=True) - else: - return run(cmd) - except BaseException as e: - if abort: - fabric.utils.abort(e) - else: - debug(env.host, "WARNING: command failed. Continuing anyway...") - - -def try_put(local_path=None, remote_path=None, abort=True, **kwargs): - try: - put(local_path=local_path, remote_path=remote_path, **kwargs) - except BaseException as e: - if abort: - fabric.utils.abort(e) - else: - debug(env.host, "WARNING: command failed. Continuing anyway...") - - -def try_get(remote_path, local_path=None, abort=True, **kwargs): - try: - get(remote_path, local_path=local_path, **kwargs) - except BaseException as e: - if abort: - fabric.utils.abort(e) - else: - debug(env.host, "WARNING: command failed. Continuing anyway...") +@with_node +def _setup_role(ctx, role): + ROLE = roles.get(role, ctx) + ROLE.setup() -def create_bridges(): - debug(env.host, " * Creating bridges...") - install_package("bridge-utils") - cmd = """ - brctl addbr {0} ; ip link set {0} up - """.format(env.env.common_bridge) - try_run(cmd) - - -def connect_bridges(): - debug(env.host, " * Connecting bridges...") - #cmd = """ - #brctl addif {0} {1} - #""".format(env.env.common_bridge, env.env.public_iface) - #try_run(cmd) - - -@roles("ganeti") -def setup_net_infra(): - debug(env.host, "Setup networking infrastracture..") - create_bridges() - connect_bridges() - - -@roles("ganeti") -def setup_lvm(): - debug(env.host, "create volume group %s for ganeti.." % env.env.vg) - if env.enable_lvm: - install_package("lvm2") - cmd = """ - pvcreate {0} - vgcreate {1} {0} - """.format(env.env.extra_disk, env.env.vg) - try_run(cmd) - - -def customize_settings_from_tmpl(tmpl, replace): - debug(env.host, " * Customizing template %s..." % tmpl) - local = env.env.templates + tmpl - _, custom = tempfile.mkstemp() - shutil.copyfile(local, custom) - for k, v in replace.iteritems(): - regex = "re.sub('%{0}%', '{1}', line)".format(k.upper(), v) - massedit.edit_files([custom], [regex], dry_run=False) - - return custom - - -@roles("nodes") -def setup_apt(): - debug(env.host, "Setting up apt sources...") - install_package("curl") - cmd = """ - echo 'APT::Install-Suggests "false";' >> /etc/apt/apt.conf - curl -k https://dev.grnet.gr/files/apt-grnetdev.pub | apt-key add - - """ - try_run(cmd) - host_info = env.env.ips_info[env.host] - if host_info.os == "squeeze": - tmpl = "/etc/apt/sources.list.d/synnefo.squeeze.list" - else: - tmpl = "/etc/apt/sources.list.d/synnefo.wheezy.list" - replace = {} - custom = customize_settings_from_tmpl(tmpl, replace) - try_put(custom, tmpl) - apt_get_update() - - -@roles("cyclades", "cms", "pithos", "accounts") -def restart_services(): - debug(env.host, " * Restarting apache2 and gunicorn...") - try_run("/etc/init.d/gunicorn restart") - try_run("/etc/init.d/apache2 restart") - - -def setup_gunicorn(): - debug(env.host, " * Setting up gunicorn...") - install_package("gunicorn") - try_run("chown root.www-data /var/log/gunicorn") - tmpl = "/etc/gunicorn.d/synnefo" - replace = {} - custom = customize_settings_from_tmpl(tmpl, replace) - try_put(custom, tmpl, mode=0644) - try_run("/etc/init.d/gunicorn restart") - - -def setup_apache(): - debug(env.host, " * Setting up apache2...") - host_info = env.env.ips_info[env.host] - install_package("apache2") - tmpl = "/etc/apache2/sites-available/synnefo" - replace = { - "HOST": host_info.fqdn, - } - custom = customize_settings_from_tmpl(tmpl, replace) - try_put(custom, tmpl) - tmpl = "/etc/apache2/sites-available/synnefo-ssl" - custom = customize_settings_from_tmpl(tmpl, replace) - try_put(custom, tmpl) - cmd = """ - a2enmod ssl - a2enmod rewrite - a2dissite default - a2ensite synnefo - a2ensite synnefo-ssl - a2enmod headers - a2enmod proxy_http - a2dismod autoindex - """ - try_run(cmd) - try_run("/etc/init.d/apache2 restart") - - -@roles("mq") -def setup_mq(): - debug(env.host, "Setting up RabbitMQ...") - install_package("rabbitmq-server") - cmd = """ - rabbitmqctl add_user {0} {1} - rabbitmqctl set_permissions {0} ".*" ".*" ".*" - rabbitmqctl delete_user guest - rabbitmqctl set_user_tags {0} administrator - """.format(env.env.synnefo_user, env.env.synnefo_rabbitmq_passwd) - try_run(cmd) - try_run("/etc/init.d/rabbitmq-server restart") - - -@roles("db") -def allow_access_in_db(ip, user="all", method="md5"): - cmd = """ - pg_hba=$(ls /etc/postgresql/*/main/pg_hba.conf) - echo host all {0} {1}/32 {2} >> $pg_hba - """.format(user, ip, method) - try_run(cmd) - cmd = """ - pg_hba=$(ls /etc/postgresql/*/main/pg_hba.conf) - sed -i 's/\(host.*127.0.0.1.*\)md5/\\1trust/' $pg_hba - """ - try_run(cmd) - try_run("/etc/init.d/postgresql restart") - - -@roles("db") -def setup_db(): - debug(env.host, "Setting up DataBase server...") - install_package("postgresql") - - tmpl = "/tmp/db-init.psql" - replace = { - "synnefo_user": env.env.synnefo_user, - "synnefo_db_passwd": env.env.synnefo_db_passwd, - } - custom = customize_settings_from_tmpl(tmpl, replace) - try_put(custom, tmpl) - cmd = 'su - postgres -c "psql -w -f %s" ' % tmpl - try_run(cmd) - cmd = """ - conf=$(ls /etc/postgresql/*/main/postgresql.conf) - echo "listen_addresses = '*'" >> $conf - """ - try_run(cmd) - - if env.env.testing_vm: - cmd = """ - conf=$(ls /etc/postgresql/*/main/postgresql.conf) - echo "fsync=off\nsynchronous_commit=off\nfull_page_writes=off" >> $conf - """ - try_run(cmd) - - allow_access_in_db(env.host, "all", "trust") - try_run("/etc/init.d/postgresql restart") - - -@roles("db") -def destroy_db(): - try_run("""su - postgres -c ' psql -w -c "drop database snf_apps" '""") - try_run("""su - postgres -c ' psql -w -c "drop database snf_pithos" '""") - - -def setup_webproject(): - debug(env.host, " * Setting up snf-webproject...") - with settings(hide("everything")): - try_run("ping -c1 " + env.env.db.ip) - setup_common() - install_package("snf-webproject") - install_package("python-psycopg2") - install_package("python-gevent") - install_package("python-django") - tmpl = "/etc/synnefo/webproject.conf" - replace = { - "synnefo_user": env.env.synnefo_user, - "synnefo_db_passwd": env.env.synnefo_db_passwd, - "db_node": env.env.db.ip, - "domain": env.env.domain, - } - custom = customize_settings_from_tmpl(tmpl, replace) - try_put(custom, tmpl, mode=0644) - with settings(host_string=env.env.db.ip): - host_info = env.env.ips_info[env.host] - allow_access_in_db(host_info.ip, "all", "trust") - try_run("/etc/init.d/gunicorn restart") - - -def setup_common(): - debug(env.host, " * Setting up snf-common...") - host_info = env.env.ips_info[env.host] - install_package("python-objpool") - install_package("snf-common") - install_package("python-astakosclient") - install_package("snf-django-lib") - install_package("snf-branding") - tmpl = "/etc/synnefo/common.conf" - replace = { - #FIXME: - "EMAIL_SUBJECT_PREFIX": env.host, - "domain": env.env.domain, - "HOST": host_info.fqdn, - "MAIL_DIR": env.env.mail_dir, - } - custom = customize_settings_from_tmpl(tmpl, replace) - try_put(custom, tmpl, mode=0644) - try_run("mkdir -p {0}; chmod 777 {0}".format(env.env.mail_dir)) - try_run("/etc/init.d/gunicorn restart") - - -@roles("accounts") -def astakos_loaddata(): - debug(env.host, " * Loading initial data to astakos...") - cmd = """ - snf-manage loaddata groups - """ - try_run(cmd) - - -@roles("accounts") -def astakos_register_components(): - debug(env.host, " * Register services in astakos...") - - cyclades_base_url = "https://%s/cyclades" % env.env.cyclades.fqdn - pithos_base_url = "https://%s/pithos" % env.env.pithos.fqdn - astakos_base_url = "https://%s/astakos" % env.env.accounts.fqdn - - cmd = """ - snf-manage component-add "home" --ui-url https://{0} - snf-manage component-add "cyclades" --base-url {1} --ui-url {1}/ui - snf-manage component-add "pithos" --base-url {2} --ui-url {2}/ui - snf-manage component-add "astakos" --base-url {3} --ui-url {3}/ui - """.format(env.env.cms.fqdn, cyclades_base_url, - pithos_base_url, astakos_base_url) - try_run(cmd) - - -@roles("accounts") -def astakos_register_pithos_view(): - debug(env.host, " * Register pithos view as oauth2 client...") - - pithos_base_url = "https://%s/pithos" % env.env.pithos.fqdn - - cmd = """ - snf-manage oauth2-client-add pithos-view --secret={0} --is-trusted \ - --url {1} - """.format(env.env.oa2_secret, '%s/ui/view' % pithos_base_url) - try_run(cmd) - - -@roles("accounts") -def add_user(): - debug(env.host, " * adding user %s to astakos..." % env.env.user_email) - email = env.env.user_email - name = env.env.user_name - lastname = env.env.user_lastname - passwd = env.env.user_passwd - cmd = """ - snf-manage user-add {0} {1} {2} - """.format(email, name, lastname) - try_run(cmd) - with settings(host_string=env.env.db.ip): - uid, user_auth_token, user_uuid = get_auth_token_from_db(email) - cmd = """ - snf-manage user-modify --password {0} {1} - """.format(passwd, uid) - try_run(cmd) - - -@roles("accounts") -def activate_user(user_email=None): - if not user_email: - user_email = env.env.user_email - debug(env.host, " * Activate user %s..." % user_email) - with settings(host_string=env.env.db.ip): - uid, user_auth_token, user_uuid = get_auth_token_from_db(user_email) - - cmd = """ - snf-manage user-modify --verify {0} - snf-manage user-modify --accept {0} - """.format(uid) - try_run(cmd) - - -@roles("accounts") -def setup_astakos(): - debug(env.host, "Setting up snf-astakos-app...") - setup_gunicorn() - setup_apache() - setup_webproject() - install_package("python-django-south") - install_package("snf-astakos-app") - install_package("kamaki") - - tmpl = "/etc/synnefo/astakos.conf" - replace = { - "ACCOUNTS": env.env.accounts.fqdn, - "domain": env.env.domain, - "CYCLADES": env.env.cyclades.fqdn, - "PITHOS": env.env.pithos.fqdn, - } - custom = customize_settings_from_tmpl(tmpl, replace) - try_put(custom, tmpl, mode=0644) - if env.csrf_disable: - cmd = """ -cat <<EOF >> /etc/synnefo/astakos.conf -try: - MIDDLEWARE_CLASSES.remove('django.middleware.csrf.CsrfViewMiddleware') -except: - pass -EOF -""" - try_run(cmd) - - try_run("/etc/init.d/gunicorn restart") +@with_cluster +def _setup_cluster(ctx): + execute(_setup_master, ctx, hosts=ctx.masters) + execute(_setup_vmc, ctx, hosts=ctx.vmcs) - cmd = """ - snf-manage syncdb --noinput - snf-manage migrate im --delete-ghost-migrations - snf-manage migrate quotaholder_app - snf-manage migrate oa2 - """ - try_run(cmd) +# Helper method that get a context snapshot and +# invoke fabric's execute with proper host argument -@roles("accounts") -def get_service_details(service="pithos"): - debug(env.host, - " * Getting registered details for %s service..." % service) - result = try_run("snf-manage component-list -o id,name,token") - r = re.compile(r".*%s.*" % service, re.M) - service_id, _, service_token = r.search(result).group().split() - # print("%s: %s %s" % (service, service_id, service_token)) - return (service_id, service_token) +@with_ctx +def setup_role(role, ctx=None): + execute(_setup_role, ctx, role, hosts=ctx.get(role)) -@roles("db") -def get_auth_token_from_db(user_email=None): - if not user_email: - user_email = env.env.user_email - debug(env.host, - " * Getting authentication token and uuid for user %s..." - % user_email) - cmd = """ -echo "select id, auth_token, uuid, email from auth_user, im_astakosuser \ -where auth_user.id = im_astakosuser.user_ptr_id and auth_user.email = '{0}';" \ -> /tmp/psqlcmd -su - postgres -c "psql -w -d snf_apps -f /tmp/psqlcmd" -""".format(user_email) +@with_ctx +def setup_cluster(ctx=None): + execute(_setup_cluster, ctx, hosts=ctx.clusters) - result = try_run(cmd) - r = re.compile(r"(\d+)[ |]*(\S+)[ |]*(\S+)[ |]*" + user_email, re.M) - match = r.search(result) - uid, user_auth_token, user_uuid = match.groups() - # print("%s: %s %s %s" % ( user_email, uid, user_auth_token, user_uuid)) - return (uid, user_auth_token, user_uuid) +def setup_synnefo(): + setup_role(constants.NS) + setup_role(constants.CA) + setup_role(constants.NFS) + setup_role(constants.DB) + setup_role(constants.MQ) + setup_role(constants.ASTAKOS) + setup_role(constants.PITHOS) + setup_role(constants.VNC) + setup_role(constants.CYCLADES) + setup_role(constants.ADMIN) + setup_role(constants.CMS) -@roles("cms") -def cms_loaddata(): - debug(env.host, " * Loading cms initial data...") - if env.cms_pass: - debug(env.host, "Aborting. Prerequisites not met.") - return - tmpl = "/tmp/sites.json" - replace = {} - custom = customize_settings_from_tmpl(tmpl, replace) - try_put(custom, tmpl) + setup_cluster() - tmpl = "/tmp/page.json" - replace = {} - custom = customize_settings_from_tmpl(tmpl, replace) - try_put(custom, tmpl) + setup_role(constants.STATS) + setup_role(constants.CLIENT) - cmd = """ - snf-manage loaddata /tmp/sites.json - snf-manage loaddata /tmp/page.json - snf-manage createsuperuser --username=admin --email=admin@{0} --noinput - """.format(env.env.domain) - try_run(cmd) - -@roles("cms") -def setup_cms(): - debug(env.host, "Setting up cms...") - if env.cms_pass: - debug(env.host, "Aborting. Prerequisites not met.") - return - with settings(hide("everything")): - try_run("ping -c1 accounts." + env.env.domain) - setup_gunicorn() - setup_apache() - setup_webproject() - install_package("snf-cloudcms") - - tmpl = "/etc/synnefo/cms.conf" - replace = { - "ACCOUNTS": env.env.accounts.fqdn, - } - custom = customize_settings_from_tmpl(tmpl, replace) - try_put(custom, tmpl, mode=0644) - try_run("/etc/init.d/gunicorn restart") - - cmd = """ - snf-manage syncdb - snf-manage migrate --delete-ghost-migrations - """.format(env.env.domain) - try_run(cmd) - - -def setup_nfs_dirs(): - debug(env.host, " * Creating NFS mount point for pithos and ganeti...") - cmd = """ - mkdir -p {0} - cd {0} - mkdir -p data - chown www-data:www-data data - chmod g+ws data - mkdir -p {1} - """.format(env.env.pithos_dir, env.env.image_dir) - try_run(cmd) - - -@roles("nodes") -def setup_nfs_clients(): - if env.host == env.env.pithos.ip: - return - - host_info = env.env.ips_info[env.host] - debug(env.host, " * Mounting pithos NFS mount point...") - with settings(hide("everything")): - try_run("ping -c1 " + env.env.pithos.hostname) - with settings(host_string=env.env.pithos.ip): - update_nfs_exports(host_info.ip) - - install_package("nfs-common") - for d in [env.env.pithos_dir, env.env.image_dir]: - try_run("mkdir -p " + d) - cmd = """ -echo "{0}:{1} {1} nfs defaults,rw,noatime,rsize=131072,\ -wsize=131072,timeo=14,intr,noacl" >> /etc/fstab -""".format(env.env.pithos.ip, d) - try_run(cmd) - try_run("mount " + d) - - -@roles("pithos") -def update_nfs_exports(ip): - tmpl = "/tmp/exports" - replace = { - "pithos_dir": env.env.pithos_dir, - "image_dir": env.env.image_dir, - "ip": ip, - } - custom = customize_settings_from_tmpl(tmpl, replace) - try_put(custom, tmpl) - try_run("cat %s >> /etc/exports" % tmpl) - try_run("/etc/init.d/nfs-kernel-server restart") - - -@roles("pithos") -def setup_nfs_server(): - debug(env.host, " * Setting up NFS server for pithos...") - setup_nfs_dirs() - install_package("nfs-kernel-server") - - -@roles("pithos") -def setup_pithos(): - debug(env.host, "Setting up snf-pithos-app...") - with settings(hide("everything")): - try_run("ping -c1 accounts." + env.env.domain) - try_run("ping -c1 " + env.env.db.ip) - setup_gunicorn() - setup_apache() - setup_webproject() - - with settings(host_string=env.env.accounts.ip): - service_id, service_token = get_service_details("pithos") - - install_package("kamaki") - install_package("snf-pithos-backend") - install_package("snf-pithos-app") - tmpl = "/etc/synnefo/pithos.conf" - replace = { - "ACCOUNTS": env.env.accounts.fqdn, - "PITHOS": env.env.pithos.fqdn, - "db_node": env.env.db.ip, - "synnefo_user": env.env.synnefo_user, - "synnefo_db_passwd": env.env.synnefo_db_passwd, - "pithos_dir": env.env.pithos_dir, - "PITHOS_SERVICE_TOKEN": service_token, - "oa2_secret": env.env.oa2_secret, - } - custom = customize_settings_from_tmpl(tmpl, replace) - try_put(custom, tmpl, mode=0644) - try_run("/etc/init.d/gunicorn restart") - - install_package("snf-pithos-webclient") - tmpl = "/etc/synnefo/webclient.conf" - replace = { - "ACCOUNTS": env.env.accounts.fqdn, - "PITHOS_UI_CLOUDBAR_ACTIVE_SERVICE": service_id, - } - custom = customize_settings_from_tmpl(tmpl, replace) - try_put(custom, tmpl, mode=0644) - - try_run("/etc/init.d/gunicorn restart") - #TOFIX: the previous command lets pithos-backend create blocks and maps - # with root owner - try_run("chown -R www-data:www-data %s/data " % env.env.pithos_dir) - try_run("pithos-migrate stamp head") - #try_run("pithos-migrate upgrade head") - - -@roles("ganeti") def setup_ganeti(): - debug(env.host, "Setting up snf-ganeti...") - node_info = env.env.ips_info[env.host] - with settings(hide("everything")): - #if env.enable_lvm: - # try_run("vgs " + env.env.vg) - try_run("getent hosts " + env.env.cluster.fqdn) - try_run("getent hosts %s | grep -v ^127" % env.host) - try_run("hostname -f | grep " + node_info.fqdn) - #try_run("ip link show " + env.env.common_bridge) - #try_run("ip link show " + env.env.common_bridge) - #try_run("apt-get update") - install_package("qemu-kvm") - install_package("python-bitarray") - install_package("ganeti-haskell") - install_package("ganeti-htools") - install_package("snf-ganeti") - try_run("mkdir -p /srv/ganeti/file-storage/") - cmd = """ -cat <<EOF > /etc/ganeti/file-storage-paths -/srv/ganeti/file-storage -/srv/ganeti/shared-file-storage -EOF -""" - try_run(cmd) - - -@roles("master") -def add_rapi_user(): - debug(env.host, " * Adding RAPI user to Ganeti backend...") - cmd = """ - echo -n "{0}:Ganeti Remote API:{1}" | openssl md5 | sed 's/^.* //' - """.format(env.env.synnefo_user, env.env.synnefo_rapi_passwd) - result = try_run(cmd) - if result.startswith("(stdin)= "): - result = result.split("(stdin)= ")[1] - cmd = """ - echo "{0} {1}{2} write" >> /var/lib/ganeti/rapi/users - """.format(env.env.synnefo_user, '{ha1}', result) - try_run(cmd) - try_run("/etc/init.d/ganeti restart") + setup_role(constants.NS) + setup_role(constants.NFS) + setup_cluster() -@roles("master") -def add_nodes(): - nodes = env.env.cluster_nodes.split(",") - nodes.remove(env.env.master_node) - debug(env.host, " * Adding nodes to Ganeti backend...") - for n in nodes: - add_node(n) +@with_cluster +def _setup_qa(ctx): + setup_role(constants.NS) + setup_role(constants.NFS) + setup_cluster() + setup_role(constants.DEV) -@roles("master") -def add_node(node): - node_info = env.env.nodes_info[node] - debug(env.host, " * Adding node %s to Ganeti backend..." % node_info.fqdn) - cmd = "gnt-node add --no-ssh-key-check --master-capable=yes " + \ - "--vm-capable=yes " + node_info.fqdn - try_run(cmd) +@with_ctx +def setup_qa(ctx=None): + execute(_setup_qa, ctx, hosts=ctx.clusters) -@roles("ganeti") -def enable_drbd(): - if env.enable_drbd: - debug(env.host, " * Enabling DRBD...") - install_package("drbd8-utils") - try_run("modprobe drbd minor_count=255 usermode_helper=/bin/true") - try_run("echo drbd minor_count=255 usermode_helper=/bin/true " + - ">> /etc/modules") +@with_ctx +def setup(ctx=None): + if context.node: + if context.component: + C = roles.get(context.component, ctx) + elif context.role: + C = roles.get(context.role, ctx) + if context.method: + fn = getattr(C, context.method) + fn() + else: + C.setup() -@roles("master") -def setup_drbd_dparams(): - if env.enable_drbd: - debug(env.host, - " * Twicking drbd related disk parameters in Ganeti...") - cmd = """ - gnt-cluster modify --disk-parameters=drbd:metavg={0} - gnt-group modify --disk-parameters=drbd:metavg={0} default - """.format(env.env.vg) - try_run(cmd) + elif context.cluster: + _setup_cluster(ctx) -@roles("master") -def enable_lvm(): - if env.enable_lvm: - debug(env.host, " * Enabling LVM...") - cmd = """ - gnt-cluster modify --vg-name={0} - """.format(env.env.vg) - try_run(cmd) +@with_ctx +def run(ctx=None): + if context.target_nodes: + nodes = context.target_nodes.split(",") else: - debug(env.host, " * Disabling LVM...") - try_run("gnt-cluster modify --no-lvm-storage") - - -@roles("master") -def destroy_cluster(): - debug(env.host, " * Destroying Ganeti cluster...") - #TODO: remove instances first - allnodes = env.env.cluster_hostnames[:] - allnodes.remove(env.host) - for n in allnodes: - host_info = env.env.ips_info[env.host] - debug(env.host, " * Removing node %s..." % n) - cmd = "gnt-node remove " + host_info.fqdn - try_run(cmd) - try_run("gnt-cluster destroy --yes-do-it") - - -@roles("master") -def init_cluster(): - debug(env.host, " * Initializing Ganeti backend...") - # extra = "" - # if env.enable_lvm: - # extra += " --vg-name={0} ".format(env.env.vg) - # else: - # extra += " --no-lvm-storage " - # if not env.enable_drbd: - # extra += " --no-drbd-storage " - extra = " --no-lvm-storage --no-drbd-storage " - cmd = """ - gnt-cluster init --enabled-hypervisors=kvm \ - {0} \ - --nic-parameters link={1},mode=bridged \ - --master-netdev {2} \ - --default-iallocator hail \ - --specs-nic-count min=0,max=8 \ - --hypervisor-parameters kvm:kernel_path=,vnc_bind_address=0.0.0.0 \ - --no-ssh-init --no-etc-hosts \ - {3} - """.format(extra, env.env.common_bridge, - env.env.cluster_netdev, env.env.cluster.fqdn) - try_run(cmd) - cmd = """gnt-cluster modify --enabled-disk-templates file,plain,ext""" - try_run(cmd) - - -@roles("ganeti") -def debootstrap(): - install_package("ganeti-instance-debootstrap") - - -@roles("ganeti") -def setup_image_host(): - debug(env.host, "Setting up snf-image...") - install_package("snf-pithos-backend") - install_package("snf-image") - try_run("mkdir -p %s" % env.env.image_dir) - tmpl = "/etc/default/snf-image" - replace = { - "synnefo_user": env.env.synnefo_user, - "synnefo_db_passwd": env.env.synnefo_db_passwd, - "pithos_dir": env.env.pithos_dir, - "db_node": env.env.db.ip, - "image_dir": env.env.image_dir, - } - custom = customize_settings_from_tmpl(tmpl, replace) - try_put(custom, tmpl) - - -@roles("ganeti") -def setup_image_helper(): - debug(env.host, " * Updating helper image...") - cmd = """ - snf-image-update-helper -y - """ - try_run(cmd) - - -@roles("ganeti") -def setup_gtools(): - debug(env.host, " * Setting up snf-cyclades-gtools...") - with settings(hide("everything")): - try_run("ping -c1 " + env.env.mq.ip) - setup_common() - install_package("snf-cyclades-gtools") - tmpl = "/etc/synnefo/gtools.conf" - replace = { - "synnefo_user": env.env.synnefo_user, - "synnefo_rabbitmq_passwd": env.env.synnefo_rabbitmq_passwd, - "mq_node": env.env.mq.ip, - } - custom = customize_settings_from_tmpl(tmpl, replace) - try_put(custom, tmpl) - - cmd = """ - sed -i 's/false/true/' /etc/default/snf-ganeti-eventd - /etc/init.d/snf-ganeti-eventd start - """ - try_run(cmd) - - -@roles("ganeti") -def setup_iptables(): - debug(env.host, " * Setting up iptables to mangle DHCP requests...") - cmd = """ - iptables -t mangle -A PREROUTING -i br+ -p udp -m udp --dport 67 \ - -j NFQUEUE --queue-num 42 - iptables -t mangle -A PREROUTING -i tap+ -p udp -m udp --dport 67 \ - -j NFQUEUE --queue-num 42 - iptables -t mangle -A PREROUTING -i prv+ -p udp -m udp --dport 67 \ - -j NFQUEUE --queue-num 42 - - ip6tables -t mangle -A PREROUTING -i br+ -p ipv6-icmp -m icmp6 \ - --icmpv6-type 133 -j NFQUEUE --queue-num 43 - ip6tables -t mangle -A PREROUTING -i br+ -p ipv6-icmp -m icmp6 \ - --icmpv6-type 135 -j NFQUEUE --queue-num 44 - """ - try_run(cmd) - - -@roles("ganeti") -def setup_network(): - debug(env.host, - "Setting up networking for Ganeti instances (nfdhcpd, etc.)...") - install_package("python-nfqueue") - install_package("nfdhcpd") - tmpl = "/etc/nfdhcpd/nfdhcpd.conf" - replace = { - "ns_node_ip": env.env.ns.ip - } - custom = customize_settings_from_tmpl(tmpl, replace) - try_put(custom, tmpl) - try_run("/etc/init.d/nfdhcpd restart") - - install_package("snf-network") - cmd = """ -sed -i 's/MAC_MASK.*/MAC_MASK = ff:ff:f0:00:00:00/' /etc/default/snf-network - """ - try_run(cmd) - - -@roles("router") -def setup_router(): - debug(env.host, " * Setting up internal router for NAT...") - cmd = """ - echo 1 > /proc/sys/net/ipv4/ip_forward - iptables -t nat -A POSTROUTING -s {0} -o {3} -j MASQUERADE - ip addr add {1} dev {2} - ip route add {0} dev {2} src {1} - """.format(env.env.synnefo_public_network_subnet, - env.env.synnefo_public_network_gateway, - env.env.common_bridge, env.env.public_iface) - try_run(cmd) - - -@roles("cyclades") -def cyclades_loaddata(): - debug(env.host, " * Loading initial data for cyclades...") - try_run("snf-manage flavor-create %s %s %s %s" % (env.env.flavor_cpu, - env.env.flavor_ram, - env.env.flavor_disk, - env.env.flavor_storage)) - #run("snf-manage loaddata flavors") - - -@roles("ganeti", "stats") -def setup_collectd(): - install_package("collectd") - tmpl = "/etc/collectd/collectd.conf" - replace = {} - custom = customize_settings_from_tmpl(tmpl, replace) - try_put(custom, tmpl, mode=0644) - - -@roles("ganeti") -def setup_ganeti_collectd(): - setup_collectd() - - tmpl = "/etc/collectd/passwd" - replace = {} - custom = customize_settings_from_tmpl(tmpl, replace) - try_put(custom, tmpl, mode=0644) - - tmpl = "/etc/collectd/synnefo-ganeti.conf" - replace = { - "STATS": env.env.stats.fqdn, - } - custom = customize_settings_from_tmpl(tmpl, replace) - try_put(custom, tmpl, mode=0644) - - try_run("/etc/init.d/collectd restart") - - -@roles("stats") -def setup_stats_collectd(): - setup_collectd() - tmpl = "/etc/collectd/synnefo-stats.conf" - - replace = { - "STATS": env.env.stats.fqdn, - } - custom = customize_settings_from_tmpl(tmpl, replace) - try_put(custom, tmpl, mode=0644) - try_run("/etc/init.d/collectd restart") - - -@roles("stats") -def setup_stats(): - debug(env.host, "Setting up snf-stats-app...") - setup_stats_collectd() - setup_gunicorn() - setup_apache() - setup_webproject() - install_package("snf-stats-app") - cmd = """ - mkdir /var/cache/snf-stats-app/ - chown www-data:www-data /var/cache/snf-stats-app/ - """ - try_run(cmd) - tmpl = "/etc/synnefo/stats.conf" - - replace = { - "STATS": env.env.stats.fqdn, - } - custom = customize_settings_from_tmpl(tmpl, replace) - try_put(custom, tmpl, mode=0644) - try_run("/etc/init.d/gunicorn restart") - - -@roles("cyclades") -def setup_cyclades(): - debug(env.host, "Setting up snf-cyclades-app...") - with settings(hide("everything")): - try_run("ping -c1 accounts." + env.env.domain) - try_run("ping -c1 " + env.env.db.ip) - try_run("ping -c1 " + env.env.mq.ip) - setup_gunicorn() - setup_apache() - setup_webproject() - install_package("memcached") - install_package("python-memcache") - install_package("snf-pithos-backend") - install_package("kamaki") - install_package("snf-cyclades-app") - install_package("python-django-south") - tmpl = "/etc/synnefo/cyclades.conf" - - with settings(host_string=env.env.accounts.ip): - service_id, service_token = get_service_details("cyclades") - - replace = { - "ACCOUNTS": env.env.accounts.fqdn, - "CYCLADES": env.env.cyclades.fqdn, - "mq_node": env.env.mq.ip, - "db_node": env.env.db.ip, - "synnefo_user": env.env.synnefo_user, - "synnefo_db_passwd": env.env.synnefo_db_passwd, - "synnefo_rabbitmq_passwd": env.env.synnefo_rabbitmq_passwd, - "pithos_dir": env.env.pithos_dir, - "common_bridge": env.env.common_bridge, - "HOST": env.env.cyclades.ip, - "domain": env.env.domain, - "CYCLADES_SERVICE_TOKEN": service_token, - 'STATS': env.env.stats.fqdn, - 'CYCLADES_NODE_IP': env.env.cyclades.ip, - } - custom = customize_settings_from_tmpl(tmpl, replace) - try_put(custom, tmpl, mode=0644) - try_run("/etc/init.d/gunicorn restart") - - cmd = """ - sed -i 's/false/true/' /etc/default/snf-dispatcher - /etc/init.d/snf-dispatcher start - """ - try_run(cmd) - - try_run("snf-manage syncdb") - try_run("snf-manage migrate --delete-ghost-migrations") - - -@roles("cyclades") -def get_backend_id(cluster_name="ganeti1.synnefo.deploy.local"): - backend_id = try_run("snf-manage backend-list 2>/dev/null " + - "| grep %s | awk '{print $1}'" % cluster_name) - return backend_id - - -@roles("cyclades") -def add_backend(): - debug(env.host, - "adding %s ganeti backend to cyclades..." % env.env.cluster.fqdn) - with settings(hide("everything")): - try_run("ping -c1 " + env.env.cluster.fqdn) - cmd = """ - snf-manage backend-add --clustername={0} --user={1} --pass={2} - """.format(env.env.cluster.fqdn, env.env.synnefo_user, - env.env.synnefo_rapi_passwd) - try_run(cmd) - backend_id = get_backend_id(env.env.cluster.fqdn) - try_run("snf-manage backend-modify --drained=False " + backend_id) - - -@roles("cyclades") -def pin_user_to_backend(user_email): - backend_id = get_backend_id(env.env.cluster.fqdn) - # pin user to backend - cmd = """ -cat <<EOF >> /etc/synnefo/cyclades.conf - -BACKEND_PER_USER = { - '{0}': {1}, -} - -EOF -/etc/init.d/gunicorn restart -""".format(user_email, backend_id) - try_run(cmd) - - -@roles("cyclades") -def add_pools(): - debug(env.host, - " * Creating pools of resources (brigdes, mac prefixes) " + - "in cyclades...") - try_run("snf-manage pool-create --type=mac-prefix " + - "--base=aa:00:0 --size=65536") - try_run("snf-manage pool-create --type=bridge --base=prv --size=20") - - -@roles("accounts", "cyclades", "pithos") -def export_services(): - debug(env.host, " * Exporting services...") - host = env.host - services = [] - if host == env.env.cyclades.ip: - services.append("cyclades") - if host == env.env.pithos.ip: - services.append("pithos") - if host == env.env.accounts.ip: - services.append("astakos") - for service in services: - filename = "%s_services.json" % service - cmd = "snf-manage service-export-%s > %s" % (service, filename) - run(cmd) - try_get(filename, filename+".local") - - -@roles("accounts") -def import_services(): - debug(env.host, " * Registering services to astakos...") - for service in ["cyclades", "pithos", "astakos"]: - filename = "%s_services.json" % service - try_put(filename + ".local", filename) - cmd = "snf-manage service-import --json=%s" % filename - run(cmd) - - debug(env.host, " * Setting default quota...") - cmd = """ - snf-manage resource-modify --default-quota 40G pithos.diskspace - snf-manage resource-modify --default-quota 2 astakos.pending_app - snf-manage resource-modify --default-quota 4 cyclades.vm - snf-manage resource-modify --default-quota 40G cyclades.disk - snf-manage resource-modify --default-quota 16G cyclades.total_ram - snf-manage resource-modify --default-quota 8G cyclades.ram - snf-manage resource-modify --default-quota 32 cyclades.total_cpu - snf-manage resource-modify --default-quota 16 cyclades.cpu - snf-manage resource-modify --default-quota 4 cyclades.network.private - snf-manage resource-modify --default-quota 4 cyclades.floating_ip - """ - try_run(cmd) - - -@roles("accounts") -def set_user_quota(): - debug(env.host, " * Setting user quota...") - cmd = """ - snf-manage user-modify -f --all --base-quota pithos.diskspace 40G - snf-manage user-modify -f --all --base-quota astakos.pending_app 2 - snf-manage user-modify -f --all --base-quota cyclades.vm 4 - snf-manage user-modify -f --all --base-quota cyclades.disk 40G - snf-manage user-modify -f --all --base-quota cyclades.total_ram 16G - snf-manage user-modify -f --all --base-quota cyclades.ram 8G - snf-manage user-modify -f --all --base-quota cyclades.total_cpu 32 - snf-manage user-modify -f --all --base-quota cyclades.cpu 16 - snf-manage user-modify -f --all --base-quota cyclades.network.private 4 - snf-manage user-modify -f --all --base-quota cyclades.floating_ip 4 - """ - try_run(cmd) - - -@roles("cyclades") -def add_network(): - debug(env.host, " * Adding public network in cyclades...") - cmd = """ - snf-manage network-create --subnet={0} --gateway={1} --public \ - --dhcp=True --flavor={2} --mode=bridged --link={3} --name=Internet \ - --floating-ip-pool=True - """.format(env.env.synnefo_public_network_subnet, - env.env.synnefo_public_network_gateway, - env.env.synnefo_public_network_type, - env.env.common_bridge) - try_run(cmd) - if env.env.testing_vm: - cmd = ("snf-manage network-create --subnet6=babe::/64" - " --gateway6=babe::1 --public --flavor={0} --mode=bridged" - " --link={1} --name=IPv6PublicNetwork" - .format(env.env.synnefo_public_network_type, - env.env.common_bridge)) - try_run(cmd) - - -@roles("cyclades") -def setup_vncauthproxy(): - debug(env.host, " * Setting up vncauthproxy...") - user = "synnefo" - salt = "$6$7FUdSvFcWAs3hfVj$" - passhash = "ZwvnvpQclTrDYWEwBvZDMRJZNgb6ZUKT1vNsh9NzUIxMpzBuGgMqYxCDTYF"\ - "6OZcbunDZb88pjL2EIBnzrGMQW1" - cmd = """ - mkdir /var/lib/vncauthproxy - echo '%s:%s%s' > /var/lib/vncauthproxy/users - """ % (user, salt, passhash) - try_run(cmd) - install_package("snf-vncauthproxy") - - -@roles("client") -def setup_kamaki(): - debug(env.host, "Setting up kamaki client...") - with settings(hide("everything")): - try_run("ping -c1 accounts." + env.env.domain) - try_run("ping -c1 cyclades." + env.env.domain) - try_run("ping -c1 pithos." + env.env.domain) - - with settings(host_string=env.env.db.ip): - uid, user_auth_token, user_uuid = \ - get_auth_token_from_db(env.env.user_email) - - install_package("python-progress") - install_package("kamaki") - cmd = """ - kamaki config set cloud.default.url "https://{0}/astakos/identity/v2.0" - kamaki config set cloud.default.token {1} - """.format(env.env.accounts.fqdn, user_auth_token) - try_run(cmd) - try_run("kamaki container create images") - - -@roles("client") -def upload_image(image="debian_base.diskdump"): - debug(env.host, " * Uploading initial image to pithos...") - image = "debian_base.diskdump" - try_run("wget {0} -O /tmp/{1}".format(env.env.debian_base_url, image)) - try_run("kamaki file upload --container images /tmp/{0} {0}".format(image)) - - -@roles("client") -def register_image(image="debian_base.diskdump"): - debug(env.host, " * Register image to plankton...") - # with settings(host_string=env.env.db.ip): - # uid, user_auth_token, user_uuid = \ - # get_auth_token_from_db(env.env.user_email) - - image_location = "/images/{0}".format(image) - cmd = """ - sleep 5 - kamaki image register --name="Debian Base" --location={0} --public \ - --disk-format=diskdump \ - --property OSFAMILY=linux --property ROOT_PARTITION=1 \ - --property description="Debian Squeeze Base System" \ - --property size=450M --property kernel=2.6.32 \ - --property GUI="No GUI" --property sortorder=1 \ - --property USERS=root --property OS=debian - """.format(image_location) - try_run(cmd) - - -@roles("client") -def setup_burnin(): - debug(env.host, "Setting up burnin testing tool...") - install_package("kamaki") - install_package("snf-tools") - - -@roles("pithos") -def add_image_locally(): - debug(env.host, - " * Getting image locally in order snf-image to use it directly..") - image = "debian_base.diskdump" - try_run("wget {0} -O {1}/{2}".format( - env.env.debian_base_url, env.env.image_dir, image)) - - -@roles("master") -def gnt_instance_add(name="test"): - debug(env.host, " * Adding test instance to Ganeti...") - osp = """img_passwd=gamwtosecurity,\ -img_format=diskdump,img_id=debian_base,\ -img_properties='{"OSFAMILY":"linux"\,"ROOT_PARTITION":"1"}'""" - cmd = """ - gnt-instance add -o snf-image+default --os-parameters {0} \ - -t plain --disk 0:size=1G --no-name-check --no-ip-check \ - --net 0:ip=pool,network=test --no-install \ - --hypervisor-parameters kvm:machine_version=pc-1.0 {1} - """.format(osp, name) - try_run(cmd) - - -@roles("master") -def gnt_network_add(name="test", subnet="10.0.0.0/26", gw="10.0.0.1", - mode="bridged", link="br0"): - debug(env.host, " * Adding test network to Ganeti...") - cmd = """ - gnt-network add --network={1} --gateway={2} {0} - gnt-network connect {0} {3} {4} - """.format(name, subnet, gw, mode, link) - try_run(cmd) - + nodes = ctx.all_nodes -@roles("ips") -def test(): - debug(env.host, "Testing...") - try_run("hostname && date") + for node in nodes: + ctx.update(node=node) + c = roles.get("HW", ctx) + c.run(context.cmd) diff --git a/snf-deploy/snfdeploy/filelocker.py b/snf-deploy/snfdeploy/filelocker.py new file mode 100644 index 0000000000000000000000000000000000000000..98580f9a2db99f5fef4b45f7e8d918c4b4397ce6 --- /dev/null +++ b/snf-deploy/snfdeploy/filelocker.py @@ -0,0 +1,125 @@ +# filelocker.py - Cross-platform (posix/nt) API for flock-style file locking. +# Requires python 1.5.2 or better. +"""Cross-platform (posix/nt) API for flock-style file locking. + + +Synopsis: + + import filelocker + with filelocker.lock("lockfile", filelocker.LOCK_EX): + print "Got it" + + +Methods: + + lock ( file, flags, tries=10 ) + + +Constants: + + LOCK_EX + LOCK_SH + LOCK_NB + + +Exceptions: + + LockException + + +Notes: + +For the 'nt' platform, this module requires the Python Extensions for Windows. +Be aware that this may not work as expected on Windows 95/98/ME. + + +History: + +I learned the win32 technique for locking files from sample code +provided by John Nielsen <nielsenjf@my-deja.com> in the documentation +that accompanies the win32 modules. + + +Author: Jonathan Feinberg <jdf@pobox.com>, + Lowell Alleman <lalleman@mfps.com> +Version: $Id: filelocker.py 5474 2008-05-16 20:53:50Z lowell $ + + +Modified to work as a contextmanager + +""" + +import os +from contextlib import contextmanager + + +class LockException(Exception): + # Error codes: + LOCK_FAILED = 1 + + +# Import modules for each supported platform +if os.name == 'nt': + import win32con + import win32file + import pywintypes + LOCK_EX = win32con.LOCKFILE_EXCLUSIVE_LOCK + LOCK_SH = 0 # the default + LOCK_NB = win32con.LOCKFILE_FAIL_IMMEDIATELY + # is there any reason not to reuse the following structure? + __overlapped = pywintypes.OVERLAPPED() +elif os.name == 'posix': + import fcntl + LOCK_EX = fcntl.LOCK_EX + LOCK_SH = fcntl.LOCK_SH + LOCK_NB = fcntl.LOCK_NB +else: + raise RuntimeError("FileLocker only defined for nt and posix platforms") + + +# -------------------------------------- +# Implementation for NT +if os.name == 'nt': + @contextmanager + def lock(filename, flags): + file = open(filename, "w+") + hfile = win32file._get_osfhandle(file.fileno()) + + try: + win32file.LockFileEx(hfile, flags, 0, -0x10000, __overlapped) + try: + yield + finally: + file.close() + except pywintypes.error, exc_value: + # error: (33, 'LockFileEx', + # 'The process cannot access the file because another + # process has locked a portion of the file.') + file.close() + if exc_value[0] == 33: + raise LockException(LockException.LOCK_FAILED, exc_value[2]) + else: + # Q: Are there exceptions/codes we should be dealing with? + raise + + +# -------------------------------------- +# Implementation for Posix +elif os.name == 'posix': + @contextmanager + def lock(filename, flags): + file = open(filename, "w+") + + try: + fcntl.flock(file.fileno(), flags) + try: + yield + finally: + file.close() + except IOError, exc_value: + # IOError: [Errno 11] Resource temporarily unavailable + file.close() + if exc_value[0] == 11: + raise LockException(LockException.LOCK_FAILED, exc_value[1]) + else: + raise diff --git a/snf-deploy/snfdeploy/lib.py b/snf-deploy/snfdeploy/lib.py index c0ffca7e46d3530cc113e72c2c5fa56d0bcbbd9e..32a1a067a093581c35e612c181d45b342d51dcf4 100644 --- a/snf-deploy/snfdeploy/lib.py +++ b/snf-deploy/snfdeploy/lib.py @@ -1,16 +1,28 @@ -#!/usr/bin/python +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import time -import ipaddr import os import signal -import ConfigParser import sys import re import random import subprocess import imp - +import ast +import string HEADER = '\033[95m' OKBLUE = '\033[94m' @@ -40,166 +52,59 @@ if not sys.stdout.isatty(): disable_color() -class Host(object): - def __init__(self, hostname, ip, mac, domain, os): - self.hostname = hostname - self.ip = ip - self.mac = mac - self.domain = domain - self.os = os +def evaluate(some_object, **kwargs): + for k, v in kwargs.items(): + setattr(some_object, k, v) + + +def getlist(value): + return list(filter(None, (x.strip() for x in value.splitlines()))) + + +def getbool(value): + return ast.literal_eval(value) + + +class FQDN(object): + def __init__(self, alias=None, **kwargs): + self.alias = alias + evaluate(self, **kwargs) + self.hostname = self.name @property def fqdn(self): - return self.hostname + "." + self.domain + return self.name + "." + self.domain @property def arecord(self): - return self.hostname + " IN A " + self.ip + "\n" + return self.fqdn + " 300 A " + self.ip @property def ptrrecord(self): - return ".".join(raddr(self.ip)) + " IN PTR " + self.fqdn + ".\n" - - -class Alias(Host): - def __init__(self, host, alias): - super(Alias, self).__init__(host.hostname, host.ip, host.mac, - host.domain, host.os) - self.alias = alias + return ".".join(raddr(self.ip)) + ".in-addr.arpa 300 PTR " + self.fqdn @property def cnamerecord(self): - return (self.alias + " IN CNAME " + self.hostname + "." + - self.domain + ".\n") + if self.cname: + return self.cname + " 300 CNAME " + self.fqdn + else: + return "" @property - def fqdn(self): - return self.alias + "." + self.domain - - -class Env(object): - - def update_packages(self, os): - for section in self.conf.files[os]: - self.evaluate(os, section) - - def evaluate(self, filename, section): - for k, v in self.conf.get_section(filename, section): - setattr(self, k, v) - - def __init__(self, conf): - self.conf = conf - for f, sections in conf.files.iteritems(): - for s in sections: - self.evaluate(f, s) - - self.node2hostname = dict(conf.get_section("nodes", "hostnames")) - self.node2ip = dict(conf.get_section("nodes", "ips")) - self.node2mac = dict(conf.get_section("nodes", "macs")) - self.node2os = dict(conf.get_section("nodes", "os")) - self.hostnames = [self.node2hostname[n] - for n in self.nodes.split(",")] - - self.ips = [self.node2ip[n] - for n in self.nodes.split(",")] - - self.cluster_hostnames = [self.node2hostname[n] - for n in self.cluster_nodes.split(",")] - - self.cluster_ips = [self.node2ip[n] - for n in self.cluster_nodes.split(",")] - - self.net = ipaddr.IPNetwork(self.subnet) - - self.nodes_info = {} - self.hosts_info = {} - self.ips_info = {} - for node in self.nodes.split(","): - host = Host(self.node2hostname[node], - self.node2ip[node], - self.node2mac[node], self.domain, self.node2os[node]) - - self.nodes_info[node] = host - self.hosts_info[host.hostname] = host - self.ips_info[host.ip] = host - - self.cluster = Host(self.cluster_name, self.cluster_ip, None, - self.domain, None) - self.master = self.nodes_info[self.master_node] - - self.roles = {} - for role, node in conf.get_section("synnefo", "roles"): - self.roles[role] = Alias(self.nodes_info[node], role) - setattr(self, role, self.roles[role]) - - -class Conf(object): - - files = { - "nodes": ["network", "info"], - "deploy": ["dirs", "packages", "keys", "options"], - "vcluster": ["cluster", "image", "network"], - "synnefo": ["cred", "synnefo", "roles"], - "squeeze": ["debian", "ganeti", "synnefo", "other"], - "wheezy": ["debian", "ganeti", "synnefo", "other"], - } - confdir = "/etc/snf-deploy" - - def get_ganeti(self, cluster_name): - self.files["ganeti"] = [cluster_name] - - def __init__(self, args): - self.confdir = args.confdir - self.get_ganeti(args.cluster_name) - for f in self.files.keys(): - setattr(self, f, self.read_config(f)) - for f, sections in self.files.iteritems(): - for s in sections: - for k, v in self.get_section(f, s): - if getattr(args, k, None): - self.set(f, s, k, getattr(args, k)) - if args.autoconf: - self.autoconf() - - def autoconf(self): - #domain = get_domain() - #if domain: - # self.nodes.set("network", "domain", get_domain()) - # self.nodes.set("network", "subnet", "/".join(get_netinfo())) - # self.nodes.set("network", "gateway", get_default_route()[0]) - self.nodes.set("hostnames", "node1", get_hostname()) - self.nodes.set("ips", "node1", get_netinfo()[0]) - self.nodes.set("info", "nodes", "node1") - self.nodes.set("info", "public_iface", get_default_route()[1]) - - def read_config(self, f): - config = ConfigParser.ConfigParser() - config.optionxform = str - filename = os.path.join(self.confdir, f) + ".conf" - config.read(filename) - return config - - def set(self, conf, section, option, value): - c = getattr(self, conf) - c.set(section, option, value) - - def get(self, conf, section, option): - c = getattr(self, conf) - return c.get(section, option, True) - - def get_section(self, conf, section): - c = getattr(self, conf) - return c.items(section, True) - - def print_config(self): - for f in self.files.keys(): - getattr(self, f).write(sys.stdout) - - -def debug(host, msg): - - print HEADER + host + \ - OKBLUE + ": " + msg + ENDC + def cname(self): + if self.alias: + return self.alias + "." + self.domain + else: + return "" + + +def debug(*args): + + print " ".join([ + HEADER, args[0], + OKBLUE, args[1], + OKGREEN, args[2], + ENDC]) def check_pidfile(pidfile): @@ -275,3 +180,8 @@ def import_conf_file(filename, directory): def raddr(addr): return list(reversed(addr.replace("/", "-").split("."))) + + +def create_passwd(length): + char_set = string.ascii_uppercase + string.digits + string.ascii_lowercase + return ''.join(random.sample(char_set, length)) diff --git a/snf-deploy/snfdeploy/roles.py b/snf-deploy/snfdeploy/roles.py new file mode 100644 index 0000000000000000000000000000000000000000..73f1c2c15b30e37dbf1c3843611208a9c89b0561 --- /dev/null +++ b/snf-deploy/snfdeploy/roles.py @@ -0,0 +1,52 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from snfdeploy import constants +from snfdeploy import components + + +_ROLE_MAP = { + constants.NS: components.NS, + constants.NFS: components.NFS, + constants.CA: components.CA, + constants.DB: components.DB, + constants.MQ: components.MQ, + constants.ASTAKOS: components.Astakos, + constants.CYCLADES: components.Cyclades, + constants.ADMIN: components.Admin, + constants.VNC: components.VNC, + constants.PITHOS: components.Pithos, + constants.CMS: components.CMS, + constants.STATS: components.Stats, + constants.MASTER: components.Master, + constants.VMC: components.VMC, + constants.CLIENT: components.Client, + constants.DEV: components.GanetiDev, + constants.ROUTER: components.Router, + } + + +def _get_role_map(role): + if role in _ROLE_MAP: + return _ROLE_MAP[role] + else: + return getattr(components, role) + + +def get(role, ctx): + assert role and ctx + c = _get_role_map(role) + ctx.role = role + return c(ctx=ctx) diff --git a/snf-deploy/snfdeploy/status.py b/snf-deploy/snfdeploy/status.py new file mode 100644 index 0000000000000000000000000000000000000000..c5c3e6b1b2040beb6955ca05840715ee0eebe0ef --- /dev/null +++ b/snf-deploy/snfdeploy/status.py @@ -0,0 +1,90 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import ConfigParser +import os +import sys +from snfdeploy import constants +from snfdeploy import config +from snfdeploy import filelocker +from snfdeploy.lib import create_passwd + +status = sys.modules[__name__] + + +def _lock_read_write(fn): + def wrapper(*args, **kwargs): + with filelocker.lock(status.lockfile, filelocker.LOCK_EX): + status.cfg.read(status.statusfile) + ret = fn(*args, **kwargs) + if config.force or not config.dry_run: + with open(status.statusfile, 'wb') as configfile: + status.cfg.write(configfile) + return ret + return wrapper + + +def _create_section(section): + if not status.cfg.has_section(section): + status.cfg.add_section(section) + + +@_lock_read_write +def _check(section, option): + try: + return status.cfg.get(section, option, True) + except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): + return None + + +@_lock_read_write +def _update(section, option, value): + _create_section(section) + status.cfg.set(section, option, value) + + +def get_passwd(setup, target): + passwd = _check(setup, target) + if not passwd: + passwd = create_passwd(constants.DEFAULT_PASSWD_LENGTH) + _update(setup, target, passwd) + return passwd + + +def update(component): + section = component.node.ip + option = component.__class__.__name__ + _update(section, option, constants.VALUE_OK) + + +def check(component): + section = component.node.ip + option = component.__class__.__name__ + return _check(section, option) + + +def reset(): + try: + os.remove(status.statusfile) + except OSError: + pass + + +def init(): + status.state_dir = config.state_dir + status.cfg = ConfigParser.ConfigParser() + status.cfg.optionxform = str + status.statusfile = os.path.join(config.state_dir, constants.STATUS_FILE) + status.lockfile = "%s.lock" % status.statusfile diff --git a/snf-deploy/snfdeploy/vcluster.py b/snf-deploy/snfdeploy/vcluster.py new file mode 100644 index 0000000000000000000000000000000000000000..ed2561dc3fc2c9baf1e4045d6e3d2ab6baf13a65 --- /dev/null +++ b/snf-deploy/snfdeploy/vcluster.py @@ -0,0 +1,275 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import re +import os +import sys +import random +import subprocess +import ipaddr +from snfdeploy import config +from snfdeploy import context +from snfdeploy import constants +from snfdeploy.lib import check_pidfile, create_dir, get_default_route, \ + random_mac, get_netinfo + + +def runcmd(cmd): + if config.dry_run: + print cmd + else: + os.system(cmd) + + +def help(): + + print """ +Usage: snf-deploy vcluster + + Run the following actions concerning the local virtual cluster: + + - Downloads base image and creates additional disk \ +(if --create-extra-disk is passed) + - Does all the network related actions (bridge, iptables, NAT) + - Launches dnsmasq for dhcp server on bridge + - Creates the virtual cluster (with kvm) + + """ + sys.exit(0) + + +def create_dnsmasq_files(ctx): + + print("Customize dnsmasq..") + + hosts = opts = conf = "\n" + hostsf = os.path.join(config.dns_dir, "dhcp-hostsfile") + optsf = os.path.join(config.dns_dir, "dhcp-optsfile") + conff = os.path.join(config.dns_dir, "conf-file") + + for node in ctx.all_nodes: + info = config.get_info(node=node) + if ipaddr.IPAddress(info.ip) not in config.net: + raise Exception("%s's IP outside vcluster's network." % node) + # serve ip and name to nodes + hosts += "%s,%s,%s,2m\n" % (info.mac, info.ip, info.name) + + hosts += "52:54:56:*:*:*,ignore\n" + + opts = """ +# Netmask +1,{0} +# Gateway +3,{1} +# Nameservers +6,{2} +""".format(config.net.netmask, config.gateway, constants.EXTERNAL_PUBLIC_DNS) + + conf = """ +user=dnsmasq +bogus-priv +no-poll +no-negcache +leasefile-ro +bind-interfaces +except-interface=lo +dhcp-fqdn +no-resolv +# disable DNS +port=0 +""".format(ctx.ns.ip) + + conf += """ +# serve domain and search domain for resolv.conf +domain={5} +interface={0} +dhcp-hostsfile={1} +dhcp-optsfile={2} +dhcp-range={0},{4},static,2m +""".format(config.bridge, hostsf, optsf, + info.domain, config.net.network, info.domain) + + if config.dry_run: + print hostsf, hosts + print optsf, opts + print conff, conf + else: + hostsfile = open(hostsf, "w") + optsfile = open(optsf, "w") + conffile = open(conff, "w") + + hostsfile.write(hosts) + optsfile.write(opts) + conffile.write(conf) + + hostsfile.close() + optsfile.close() + conffile.close() + + +def cleanup(): + print("Stopping processes..") + for f in os.listdir(config.run_dir): + if re.search(".pid$", f): + check_pidfile(os.path.join(config.run_dir, f)) + + create_dir(config.run_dir, True) + # create_dir(env.cmd, True) + + print("Reseting NAT..") + cmd = """ + iptables -t nat -D POSTROUTING -s {0} -o {1} -j MASQUERADE + echo 0 > /proc/sys/net/ipv4/ip_forward + iptables -D INPUT -i {2} -j ACCEPT + iptables -D FORWARD -i {2} -j ACCEPT + iptables -D OUTPUT -o {2} -j ACCEPT + """.format(config.subnet, get_default_route()[1], config.bridge) + runcmd(cmd) + + print("Deleting bridge %s.." % config.bridge) + cmd = """ + ip link show {0} && ip addr del {1}/{2} dev {0} + sleep 1 + ip link set {0} down + sleep 1 + brctl delbr {0} + """.format(config.bridge, config.gateway, config.net.prefixlen) + runcmd(cmd) + + +def network(): + print("Creating bridge %s.." % config.bridge) + print("Add gateway IP %s.." % config.gateway) + + cmd = """ + ! ip link show {0} && brctl addbr {0} && ip link set {0} up + sleep 1 + ip link set promisc on dev {0} + ip addr add {1}/{2} dev {0} + """.format(config.bridge, config.gateway, config.net.prefixlen) + runcmd(cmd) + + print("Activate NAT..") + cmd = """ + iptables -t nat -A POSTROUTING -s {0} -o {1} -j MASQUERADE + echo 1 > /proc/sys/net/ipv4/ip_forward + iptables -I INPUT 1 -i {2} -j ACCEPT + iptables -I FORWARD 1 -i {2} -j ACCEPT + iptables -I OUTPUT 1 -o {2} -j ACCEPT + """.format(config.subnet, get_default_route()[1], config.bridge) + runcmd(cmd) + + +def image(): + disk0 = os.path.join(config.vcluster_dir, "disk0") + disk1 = os.path.join(config.vcluster_dir, "disk1") + + create_dir(config.vcluster_dir, False) + + env = os.environ.copy() + env.update({ + "DISK0": disk0, + "DISK0_SIZE": config.disk0_size, + "DISK1": disk1, + "DISK1_SIZE": config.disk1_size, + }) + cmd = os.path.join(config.lib_dir, "mkimage.sh") + + subprocess.Popen([cmd], env=env) + + +def cluster(ctx): + vms = [] + for node in ctx.all_nodes: + node_info = config.get_info(node=node) + vnc = _launch_vm(node_info.name, node_info.mac) + vms.append((node_info, vnc)) + + runcmd("reset") + for vm, port in vms: + if port: + vnc = "vncviewer %s:%s" % (get_netinfo()[0], 5900 + port) + else: + vnc = "no vnc" + print "%s: ssh root@%s or %s" % (vm.name, vm.ip, vnc) + + +def _launch_vm(name, mac): + check_pidfile("%s/%s.pid" % (config.run_dir, name)) + + disk0 = os.path.join(config.vcluster_dir, "disk0") + disk1 = os.path.join(config.vcluster_dir, "disk1") + + print("Launching cluster node {0}..".format(name)) + os.environ["BRIDGE"] = config.bridge + if config.vnc: + random_vnc_port = random.randint(1, 1000) + graphics = "-vnc :{0}".format(random_vnc_port) + else: + random_vnc_port = None + graphics = "-nographic" + + disks = """ \ +-drive file={0},format=raw,if=none,id=drive0,snapshot=on \ +-device virtio-blk-pci,drive=drive0,id=virtio-blk-pci.0 \ +-drive file={1},format=raw,if=none,id=drive1,snapshot=on \ +-device virtio-blk-pci,drive=drive1,id=virtio-blk-pci.1 \ +""".format(disk0, disk1) + + ifup = os.path.join(config.lib_dir, "ifup") + nics = """ \ +-netdev tap,id=netdev0,script={0},downscript=no \ +-device virtio-net-pci,mac={1},netdev=netdev0,id=virtio-net-pci.0 \ +-netdev tap,id=netdev1,script={0},downscript=no \ +-device virtio-net-pci,mac={2},netdev=netdev1,id=virtio-net-pci.1 \ +-netdev tap,id=netdev2,script={0},downscript=no \ +-device virtio-net-pci,mac={3},netdev=netdev2,id=virtio-net-pci.2 \ +""".format(ifup, mac, random_mac(), random_mac()) + + kernel = """ \ +--kernel /boot/vmlinuz-3.2.0-4-amd64 \ +--initrd /boot/initrd.img-3.2.0-4-amd64 \ +--append "root=/dev/vda1 ro console=ttyS0,38400" \ +""" + + cmd = """ +/usr/bin/kvm -name {0} -pidfile {1}/{0}.pid -balloon virtio -daemonize \ +-monitor unix:{1}/{0}.monitor,server,nowait -usbdevice tablet -boot c \ +{2} \ +{3} \ +-m {4} -smp {5} {6} {7} \ +""".format(name, config.run_dir, disks, nics, + config.mem, config.smp, graphics, kernel) + + runcmd(cmd) + + return random_vnc_port + + +def dnsmasq(): + check_pidfile(config.run_dir + "/dnsmasq.pid") + cmd = "dnsmasq --pid-file={0}/dnsmasq.pid --conf-file={1}/conf-file"\ + .format(config.run_dir, config.dns_dir) + runcmd(cmd) + + +def launch(): + ctx = context.Context() + assert len(ctx.clusters) == 1 + assert ctx.all_nodes + network() + create_dnsmasq_files(ctx) + dnsmasq() + cluster(ctx) diff --git a/snf-django-lib/MANIFEST.in b/snf-django-lib/MANIFEST.in index 6106c5d89680afa76e234fbcf1feb50c52b9a5f6..ce7f614828c2d727bb93e8692c231749d45fdd5a 100644 --- a/snf-django-lib/MANIFEST.in +++ b/snf-django-lib/MANIFEST.in @@ -1 +1 @@ -include distribute_setup.py +include distribute_setup.py README.md diff --git a/snf-django-lib/README.md b/snf-django-lib/README.md new file mode 100644 index 0000000000000000000000000000000000000000..d1212cd74fb437eca4719a0d830b70872563e83d --- /dev/null +++ b/snf-django-lib/README.md @@ -0,0 +1,27 @@ +snf-django-lib +============== + +Overview +-------- + +This is Synnefo's snf-django-lib component. Please see the [official Synnefo +site](http://www.synnefo.org) for more information. + + +Copyright and license +===================== + +Copyright (C) 2010-2014 GRNET S.A. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. diff --git a/snf-django-lib/setup.py b/snf-django-lib/setup.py index ff08e8eacdf0ff7b8d38f897b6c5d6a748f80b60..7adee7e65fd5d7a696bff37df3e85e2598e6e511 100644 --- a/snf-django-lib/setup.py +++ b/snf-django-lib/setup.py @@ -1,35 +1,17 @@ -# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # import distribute_setup @@ -160,7 +142,7 @@ def find_package_data( setup( name='snf-django-lib', version=VERSION, - license='BSD', + license='GNU GPLv3', url='http://www.synnefo.org/', description=SHORT_DESCRIPTION, classifiers=CLASSIFIERS, diff --git a/snf-django-lib/snf_django/lib/api/__init__.py b/snf-django-lib/snf_django/lib/api/__init__.py index 875e98a566377eee4f3c4170b68e79aa5bfc7cdf..48ec62a623394aeb0ce8702110b0ea12078061b5 100644 --- a/snf-django-lib/snf_django/lib/api/__init__.py +++ b/snf-django-lib/snf_django/lib/api/__init__.py @@ -1,35 +1,17 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import sys from functools import wraps from traceback import format_exc @@ -75,6 +57,11 @@ def api_method(http_method=None, token_required=True, user_required=True, @wraps(func) def wrapper(request, *args, **kwargs): try: + # Explicitly set request encoding to UTF-8 instead of relying + # to the DEFAULT_CHARSET setting. See: + # https://docs.djangoproject.com/en/1.4/ref/unicode/#form-submission # flake8: noqa + request.encoding = 'utf-8' + # Get the requested serialization format serialization = get_serialization( request, format_allowed, serializations[0]) diff --git a/snf-django-lib/snf_django/lib/api/faults.py b/snf-django-lib/snf_django/lib/api/faults.py index 9542dc4d525933b2152975e093982ad58e2ed5d4..297b639eff9d1c878e89c4269b23589fe5768527 100644 --- a/snf-django-lib/snf_django/lib/api/faults.py +++ b/snf-django-lib/snf_django/lib/api/faults.py @@ -1,35 +1,17 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. """Common API faults.""" diff --git a/snf-django-lib/snf_django/lib/api/proxy/__init__.py b/snf-django-lib/snf_django/lib/api/proxy/__init__.py index 1de657b5dbe71c00a61def2bcf7834a179157cbe..755dc741d704e4e5a92bfbd50e0bdf2360f8674f 100644 --- a/snf-django-lib/snf_django/lib/api/proxy/__init__.py +++ b/snf-django-lib/snf_django/lib/api/proxy/__init__.py @@ -1,35 +1,17 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.http import HttpResponse, HttpResponseRedirect from django.utils.encoding import smart_str diff --git a/snf-django-lib/snf_django/lib/api/proxy/utils.py b/snf-django-lib/snf_django/lib/api/proxy/utils.py index b69ae9a23df4a3a37e2824f22489062402dc7545..18f3f7ddf4969325b2d966f0a3be9513dd7c85fc 100644 --- a/snf-django-lib/snf_django/lib/api/proxy/utils.py +++ b/snf-django-lib/snf_django/lib/api/proxy/utils.py @@ -1,35 +1,17 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. try: from django.core.servers.basehttp import is_hop_by_hop diff --git a/snf-django-lib/snf_django/lib/api/urls.py b/snf-django-lib/snf_django/lib/api/urls.py index 1cf4c2a0f474aa2c0d6bcf83e6593af3d0b6b2a9..0505d0e7c4123f4fe2b0e5aaa7a5886d524772a9 100644 --- a/snf-django-lib/snf_django/lib/api/urls.py +++ b/snf-django-lib/snf_django/lib/api/urls.py @@ -1,35 +1,17 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.core import urlresolvers from django.views.decorators import csrf diff --git a/snf-django-lib/snf_django/lib/api/utils.py b/snf-django-lib/snf_django/lib/api/utils.py index b111b4f4e1660d471b4cc927026c8ce8de04135f..77c1627608a657e27fa688fcb38497c7fd08cce7 100644 --- a/snf-django-lib/snf_django/lib/api/utils.py +++ b/snf-django-lib/snf_django/lib/api/utils.py @@ -1,35 +1,17 @@ -# Copyright 2011-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import datetime from dateutil.parser import parse as date_parse @@ -88,10 +70,11 @@ def isoparse(s): return utc_since -def get_request_dict(request): - """Return data sent by the client as python dictionary. +def get_json_body(request): + """Get the JSON request body as a Python object. - Only JSON format is supported + Check that the content type is json and deserialize the body of the + request that contains a JSON document to a Python object. """ data = request.body @@ -101,8 +84,10 @@ def get_request_dict(request): if content_type.startswith("application/json"): try: return json.loads(data) + except UnicodeDecodeError: + raise faults.BadRequest("Could not decode request as UTF-8 string") except ValueError: - raise faults.BadRequest("Invalid JSON data") + raise faults.BadRequest("Could not decode request body as JSON") else: raise faults.BadRequest("Unsupported Content-type: '%s'" % content_type) @@ -135,7 +120,8 @@ def filter_modified_since(request, objects): return objects.filter(deleted=False) -def get_attribute(request, attribute, attr_type=None, required=True): +def get_attribute(request, attribute, attr_type=None, required=True, + default=None): value = request.get(attribute, None) if required and value is None: raise faults.BadRequest("Malformed request. Missing attribute '%s'." % @@ -144,4 +130,7 @@ def get_attribute(request, attribute, attr_type=None, required=True): and not isinstance(value, attr_type): raise faults.BadRequest("Malformed request. Invalid '%s' field" % attribute) - return value + if value is not None: + return value + else: + return default diff --git a/snf-django-lib/snf_django/lib/astakos.py b/snf-django-lib/snf_django/lib/astakos.py index 75c52b12b89dfebb6ae2fc54fb2b0e343e3c92b3..0963e25113932ba54e47c54c5f7d3daf5d6a8b7e 100644 --- a/snf-django-lib/snf_django/lib/astakos.py +++ b/snf-django-lib/snf_django/lib/astakos.py @@ -1,35 +1,17 @@ -# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import logging diff --git a/snf-django-lib/snf_django/lib/db/fields.py b/snf-django-lib/snf_django/lib/db/fields.py index 9f140cbfec538f42bf5ec6ed6f211fadd6c7a9ac..d95a615ed5f5f6d48a0a1fef7e7ea3a5d9328692 100644 --- a/snf-django-lib/snf_django/lib/db/fields.py +++ b/snf-django-lib/snf_django/lib/db/fields.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.core import exceptions from django.db.models import DecimalField, SubfieldBase diff --git a/snf-django-lib/snf_django/management/commands/__init__.py b/snf-django-lib/snf_django/management/commands/__init__.py index 2884d21166e7b894025988bad6b07fa1af7341cf..2227bd67ee214ceffab4c4ed77ea5ca620a7052c 100644 --- a/snf-django-lib/snf_django/management/commands/__init__.py +++ b/snf-django-lib/snf_django/management/commands/__init__.py @@ -1,47 +1,80 @@ -# Copyright 2012-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. - -from optparse import make_option - -from django.core.management.base import BaseCommand, CommandError +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import re +import sys +import datetime +import logging +from optparse import (make_option, OptionParser, OptionGroup, + TitledHelpFormatter) +from synnefo import settings +from django.core.management.base import (BaseCommand, + CommandError as DjangoCommandError) from django.core.exceptions import FieldError - from snf_django.management import utils from snf_django.lib.astakos import UserCache +from snf_django.utils.line_logging import NewlineStreamHandler import distutils USER_EMAIL_FIELD = "user.email" +LOGGER_EXCLUDE_COMMANDS = "-list$|-show$" + + +class SynnefoOutputWrapper(object): + """Wrapper around stdout/stderr + + This class replaces Django's 'OutputWrapper' which doesn't handle + logging to file. + + Since 'BaseCommand' doesn't initialize the 'stdout' and 'stderr' + attributes at '__init__' but sets them only when it needs to, + this class has to be a descriptor. + + We will use the old 'OutputWrapper' class for print to the screen and + a logger for logging to the file. + + """ + def __init__(self): + self.django_wrapper = None + self.logger = None + + def __set__(self, obj, value): + self.django_wrapper = value + + def __getattr__(self, name): + return getattr(self.django_wrapper, name) + + def write(self, msg, *args, **kwargs): + if self.logger is not None: + self.logger.info(msg) + if self.django_wrapper is not None: + self.django_wrapper.write(msg, *args, **kwargs) + + +class CommandError(DjangoCommandError): + def __str__(self): + return utils.smart_locale_str(self.message, errors='replace') + + +class SynnefoCommandFormatter(TitledHelpFormatter): + def format_heading(self, heading): + if heading == "Options": + return "" + return "%s\n%s\n" % (heading, "=-"[self.level] * len(heading)) class SynnefoCommand(BaseCommand): @@ -56,6 +89,137 @@ class SynnefoCommand(BaseCommand): "csv [comma-separated output]"), ) + stdout = SynnefoOutputWrapper() + stderr = SynnefoOutputWrapper() + + def run_from_argv(self, argv): + """Initialize loggers and convert arguments to unicode objects + + Create a filename based on the timestamp, the running + command and the pid. Then create a new logger that will + write to this file and pass it to stdout and stderr + 'SynnefoOutputWrapper' objects. + + Modify all existing loggers to write to this file as well. + + Commands that match the 'LOGGER_EXCLUE_COMMANDS' pattern will not be + logged (by default all *-list and *-show commands). + + Also, convert command line arguments and options to unicode objects + using user's preferred encoding. + + """ + curr_time = datetime.datetime.now() + curr_time = datetime.datetime.strftime(curr_time, "%y%m%d%H%M%S") + command = argv[1] + pid = os.getpid() + fd = None + stream = None + + exclude_commands = getattr(settings, "LOGGER_EXCLUDE_COMMANDS", + LOGGER_EXCLUDE_COMMANDS) + if re.search(exclude_commands, command) is None: + # The filename will be of the form time_command_pid.log + basename = "%s_%s_%s" % (curr_time, command, pid) + log_dir = os.path.join(settings.LOG_DIR, "commands") + # If log_dir is missing, create it + if not os.path.exists(log_dir): + os.makedirs(log_dir) + filename = os.path.join(log_dir, basename + ".log") + + try: + fd = os.open(filename, + os.O_RDWR | os.O_APPEND | os.O_CREAT, + 0600) + stream = os.fdopen(fd, 'a') + + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s: %(message)s") + # Our file handler + # We need one handler without newline terminator + # for commnand-line's output (the programmer is + # responsible for formatting the output) and one + # FileHandler for the rest loggers. + # TODO: Replace 'NewlineStreamHandler' with pythons + # 'logging.StreamHandler' when python version >= 3.2 + line_handler = NewlineStreamHandler(stream) + line_handler.terminator = '' + line_handler.setLevel(logging.DEBUG) + line_handler.setFormatter(formatter) + + file_handler = logging.FileHandler(filename, mode='a') + file_handler.setLevel(logging.DEBUG) + file_handler.setFormatter(formatter) + + # Change all loggers to use our new file_handler + all_loggers = logging.Logger.manager.loggerDict.keys() + for logger_name in all_loggers: + logger = logging.getLogger(logger_name) + logger.addHandler(file_handler) + + # Create our new logger + logger = logging.getLogger(basename) + logger.setLevel(logging.DEBUG) + logger.propagate = False + logger.addHandler(line_handler) + + # Write the command which is executed + header = "\n\tcommand: %s\n\tpid: %s\n" \ + % (" ".join(map(str, argv)), pid) + logger.info(header + "\n\nOutput:\n") + + # Give the logger to our stdout, stderr objects + self.stdout.logger = logger + self.stderr.logger = logger + except OSError as err: + msg = ("Could not open file %s for write: %s\n" + "Will not log this command's output\n") % ( + filename, err) + sys.stderr.write(msg) + + argv = [utils.smart_locale_unicode(a) for a in argv] + super(SynnefoCommand, self).run_from_argv(argv) + + if stream is not None: + stream.close() + fd = None + if fd is not None: + os.close(fd) + + def create_parser(self, prog_name, subcommand): + parser = OptionParser(prog=prog_name, add_help_option=False, + formatter=SynnefoCommandFormatter()) + + parser.set_usage(self.usage(subcommand)) + parser.version = self.get_version() + + # Handle Django's and common options + common_options = OptionGroup(parser, "Common Options") + common_options.add_option("-h", "--help", action="help", + help="show this help message and exit") + + common_options.add_option("--version", action="version", + help="show program's version number and" + " exit") + [common_options.add_option(o) for o in self.option_list] + if common_options.option_list: + parser.add_option_group(common_options) + + # Handle command specific options + command_options = OptionGroup(parser, "Command Specific Options") + [command_options.add_option(o) + for o in getattr(self, "command_option_list", ())] + if command_options.option_list: + parser.add_option_group(command_options) + + return parser + + def pprint_table(self, *args, **kwargs): + return utils.pprint_table(self.stdout, *args, **kwargs) + + def escape_ctrl_chars(self, *args, **kwargs): + return utils.escape_ctrl_chars(*args, **kwargs) + class ListCommand(SynnefoCommand): """Generic *-list management command. @@ -218,8 +382,11 @@ class ListCommand(SynnefoCommand): # --filter-by option if options["filter_by"]: - filters, excludes = \ - utils.parse_queryset_filters(options["filter_by"]) + try: + filters, excludes = \ + utils.parse_queryset_filters(options["filter_by"]) + except ValueError as e: + raise CommandError(e) else: filters, excludes = ({}, {}) @@ -250,12 +417,13 @@ class ListCommand(SynnefoCommand): objects = self.object_class.objects try: - for sr in select_related: - objects = objects.select_related(sr) - for pr in prefetch_related: - objects = objects.prefetch_related(pr) + if select_related: + objects = objects.select_related(*select_related) + if prefetch_related: + objects = objects.prefetch_related(*prefetch_related) objects = objects.filter(**self.filters) - objects = objects.exclude(**self.excludes) + for key, value in self.excludes.iteritems(): + objects = objects.exclude(**{key: value}) except FieldError as e: raise CommandError(e) except Exception as e: @@ -328,16 +496,17 @@ class ListCommand(SynnefoCommand): % f) def display_filters(self): - headers = ["Filter", "Description", "Help"] + headers = ["Filter", "Description"] table = [] for field in self.object_class._meta.fields: - table.append((field.name, field.verbose_name, field.help_text)) + table.append((field.name, field.verbose_name)) utils.pprint_table(self.stdout, table, headers) -class RemoveCommand(BaseCommand): +class RemoveCommand(SynnefoCommand): help = "Generic remove command" - option_list = BaseCommand.option_list + ( + + command_option_list = ( make_option( "-f", "--force", dest="force", diff --git a/snf-django-lib/snf_django/management/unicodecsv.py b/snf-django-lib/snf_django/management/unicodecsv.py new file mode 100644 index 0000000000000000000000000000000000000000..d4f02db05fe32e02ddd2fe623e3dccf917eb0a95 --- /dev/null +++ b/snf-django-lib/snf_django/management/unicodecsv.py @@ -0,0 +1,38 @@ +# Adapted from http://docs.python.org/2/library/csv.html + +import csv +import codecs +import cStringIO + + +class UnicodeWriter(object): + """CSV Unicode Writer + + A CSV writer which will write rows to CSV file "f", which is encoded in the + given encoding. + + """ + + def __init__(self, f, dialect=csv.excel, encoding="utf-8", + errors='replace', **kwds): + # Redirect output to a queue + self.queue = cStringIO.StringIO() + self.writer = csv.writer(self.queue, dialect=dialect, **kwds) + self.stream = f + self.encoder = codecs.getincrementalencoder(encoding)(errors=errors) + + def writerow(self, row): + self.writer.writerow([s.encode("utf-8") for s in row]) + # Fetch UTF-8 output from the queue ... + data = self.queue.getvalue() + data = data.decode("utf-8") + # ... and reencode it into the target encoding + data = self.encoder.encode(data) + # write to the target stream + self.stream.write(data) + # empty queue + self.queue.truncate(0) + + def writerows(self, rows): + for row in rows: + self.writerow(row) diff --git a/snf-django-lib/snf_django/management/utils.py b/snf-django-lib/snf_django/management/utils.py index 81d710e13e2e673944d78d45ed1bcfc38cc1aeeb..04a422ddc2213f968b774e0cff754d07b8de47ea 100644 --- a/snf-django-lib/snf_django/management/utils.py +++ b/snf-django-lib/snf_django/management/utils.py @@ -1,45 +1,52 @@ -# Copyright 2012 - 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import json -import csv -import functools import operator +import locale +import unicodedata + from datetime import datetime from django.utils.timesince import timesince, timeuntil from django.db.models.query import QuerySet +from django.utils.encoding import smart_unicode, smart_str +from snf_django.management.unicodecsv import UnicodeWriter + + +def smart_locale_unicode(s, **kwargs): + """Wrapper around 'smart_unicode' using user's preferred encoding.""" + encoding = locale.getpreferredencoding() + return smart_unicode(s, encoding=encoding, **kwargs) -from synnefo.util.text import uenc, udec + +def smart_locale_str(s, errors='replace', **kwargs): + """Wrapper around 'smart_str' using user's preferred encoding.""" + encoding = locale.getpreferredencoding() + return smart_str(s, encoding=encoding, errors=errors, **kwargs) + + +def escape_ctrl_chars(s): + """Escape control characters from unicode and string objects.""" + if isinstance(s, unicode): + return "".join(ch.encode("unicode_escape") + if unicodedata.category(ch)[0] == "C" else + ch for ch in s) + if isinstance(s, basestring): + return "".join([c if 31 < ord(c) < 127 else c.encode("string_escape") + for c in s]) + return s def parse_bool(value, strict=True): @@ -49,6 +56,9 @@ def parse_bool(value, strict=True): converted to boolean. Otherwise the string will be returned as is. """ + if isinstance(value, bool): + return value + if value.lower() in ("yes", "true", "t", "1"): return True if value.lower() in ("no", "false", "f", "0"): @@ -117,7 +127,11 @@ def parse_queryset_filters(filters): filter_dict[key + new_op] = parse_bool(val, strict=False) break else: - raise ValueError("Unknown filter expression: %s" % filter_str) + msg = (("Invalid filter-by expression '%s'." + " Filters must be of the form \"key `operator` value\"." + " Available operators: " + + ",".join([k for k, v in OP_MAP])) % filter_str) + raise ValueError(msg) return (filter_dict, exclude_dict) @@ -162,6 +176,14 @@ def filter_object_results(results, filters): return results +def print_title(out, title, t_length): + if title is not None: + t_length = max(t_length, len(title)) + out.write("-" * t_length + "\n") + out.write(title.center(t_length) + "\n") + out.write("-" * t_length + "\n") + + def pprint_table(out, table, headers=None, output_format='pretty', separator=None, vertical=False, title=None): """Print a pretty, aligned string representation of table. @@ -176,15 +198,12 @@ def pprint_table(out, table, headers=None, output_format='pretty', sep = separator if separator else " " - def stringnify(obj): - if isinstance(obj, (unicode, str)): - return udec(obj) - else: - return str(obj) - if headers: - headers = map(stringnify, headers) - table = [map(stringnify, row) for row in table] + headers = map(smart_unicode, headers) + table = [map(smart_unicode, row) for row in table] + + if output_format != "json": + table = [[escape_ctrl_chars(c) for c in row] for row in table] if output_format == "json": assert(headers is not None), "json output format requires headers" @@ -192,20 +211,22 @@ def pprint_table(out, table, headers=None, output_format='pretty', out.write(json.dumps(table, indent=4)) out.write("\n") elif output_format == "csv": - cw = csv.writer(out) + enc = locale.getpreferredencoding() + cw = UnicodeWriter(out, encoding=enc) if headers: table.insert(0, headers) - table = map(functools.partial(map, uenc), table) cw.writerows(table) elif output_format == "pretty": if vertical: - assert(len(table) == 1) row = table[0] max_key = max(map(len, headers)) + max_val = max(map(len, row)) + t_length = max_key + len(sep) + max_val + print_title(out, title, t_length) + assert(len(table) == 1) for row in table: for (k, v) in zip(headers, row): - k = uenc(k.ljust(max_key)) - v = uenc(v) + k = k.ljust(max_key) out.write("%s: %s\n" % (k, v)) else: # Find out the max width of each column @@ -213,21 +234,17 @@ def pprint_table(out, table, headers=None, output_format='pretty', widths = [max(map(len, col)) for col in zip(*(columns))] t_length = sum(widths) + len(sep) * (len(widths) - 1) - if title is not None: - t_length = max(t_length, len(title)) - out.write("-" * t_length + "\n") - out.write(title.center(t_length) + "\n") - out.write("-" * t_length + "\n") + print_title(out, title, t_length) if headers: # pretty print the headers - line = sep.join(uenc(v.rjust(w)) + line = sep.join(v.rjust(w) for v, w in zip(headers, widths)) out.write(line + "\n") out.write("-" * t_length + "\n") # print the rest table for row in table: - line = sep.join(uenc(v.rjust(w)) for v, w in zip(row, widths)) + line = sep.join(v.rjust(w) for v, w in zip(row, widths)) out.write(line + "\n") else: raise ValueError("Unknown output format '%s'" % output_format) diff --git a/snf-django-lib/snf_django/utils/db.py b/snf-django-lib/snf_django/utils/db.py new file mode 100644 index 0000000000000000000000000000000000000000..d24d2f1892f1b592624394d2d431e819d7997bcd --- /dev/null +++ b/snf-django-lib/snf_django/utils/db.py @@ -0,0 +1,40 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from django.conf import settings + +ASTAKOS_DATABASE = "astakos" +CYCLADES_DATABASE = "cyclades" +SYNNEFO_ROUTER = "snf_django.utils.routers.SynnefoRouter" + + +def select_db(app): + """Database selection based on the provided app and database settings. + + This function takes as argument a Synnefo app name and returns the database + where its models are stored. Commonly, if this function is called from + astakos code, the provided app name should be "im". Likewise, if this + function is called from cyclades code, the app name should be "db". + """ + routers = getattr(settings, "DATABASE_ROUTERS", []) + if not routers or SYNNEFO_ROUTER not in settings.DATABASE_ROUTERS: + return "default" + + if (app in ["im", "auth", "quotaholder_app"] and + ASTAKOS_DATABASE in settings.DATABASES): + return ASTAKOS_DATABASE + elif app == "db" and CYCLADES_DATABASE in settings.DATABASES: + return CYCLADES_DATABASE + return "default" diff --git a/snf-django-lib/snf_django/utils/line_logging.py b/snf-django-lib/snf_django/utils/line_logging.py new file mode 100644 index 0000000000000000000000000000000000000000..ab3f0dad18281b4f60f663f7effdf7b2503bb578 --- /dev/null +++ b/snf-django-lib/snf_django/utils/line_logging.py @@ -0,0 +1,67 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import logging + + +class NewlineStreamHandler(logging.StreamHandler): + """A StreamHandler with configurable message terminator + + When StreamHandler writes a formatted log message to its stream, it + adds a newline terminator. This behavior is inherited by FileHandler + and the other classes which derive from it (such as the rotating file + handlers). + + Starting with Python 3.2, the message terminator will be configurable. + This has been done by adding a terminator attribute to StreamHandler, + which when emitting an event now writes the formatted message to its + stream first, and then writes the terminator. If you don't want + newline termination for a handler, just set the handler instance's + terminator attribute to the empty string. + + This is the StreamHandler class from python 3.2 + + """ + terminator = '\n' + + def __init__(self, stream=None): + """Initialize the handler.""" + super(NewlineStreamHandler, self).__init__(stream) + + def flush(self): + """Flushes the stream.""" + if self.stream and hasattr(self.stream, "flush"): + self.stream.flush() + + def emit(self, record): + """Emit a record. + + If a formatter is specified, it is used to format the record. + The record is then written to the stream with a trailing newline. If + exception information is present, it is formatted using + traceback.print_exception and appended to the stream. If the stream + has an 'encoding' attribute, it is used to determine how to do the + output to the stream. + """ + try: + msg = self.format(record) + stream = self.stream + stream.write(msg) + stream.write(self.terminator) + self.flush() + except (KeyboardInterrupt, SystemExit): + raise + except: + self.handleError(record) diff --git a/snf-django-lib/snf_django/utils/reconcile.py b/snf-django-lib/snf_django/utils/reconcile.py new file mode 100644 index 0000000000000000000000000000000000000000..9605afa9adae8d3d1d40f9c490257dad9ce6ab90 --- /dev/null +++ b/snf-django-lib/snf_django/utils/reconcile.py @@ -0,0 +1,113 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +def strcontext(user): + if user is None: + return "" + return "user: %s, " % user + + +def get_qh_values(qh_values, user=None): + prefix = "" if user is not None else "project_" + try: + usage = qh_values[prefix+"usage"] + pending = qh_values[prefix+"pending"] + except KeyError: + raise AttributeError("Malformed quota response.") + return usage, pending + + +def check_projects(stderr, resources, db_usage, qh_usage, user=None): + write = stderr.write + unsynced = [] + pending_exists = False + unknown_exists = False + + projects = set(db_usage.keys()) + projects.update(qh_usage.keys()) + + for project in projects: + db_project_usage = db_usage.get(project, {}) + try: + qh_project_usage = qh_usage[project] + except KeyError: + write("No holdings for %sproject: %s.\n" + % (strcontext(user), project)) + unknown_exists = True + continue + + for resource in resources: + db_value = db_project_usage.get(resource, 0) + try: + qh_values = qh_project_usage[resource] + except KeyError: + write("No holding for %sproject: %s, resource: %s.\n" + % (strcontext(user), project, resource)) + continue + + qh_value, qh_pending = get_qh_values(qh_values, user=user) + if qh_pending: + write("Pending commission for %sproject: %s, resource: %s.\n" + % (strcontext(user), project, resource)) + pending_exists = True + continue + if db_value != qh_value: + tail = (resource, db_value, qh_value) + head = (("project", project, None) if user is None + else ("user", user, project)) + unsynced.append(head + tail) + return unsynced, pending_exists, unknown_exists + + +def check_users(stderr, resources, db_usage, qh_usage): + write = stderr.write + unsynced = [] + pending_exists = False + unknown_exists = False + + users = set(db_usage.keys()) + users.update(qh_usage.keys()) + users.discard(None) + + for user in users: + db_user_usage = db_usage.get(user, {}) + try: + qh_user_usage = qh_usage[user] + except KeyError: + write("No holdings for user: %s.\n" % user) + unknown_exists = True + continue + uns, pend, unkn = check_projects(stderr, resources, + db_user_usage, qh_user_usage, + user=user) + unsynced += uns + pending_exists = pending_exists or pend + unknown_exists = unknown_exists or unkn + return unsynced, pending_exists, unknown_exists + + +def create_user_provisions(provision_list): + provisions = {} + for _, holder, source, resource, db_value, qh_value in provision_list: + provisions[(holder, source, resource)] = db_value - qh_value + return provisions + + +def create_project_provisions(provision_list): + provisions = {} + for _, holder, _, resource, db_value, qh_value in provision_list: + provisions[(holder, resource)] = db_value - qh_value + return provisions diff --git a/snf-django-lib/snf_django/utils/routers.py b/snf-django-lib/snf_django/utils/routers.py new file mode 100644 index 0000000000000000000000000000000000000000..814095cdafcf5e688c1ffca94f281041a784de68 --- /dev/null +++ b/snf-django-lib/snf_django/utils/routers.py @@ -0,0 +1,39 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +""" +Router for the Astakos/Cyclades app. It is used to specify which database will +be used for each model. +""" + +from snf_django.utils.db import select_db + + +class SynnefoRouter(object): + + """Router for Astakos/Cyclades models.""" + + def db_for_read(self, model, **hints): + """Select db to read.""" + app = model._meta.app_label + return select_db(app) + + def db_for_write(self, model, **hints): + """Select db to write.""" + app = model._meta.app_label + return select_db(app) + + # The rest of the methods are ommited since relations and syncing should + # not affect the router. diff --git a/snf-django-lib/snf_django/utils/testing.py b/snf-django-lib/snf_django/utils/testing.py index aed9cf8e68d5ff2f6e03dde7fc932867517451c2..de1980bbeea1bcb48834dfb28b7c099aefedcd2a 100644 --- a/snf-django-lib/snf_django/utils/testing.py +++ b/snf-django-lib/snf_django/utils/testing.py @@ -1,41 +1,23 @@ -# Copyright 2011-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from contextlib import contextmanager from django.test import TestCase from django.utils import simplejson as json -from synnefo.util import text +from django.utils.encoding import smart_unicode from mock import patch @@ -138,25 +120,26 @@ def astakos_user(user): "expires": "2013-06-19T15:23:59.975572+00:00", "id": "DummyToken", "tenant": { - "id": text.udec(user, 'utf8'), + "id": smart_unicode(user, encoding='utf-8'), "name": "Firstname Lastname" } }, "serviceCatalog": [], "user": { "roles_links": [], - "id": text.udec(user, 'utf8'), + "id": smart_unicode(user, encoding='utf-8'), "roles": [{"id": 1, "name": "default"}], "name": "Firstname Lastname"}} } - with patch('astakosclient.AstakosClient.get_quotas') as m3: - m3.return_value = { + with patch('astakosclient.AstakosClient.service_get_quotas') as m2: + m2.return_value = {user: { "system": { "pithos.diskspace": { "usage": 0, "limit": 1073741824, # 1GB "pending": 0 + } } } } @@ -204,12 +187,19 @@ def mocked_quotaholder(success=True): return (len(astakos.return_value.issue_one_commission.mock_calls) + serial) astakos.return_value.issue_one_commission.side_effect = foo + def resolve_mock(*args, **kwargs): return {"failed": [], "accepted": args[0], "rejected": args[1], } astakos.return_value.resolve_commissions.side_effect = resolve_mock + + def reassign(*args, **kwargs): + v = len( + astakos.return_value.issue_resource_reassignment.mock_calls) + return v + serial + astakos.return_value.issue_resource_reassignment.side_effect = reassign yield astakos.return_value @@ -220,6 +210,12 @@ class BaseAPITest(TestCase): response = self.client.get(url, *args, **kwargs) return response + def head(self, url, user='user', *args, **kwargs): + with astakos_user(user): + with mocked_quotaholder(): + response = self.client.head(url, *args, **kwargs) + return response + def delete(self, url, user='user'): with astakos_user(user): with mocked_quotaholder() as m: diff --git a/snf-django-lib/snf_django/utils/transaction.py b/snf-django-lib/snf_django/utils/transaction.py new file mode 100644 index 0000000000000000000000000000000000000000..6c7015ae49ad9c98e1d44d7f399e02da7720bf0b --- /dev/null +++ b/snf-django-lib/snf_django/utils/transaction.py @@ -0,0 +1,88 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from snf_django.utils.db import select_db + + +def _transaction_func(app, method, using): + """Synnefo wrapper for Django transactions. + + This function serves as a wrapper for Django transaction functions. It is + mainly used by Synnefo transaction functions and its goal is to assist in + using transactions in a multi-database setup. + + Arguments: + @app: The name of app that initiates the transaction (e.g. "db", "im"), + @method: The actual Django transaction function that will be used (e.g. + "commit_manually", "commit_on_success") + @using: The database to use for the transaction (e.g. "cyclades", + "astakos") + + Returns: + Either a decorator/context manageer or a wrapped function, depending on + how the aforementioned Synnefo transaction functions are called. + + To illustrate the return value, let's consider the following Cyclades + transaction function: + + >>> from django.db import transaction + >>> def commit_on_success(using=None): + >>> method = transaction.commit_on_success + >>> return _transaction_func("db", method, using) + + We present below two possible uses of the above function and what will + _transaction_func return for each of them: + + 1) Decorator with provided database: + + >>> @transaction.commit_on_success(using="other_db") + >>> def func(...): + >>> ... + + In this case, the arguments for _transaction_func are: + app = "db" + method = transaction.commit_on_success + using = "other_db" + + The returned result is the following_decorator: + + transaction.commit_on_success(using="other_db") + + 2) Decorator with no database provided: + + >>> @transaction.commit_on_success + >>> def func(...): + >>> ... + + In this case, the arguments for _transaction_func are: + app = "db" + method = transaction.commit_on_success + using = func + + Not so surpringly, in this case the "using" argument contains the function + to wrap instead of a database. Therefore, _transaction_func will determine + what is the appropriate database for the app and return the decorated + function: + + transaction.commit_on_success(using="db")(func) + """ + db = using + if not using or callable(using): + db = select_db(app) + + if callable(using): + return method(using=db)(using) + else: + return method(using=db) diff --git a/snf-django-lib/snf_django/utils/urls.py b/snf-django-lib/snf_django/utils/urls.py index de0b8f4bc6c05ad8c6b2e3401d39dd8ee75dfaee..9903e7721ea40f4e3b8e7cd5db2f9ffabb016227 100644 --- a/snf-django-lib/snf_django/utils/urls.py +++ b/snf-django-lib/snf_django/utils/urls.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.conf.urls import url, patterns diff --git a/snf-pithos-app/MANIFEST.in b/snf-pithos-app/MANIFEST.in index 6486317c0ec390160eac0c29c25c2538f40b0dab..76045e59ea3f0b1d12e19999a2334f960cd3cfbb 100644 --- a/snf-pithos-app/MANIFEST.in +++ b/snf-pithos-app/MANIFEST.in @@ -1,4 +1,3 @@ recursive-include pithos *.json *.html *.json *.xml *.txt -recursive-include pithos/ui/static * -include distribute_setup.py +include distribute_setup.py README.md diff --git a/snf-pithos-app/README.md b/snf-pithos-app/README.md new file mode 100644 index 0000000000000000000000000000000000000000..3df9db51b3830b8af4955f038c35c3465ab91413 --- /dev/null +++ b/snf-pithos-app/README.md @@ -0,0 +1,27 @@ +snf-pithos-app +============== + +Overview +-------- + +This is Synnefo's snf-pithos-app component. Please see the [official Synnefo +site](http://www.synnefo.org) for more information. + + +Copyright and license +===================== + +Copyright (C) 2010-2014 GRNET S.A. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. diff --git a/snf-pithos-app/conf/20-snf-pithos-app-settings.conf b/snf-pithos-app/conf/20-snf-pithos-app-settings.conf index 4809b52b576b57eb67573e40f1f6597c5efe8f94..e1491aca4cc95f4507f7f600230ac1a1c972e7d7 100644 --- a/snf-pithos-app/conf/20-snf-pithos-app-settings.conf +++ b/snf-pithos-app/conf/20-snf-pithos-app-settings.conf @@ -10,8 +10,6 @@ # Block storage. #PITHOS_BACKEND_BLOCK_MODULE = 'pithos.backends.lib.hashfiler' -#PITHOS_BACKEND_BLOCK_PATH = '/tmp/pithos-data/' -#PITHOS_BACKEND_BLOCK_UMASK = 0o022 # Default setting for new accounts. #PITHOS_BACKEND_VERSIONING = 'auto' @@ -25,14 +23,6 @@ # Service Token acquired by identity provider. #PITHOS_SERVICE_TOKEN = '' -# Select Pithos backend storage. Possible values are 'nfs' and 'rados' -#PITHOS_BACKEND_STORAGE = 'nfs' - -# Configure rados storage for pithos -#PITHOS_RADOS_CEPH_CONF = '/etc/ceph/ceph.conf' -#PITHOS_RADOS_POOL_BLOCKS = 'blocks' -#PITHOS_RADOS_POOL_MAPS = 'maps' - # This enables a ui compatibility layer for the introduction of UUIDs in # identity management. WARNING: Setting to True will break your installation. # PITHOS_TRANSLATE_UUIDS = False @@ -66,3 +56,24 @@ # Set domain to restrict requests of pithos object contents serve endpoint or # None for no domain restriction #PITHOS_UNSAFE_DOMAIN = None +# +#Archipelago Configuration File +#PITHOS_BACKEND_ARCHIPELAGO_CONF = '/etc/archipelago/archipelago.conf' +# +# Archipelagp xseg pool size +#PITHOS_BACKEND_XSEG_POOL_SIZE = 8 +# +# The maximum interval (in seconds) for consequent backend object map checks +#PITHOS_BACKEND_MAP_CHECK_INTERVAL = 1 +# The archipelago mapfile prefix (it should not exceed 15 characters) +# WARNING: Once set it should not be changed +#PITHOS_BACKEND_MAPFILE_PREFIX='snf_file_' +# +# The maximum allowed metadata items per domain for a Pithos+ resource +#PITHOS_RESOURCE_MAX_METADATA = 32 +# +# The maximum allowed groups for a Pithos+ account. +#PITHOS_ACC_MAX_GROUPS = 32 +# +# The maximum allowed group members per group. +#PITHOS_ACC_MAX_GROUP_MEMBERS = 32 diff --git a/snf-pithos-app/pithos/__init__.py b/snf-pithos-app/pithos/__init__.py index c78fbe672324992f6c941a7828e4827928b270c9..5cda842903d0bfec117db1c3c5a43281d46eb551 100644 --- a/snf-pithos-app/pithos/__init__.py +++ b/snf-pithos-app/pithos/__init__.py @@ -1,35 +1,17 @@ -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # this is a namespace package try: diff --git a/snf-pithos-app/pithos/api/dispatch.py b/snf-pithos-app/pithos/api/dispatch.py index b3ceed2b1970d5ba96c8ff33020c75770245a1df..079354e2d2797f8d9c3968a51ffc60782ad361d3 100644 --- a/snf-pithos-app/pithos/api/dispatch.py +++ b/snf-pithos-app/pithos/api/dispatch.py @@ -1,35 +1,17 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. #from pithos.backends import connect_backend from pithos.api.util import hashmap_md5, get_backend @@ -57,7 +39,7 @@ def update_md5(m): meta = backend.get_object_meta( account, account, container, name, 'pithos', version) if meta['checksum'] == '': - size, hashmap = backend.get_object_hashmap( + _, size, hashmap = backend.get_object_hashmap( account, account, container, name, version) checksum = hashmap_md5(backend, hashmap, size) backend.update_object_checksum( diff --git a/snf-pithos-app/pithos/api/functions.py b/snf-pithos-app/pithos/api/functions.py index b04fc5545a79b395d34c03c90db6712ef021afa0..245fbbac3037667f7a3c1d8e95c2873a9e71b7cc 100644 --- a/snf-pithos-app/pithos/api/functions.py +++ b/snf-pithos-app/pithos/api/functions.py @@ -1,41 +1,22 @@ -# Copyright 2011-2014 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.http import HttpResponse from django.template.loader import render_to_string from django.utils import simplejson as json from django.utils.http import parse_etags -from django.utils.encoding import smart_str from django.views.decorators.csrf import csrf_exempt from astakosclient import AstakosClient @@ -64,7 +45,8 @@ from pithos.api import settings from pithos.backends.base import ( NotAllowedError, QuotaError, ContainerNotEmpty, ItemNotExists, - VersionNotExists, ContainerExists, InvalidHash) + VersionNotExists, ContainerExists, InvalidHash, IllegalOperationError, + InconsistentContentSize, InvalidPolicy) from pithos.backends.filter import parse_filters @@ -104,7 +86,7 @@ def account_demux(request, v_account): if TRANSLATE_UUIDS: if not is_uuid(v_account): uuids = get_uuids([v_account]) - if not uuids or not v_account in uuids: + if not uuids or v_account not in uuids: return HttpResponse(status=404) v_account = uuids[v_account] @@ -126,7 +108,7 @@ def container_demux(request, v_account, v_container): if TRANSLATE_UUIDS: if not is_uuid(v_account): uuids = get_uuids([v_account]) - if not uuids or not v_account in uuids: + if not uuids or v_account not in uuids: return HttpResponse(status=404) v_account = uuids[v_account] @@ -156,7 +138,7 @@ def object_demux(request, v_account, v_container, v_object): if TRANSLATE_UUIDS: if not is_uuid(v_account): uuids = get_uuids([v_account]) - if not uuids or not v_account in uuids: + if not uuids or v_account not in uuids: return HttpResponse(status=404) v_account = uuids[v_account] @@ -292,6 +274,7 @@ def account_meta(request, v_account): getattr(request, 'token', None), groups[k]) policy = request.backend.get_account_policy( request.user_uniq, v_account) + logger.debug(policy) except NotAllowedError: raise faults.Forbidden('Not allowed') @@ -485,8 +468,8 @@ def container_create(request, v_account, v_container): ret = 201 except NotAllowedError: raise faults.Forbidden('Not allowed') - except ValueError: - raise faults.BadRequest('Invalid policy header') + except InvalidPolicy, e: + raise faults.BadRequest(e.args[0]) except ContainerExists: ret = 202 @@ -499,8 +482,10 @@ def container_create(request, v_account, v_container): raise faults.Forbidden('Not allowed') except ItemNotExists: raise faults.ItemNotFound('Container does not exist') - except ValueError: - raise faults.BadRequest('Invalid policy header') + except InvalidPolicy, e: + raise faults.BadRequest(e.args[0]) + except QuotaError, e: + raise faults.RequestEntityTooLarge('Quota error: %s' % e) if meta: try: request.backend.update_container_meta(request.user_uniq, v_account, @@ -536,8 +521,10 @@ def container_update(request, v_account, v_container): raise faults.Forbidden('Not allowed') except ItemNotExists: raise faults.ItemNotFound('Container does not exist') - except ValueError: - raise faults.BadRequest('Invalid policy header') + except InvalidPolicy, e: + raise faults.BadRequest(e.args[0]) + except QuotaError, e: + raise faults.RequestEntityTooLarge('Quota error: %s' % e) if meta or replace: try: request.backend.update_container_meta(request.user_uniq, v_account, @@ -557,6 +544,13 @@ def container_update(request, v_account, v_container): if (content_type and content_type == 'application/octet-stream' and content_length != 0): + + try: + request.backend.can_write_container(request.user_uniq, v_account, + v_container) + except NotAllowedError: + raise faults.Forbidden('Not allowed') + for data in socket_read_iterator(request, content_length, request.backend.block_size): # TODO: Raise 408 (Request Timeout) if this takes too long. @@ -655,8 +649,7 @@ def object_list(request, v_account, v_container): keys = request.GET.get('meta') if keys: - keys = [smart_str(x.strip()) for x in keys.split(',') - if x.strip() != ''] + keys = [x.strip() for x in keys.split(',') if x.strip() != ''] included, excluded, opers = parse_filters(keys) keys = [] keys += [format_header_key('X-Object-Meta-' + x) for x in included] @@ -931,7 +924,7 @@ def _object_read(request, v_account, v_container, v_object): try: for x in objects: - s, h = \ + snap, s, h = \ request.backend.get_object_hashmap( request.user_uniq, v_account, src_container, x[0], x[1]) @@ -943,9 +936,11 @@ def _object_read(request, v_account, v_container, v_object): raise faults.ItemNotFound('Object does not exist') except VersionNotExists: raise faults.ItemNotFound('Version does not exist') + except IllegalOperationError, e: + raise faults.Forbidden(str(e)) else: try: - s, h = request.backend.get_object_hashmap( + snap, s, h = request.backend.get_object_hashmap( request.user_uniq, v_account, v_container, v_object, version) sizes.append(s) @@ -956,6 +951,8 @@ def _object_read(request, v_account, v_container, v_object): raise faults.ItemNotFound('Object does not exist') except VersionNotExists: raise faults.ItemNotFound('Version does not exist') + except IllegalOperationError, e: + raise faults.Forbidden(str(e)) # Reply with the hashmap. if hashmap_reply: @@ -1096,6 +1093,10 @@ def object_write(request, v_account, v_container, v_object): request.user_uniq, v_account, v_container, v_object, size, content_type, hashmap, checksum, 'pithos', meta, True, permissions ) + except IllegalOperationError, e: + raise faults.Forbidden(e[0]) + except InconsistentContentSize, e: + raise faults.BadRequest(e[0]) except NotAllowedError: raise faults.Forbidden('Not allowed') except IndexError, e: @@ -1111,6 +1112,7 @@ def object_write(request, v_account, v_container, v_object): raise faults.RequestEntityTooLarge('Quota error: %s' % e) except InvalidHash, e: raise faults.BadRequest('Invalid hash: %s' % e) + if not checksum and UPDATE_MD5: # Update the MD5 after the hashmap, as there may be missing hashes. checksum = hashmap_md5(request.backend, hashmap, size) @@ -1157,6 +1159,10 @@ def object_write_form(request, v_account, v_container, v_object): request.user_uniq, v_account, v_container, v_object, file.size, file.content_type, file.hashmap, checksum, 'pithos', {}, True ) + except IllegalOperationError, e: + faults.Forbidden(e[0]) + except InconsistentContentSize, e: + raise faults.BadRequest(e[0]) except NotAllowedError: raise faults.Forbidden('Not allowed') except ItemNotExists: @@ -1342,13 +1348,15 @@ def object_update(request, v_account, v_container, v_object): raise faults.RangeNotSatisfiable('Invalid Content-Range header') try: - size, hashmap = \ + is_snapshot, size, hashmap = \ request.backend.get_object_hashmap( request.user_uniq, v_account, v_container, v_object) except NotAllowedError: raise faults.Forbidden('Not allowed') except ItemNotExists: raise faults.ItemNotFound('Object does not exist') + except IllegalOperationError, e: + raise faults.Forbidden(str(e)) offset, length, total = ranges if offset is None: @@ -1367,13 +1375,18 @@ def object_update(request, v_account, v_container, v_object): try: src_version = request.META.get('HTTP_X_SOURCE_VERSION') - src_size, src_hashmap = request.backend.get_object_hashmap( - request.user_uniq, - src_account, src_container, src_name, src_version) + src_is_snapshot, src_size, src_hashmap = \ + request.backend.get_object_hashmap(request.user_uniq, + src_account, + src_container, + src_name, + src_version) except NotAllowedError: raise faults.Forbidden('Not allowed') except ItemNotExists: raise faults.ItemNotFound('Source object does not exist') + except IllegalOperationError, e: + raise faults.Forbidden(str(e)) if length is None: length = src_size @@ -1419,8 +1432,11 @@ def object_update(request, v_account, v_container, v_object): hashmap[bi] = src_hashmap[sbi] else: data = request.backend.get_block(src_hashmap[sbi]) - hashmap[bi] = request.backend.update_block( - hashmap[bi], data[:bl], 0) + try: + hashmap[bi] = request.backend.update_block( + hashmap[bi], data[:bl], 0) + except IllegalOperationError, e: + raise faults.Forbidden(e[0]) else: hashmap.append(src_hashmap[sbi]) offset += bl @@ -1434,7 +1450,8 @@ def object_update(request, v_account, v_container, v_object): data += request.backend.get_block(src_hashmap[sbi]) if length < request.backend.block_size: data = data[:length] - bytes = put_object_block(request, hashmap, data, offset) + bytes = put_object_block(request, hashmap, data, offset, + is_snapshot=src_is_snapshot) offset += bytes data = data[bytes:] length -= bytes @@ -1447,11 +1464,13 @@ def object_update(request, v_account, v_container, v_object): # TODO: Raise 499 (Client Disconnect) if a length is defined # and we stop before getting this much data. data += d - bytes = put_object_block(request, hashmap, data, offset) + bytes = put_object_block(request, hashmap, data, offset, + is_snapshot=is_snapshot) offset += bytes data = data[bytes:] if len(data) > 0: - bytes = put_object_block(request, hashmap, data, offset) + bytes = put_object_block(request, hashmap, data, offset, + is_snapshot=is_snapshot) offset += bytes if offset > size: @@ -1467,6 +1486,10 @@ def object_update(request, v_account, v_container, v_object): prev_meta['type'], hashmap, checksum, 'pithos', meta, replace, permissions ) + except IllegalOperationError, e: + raise faults.Forbidden(e[0]) + except InconsistentContentSize, e: + raise faults.BadRequest(e[0]) except NotAllowedError: raise faults.Forbidden('Not allowed') except ItemNotExists: @@ -1475,6 +1498,7 @@ def object_update(request, v_account, v_container, v_object): raise faults.BadRequest('Invalid sharing header') except QuotaError, e: raise faults.RequestEntityTooLarge('Quota error: %s' % e) + if public is not None: try: request.backend.update_object_public(request.user_uniq, v_account, diff --git a/snf-pithos-app/pithos/api/manage_accounts/__init__.py b/snf-pithos-app/pithos/api/manage_accounts/__init__.py index e457bb6b78bdd6699ff46c639f41346585d52ed4..542559aa98504bae2e059ab5d2cfccebebf008d0 100644 --- a/snf-pithos-app/pithos/api/manage_accounts/__init__.py +++ b/snf-pithos-app/pithos/api/manage_accounts/__init__.py @@ -1,35 +1,17 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import re import os diff --git a/snf-pithos-app/pithos/api/manage_accounts/cli/__init__.py b/snf-pithos-app/pithos/api/manage_accounts/cli/__init__.py index 97d36b00826a70144417d74e1d7ce8aee77370a7..4ca2fc5113814cdbf31ed98f41a46b7ee33f5074 100644 --- a/snf-pithos-app/pithos/api/manage_accounts/cli/__init__.py +++ b/snf-pithos-app/pithos/api/manage_accounts/cli/__init__.py @@ -1,35 +1,17 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from pithos.api.manage_accounts import ManageAccounts from snf_django.management.utils import pprint_table @@ -173,7 +155,7 @@ def main(argv=None): 'list', description="List existing accounts" ) parser_list.add_argument( - '--dublicate', dest='only_duplicate', action="store_true", + '--duplicate', dest='only_duplicate', action="store_true", default=False, help="Display only case insensitive duplicate accounts." ) parser_list.add_argument( diff --git a/snf-pithos-app/pithos/api/manage_accounts/tests.py b/snf-pithos-app/pithos/api/manage_accounts/tests.py index 75225532d71cad3218161e3ed27316c3cd31308a..2f0e41762cc8989f281bbb6aab3d29d6fc24db83 100644 --- a/snf-pithos-app/pithos/api/manage_accounts/tests.py +++ b/snf-pithos-app/pithos/api/manage_accounts/tests.py @@ -1,35 +1,17 @@ -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import unittest import uuid diff --git a/snf-pithos-app/pithos/api/management/commands/file-show.py b/snf-pithos-app/pithos/api/management/commands/file-show.py index 35d6c200b31660516224652a0d03099fd962e424..4aae6dbe5ed7a6778c8f6ae8f43b021d6161621c 100644 --- a/snf-pithos-app/pithos/api/management/commands/file-show.py +++ b/snf-pithos-app/pithos/api/management/commands/file-show.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.core.management.base import CommandError @@ -93,9 +75,8 @@ class Command(SynnefoCommand): update_public_meta(public, kv) if options['hashmap']: - _, kv['hashmap'] = b.get_object_hashmap(account, account, - container, name, - options['obj_version']) + _, size, kv['hashmap'] = b.get_object_hashmap( + account, account, container, name, options['obj_version']) utils.pprint_table(self.stdout, [kv.values()], kv.keys(), options["output_format"], vertical=True) diff --git a/snf-pithos-app/pithos/api/management/commands/reconcile-commissions-pithos.py b/snf-pithos-app/pithos/api/management/commands/reconcile-commissions-pithos.py index 1ad07ef73469ea84f78b3620def4cbca35391b27..5d1a350c78bf3b3df9e13cef614a977b6fb1417d 100644 --- a/snf-pithos-app/pithos/api/management/commands/reconcile-commissions-pithos.py +++ b/snf-pithos-app/pithos/api/management/commands/reconcile-commissions-pithos.py @@ -1,40 +1,23 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. -from django.core.management.base import NoArgsCommand, CommandError +from django.core.management.base import CommandError from optparse import make_option from pithos.api.util import get_backend +from snf_django.management.commands import SynnefoCommand import logging @@ -43,10 +26,10 @@ logger = logging.getLogger(__name__) CLIENTKEY = 'pithos' -class Command(NoArgsCommand): +class Command(SynnefoCommand): help = "Display unresolved commissions and trigger their recovery" - option_list = NoArgsCommand.option_list + ( + option_list = SynnefoCommand.option_list + ( make_option('--fix', dest='fix', action="store_true", @@ -54,7 +37,7 @@ class Command(NoArgsCommand): help="Fix unresolved commissions"), ) - def handle_noargs(self, **options): + def handle(self, **options): b = get_backend() try: b.pre_exec() diff --git a/snf-pithos-app/pithos/api/management/commands/reconcile-resources-pithos.py b/snf-pithos-app/pithos/api/management/commands/reconcile-resources-pithos.py index 6431c74f796263157d2cf2e304dbf90c03cfff47..673523a04de99b1ae3a5a796bd703388c3fb94e8 100644 --- a/snf-pithos-app/pithos/api/management/commands/reconcile-resources-pithos.py +++ b/snf-pithos-app/pithos/api/management/commands/reconcile-resources-pithos.py @@ -1,62 +1,48 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. -from django.core.management.base import NoArgsCommand +from datetime import datetime +from django.core.management.base import CommandError from optparse import make_option from pithos.api.util import get_backend -from pithos.api.resources import resources -from pithos.backends.modular import DEFAULT_SOURCE from snf_django.management import utils +from snf_django.management.commands import SynnefoCommand from astakosclient.errors import QuotaLimit, NotFound +from snf_django.utils import reconcile backend = get_backend() +RESOURCES = ['pithos.diskspace'] -class Command(NoArgsCommand): +class Command(SynnefoCommand): help = """Reconcile resource usage of Astakos with Pithos DB. Detect unsynchronized usage between Astakos and Pithos DB resources and synchronize them if specified so. """ - option_list = NoArgsCommand.option_list + ( - make_option("--userid", dest="userid", + option_list = SynnefoCommand.option_list + ( + make_option("--user", dest="userid", default=None, help="Reconcile resources only for this user"), + make_option("--project", + help="Reconcile resources only for this project"), make_option("--fix", dest="fix", default=False, action="store_true", @@ -68,104 +54,79 @@ class Command(NoArgsCommand): "the Pithos quota, independently of their value.") ) - def handle_noargs(self, **options): + def handle(self, **options): + write = self.stdout.write try: backend.pre_exec() userid = options['userid'] + project = options['project'] # Get holding from Pithos DB - db_usage = backend.node.node_account_usage(userid) + db_usage = backend.node.node_account_usage(userid, project) + db_project_usage = backend.node.node_project_usage(project) users = set(db_usage.keys()) if userid and userid not in users: if backend._lookup_account(userid) is None: - self.stdout.write("User '%s' does not exist in DB!\n" % - userid) + write("User '%s' does not exist in DB!\n" % userid) return # Get holding from Quotaholder try: qh_result = backend.astakosclient.service_get_quotas(userid) except NotFound: - self.stdout.write( - "User '%s' does not exist in Quotaholder!\n" % userid) + write("User '%s' does not exist in Quotaholder!\n" % userid) return - users.update(qh_result.keys()) - - pending_exists = False - unknown_user_exists = False - unsynced = [] - for uuid in users: - db_value = db_usage.get(uuid, 0) - try: - qh_all = qh_result[uuid] - except KeyError: - self.stdout.write( - "User '%s' does not exist in Quotaholder!\n" % uuid) - unknown_user_exists = True - continue - else: - qh = qh_all.get(DEFAULT_SOURCE, {}) - for resource in [r['name'] for r in resources]: - try: - qh_resource = qh[resource] - except KeyError: - self.stdout.write( - "Resource '%s' does not exist in Quotaholder " - "for user '%s'!\n" % (resource, uuid)) - continue - - if qh_resource['pending']: - self.stdout.write( - "Pending commission. " - "User '%s', resource '%s'.\n" % - (uuid, resource)) - pending_exists = True - continue - - qh_value = qh_resource['usage'] - - if db_value != qh_value: - data = (uuid, resource, db_value, qh_value) - unsynced.append(data) - + try: + qh_project_result = \ + backend.astakosclient.service_get_project_quotas(project) + except NotFound: + write("Project '%s' does not exist in Quotaholder!\n" % + project) + + unsynced_users, users_pending, users_unknown =\ + reconcile.check_users(self.stderr, RESOURCES, + db_usage, qh_result) + + unsynced_projects, projects_pending, projects_unknown =\ + reconcile.check_projects(self.stderr, RESOURCES, + db_project_usage, qh_project_result) + pending_exists = users_pending or projects_pending + unknown_exists = users_unknown or projects_unknown + + headers = ("Type", "Holder", "Source", "Resource", + "Database", "Quotaholder") + unsynced = unsynced_users + unsynced_projects if unsynced: - headers = ("User", "Resource", "Database", "Quotaholder") utils.pprint_table(self.stdout, unsynced, headers) - if options['fix']: - request = {} - request['force'] = options['force'] - request['auto_accept'] = True - request['name'] = "RECONCILE" - request['provisions'] = map(create_provision, unsynced) + if options["fix"]: + force = options["force"] + name = ("client: reconcile-resources-pithos, time: %s" + % datetime.now()) + user_provisions = reconcile.create_user_provisions( + unsynced_users) + project_provisions = reconcile.create_project_provisions( + unsynced_projects) try: - backend.astakosclient.issue_commission(request) + backend.astakosclient.issue_commission_generic( + user_provisions, project_provisions, name=name, + force=force, auto_accept=True) except QuotaLimit: - self.stdout.write( - "Reconciling failed because a limit has been " - "reached. Use --force to ignore the check.\n") + write("Reconciling failed because a limit has been " + "reached. Use --force to ignore the check.\n") return - self.stdout.write("Fixed unsynced resources\n") + write("Fixed unsynced resources\n") if pending_exists: - self.stdout.write( - "Found pending commissions. Run 'snf-manage" - " reconcile-commissions-pithos'\n") - elif not (unsynced or unknown_user_exists): - self.stdout.write("Everything in sync.\n") + write("Found pending commissions. Run 'snf-manage" + " reconcile-commissions-pithos'\n") + elif not (unsynced or unknown_exists): + write("Everything in sync.\n") except BaseException as e: backend.post_exec(False) - self.stdout.write(str(e) + "\n") + raise CommandError(e) else: backend.post_exec(True) finally: backend.close() - - -def create_provision(provision_info): - user, resource, db_value, qh_value = provision_info - return {"holder": user, - "source": DEFAULT_SOURCE, - "resource": resource, - "quantity": int(db_value - qh_value)} diff --git a/snf-pithos-app/pithos/api/management/commands/service-export-pithos.py b/snf-pithos-app/pithos/api/management/commands/service-export-pithos.py index 3028bf4430bacba7f990a95b64adffe7a2e0c71b..fa254dda8776dd8fd7b7eec94d95e22f133cb6f7 100644 --- a/snf-pithos-app/pithos/api/management/commands/service-export-pithos.py +++ b/snf-pithos-app/pithos/api/management/commands/service-export-pithos.py @@ -1,43 +1,26 @@ -# Copyright 2012-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.utils import simplejson as json -from django.core.management.base import NoArgsCommand + from pithos.api.settings import pithos_services from synnefo.lib.services import filter_public +from snf_django.management.commands import SynnefoCommand -class Command(NoArgsCommand): +class Command(SynnefoCommand): help = "Export Pithos services in JSON format." def handle(self, *args, **options): diff --git a/snf-pithos-app/pithos/api/public.py b/snf-pithos-app/pithos/api/public.py index eb8a41171b306f7851171ff2f4f154ef78984248..0c358f82f47918b27823a68bfbef44a0931db494 100644 --- a/snf-pithos-app/pithos/api/public.py +++ b/snf-pithos-app/pithos/api/public.py @@ -1,35 +1,17 @@ -# Copyright 2011-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt @@ -142,17 +124,17 @@ def public_read(request, v_public): try: for x in objects: - s, h = request.backend.get_object_hashmap(request.user_uniq, - v_account, - src_container, - x[0], x[1]) + _, s, h = request.backend.get_object_hashmap(request.user_uniq, + v_account, + src_container, + x[0], x[1]) sizes.append(s) hashmaps.append(h) except: raise faults.ItemNotFound('Object does not exist') else: try: - s, h = request.backend.get_object_hashmap( + _, s, h = request.backend.get_object_hashmap( request.user_uniq, v_account, v_container, v_object) sizes.append(s) diff --git a/snf-pithos-app/pithos/api/resources.py b/snf-pithos-app/pithos/api/resources.py index 5ef1eb9ee0ee74177cb30b102979da49132e3fce..7b4c03f5e90609acb168827fb672e07fbdf60455 100644 --- a/snf-pithos-app/pithos/api/resources.py +++ b/snf-pithos-app/pithos/api/resources.py @@ -1,37 +1,18 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. -from synnefo.util.keypath import get_path from pithos.api.settings import pithos_services -resources = get_path(pithos_services, 'pithos_object-store.resources').values() +resources = pithos_services['pithos_object-store']['resources'].values() diff --git a/snf-pithos-app/pithos/api/services.py b/snf-pithos-app/pithos/api/services.py index 32dec2a510eff643d63455631523130fe5a06386..da48979473a99cf97516511c98bd1f94bb04ab92 100644 --- a/snf-pithos-app/pithos/api/services.py +++ b/snf-pithos-app/pithos/api/services.py @@ -1,35 +1,17 @@ -# Copyright (C) 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. pithos_services = { diff --git a/snf-pithos-app/pithos/api/settings.py b/snf-pithos-app/pithos/api/settings.py index 09673702f0e0d0f9b6678e1120ae7803e143dc1f..81e255b5b8d304af3f92e5c0b79aa6ac638ed9dc 100644 --- a/snf-pithos-app/pithos/api/settings.py +++ b/snf-pithos-app/pithos/api/settings.py @@ -1,35 +1,17 @@ -# Copyright 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. #coding=utf8 import logging @@ -37,7 +19,6 @@ import logging from django.conf import settings from synnefo.lib import parse_base_url, join_urls from synnefo.lib.services import fill_endpoints -from synnefo.util.keypath import get_path from pithos.api.services import pithos_services as vanilla_pithos_services from astakosclient import AstakosClient @@ -55,16 +36,13 @@ BASE_URL = getattr(settings, 'PITHOS_BASE_URL', # Service Token acquired by identity provider. SERVICE_TOKEN = getattr(settings, 'PITHOS_SERVICE_TOKEN', '') -# Select Pithos backend storage. -BACKEND_STORAGE = getattr(settings, 'PITHOS_BACKEND_STORAGE', 'nfs') - BASE_HOST, BASE_PATH = parse_base_url(BASE_URL) pithos_services = deepcopy(vanilla_pithos_services) fill_endpoints(pithos_services, BASE_URL) -PITHOS_PREFIX = get_path(pithos_services, 'pithos_object-store.prefix') -PUBLIC_PREFIX = get_path(pithos_services, 'pithos_public.prefix') -UI_PREFIX = get_path(pithos_services, 'pithos_ui.prefix') +PITHOS_PREFIX = pithos_services['pithos_object-store']['prefix'] +PUBLIC_PREFIX = pithos_services['pithos_public']['prefix'] +UI_PREFIX = pithos_services['pithos_ui']['prefix'] VIEW_PREFIX = join_urls(UI_PREFIX, 'view') @@ -169,8 +147,7 @@ BACKEND_POOL_SIZE = getattr(settings, 'PITHOS_BACKEND_POOL_SIZE', 5) # Update object checksums. UPDATE_MD5 = getattr(settings, 'PITHOS_UPDATE_MD5', False) -RADOS_CEPH_CONF = getattr(settings, 'PITHOS_RADOS_CEPH_CONF', \ - '/etc/ceph/ceph.conf') +RADOS_STORAGE = getattr(settings, 'PITHOS_RADOS_STORAGE', False) RADOS_POOL_BLOCKS = getattr(settings, 'PITHOS_RADOS_POOL_BLOCKS', 'blocks') RADOS_POOL_MAPS = getattr(settings, 'PITHOS_RADOS_POOL_MAPS', 'maps') @@ -208,3 +185,28 @@ OAUTH2_CLIENT_CREDENTIALS = getattr(settings, # Set domain to restrict requests of pithos object contents serve endpoint or # None for no domain restriction UNSAFE_DOMAIN = getattr(settings, 'PITHOS_UNSAFE_DOMAIN', None) + +# Archipelago Configuration File +BACKEND_ARCHIPELAGO_CONF = getattr(settings, 'PITHOS_BACKEND_ARCHIPELAGO_CONF', + '/etc/archipelago/archipelago.conf') + +# Archipelagp xseg pool size +BACKEND_XSEG_POOL_SIZE = getattr(settings, 'PITHOS_BACKEND_XSEG_POOL_SIZE', 8) + +# The maximum interval (in seconds) for consequent backend object map checks +BACKEND_MAP_CHECK_INTERVAL = getattr(settings, + 'PITHOS_BACKEND_MAP_CHECK_INTERVAL', 5) + +# The archipelago mapfile prefix (it should not exceed 15 characters) +# WARNING: Once set it should not be changed +BACKEND_MAPFILE_PREFIX = getattr(settings, + 'PITHOS_BACKEND_MAPFILE_PREFIX', 'snf_file_') + +# The maximum allowed metadata items per domain for a Pithos+ resource +RESOURCE_MAX_METADATA = getattr(settings, 'PITHOS_RESOURCE_MAX_METADATA', 32) + +# The maximum allowed groups for a Pithos+ account. +ACC_MAX_GROUPS = getattr(settings, 'PITHOS_ACC_MAX_GROUPS', 32) + +# The maximum allowed group members per group. +ACC_MAX_GROUP_MEMBERS = getattr(settings, 'PITHOS_ACC_MAX_GROUP_MEMBERS', 32) diff --git a/snf-pithos-app/pithos/api/templatetags/get_type.py b/snf-pithos-app/pithos/api/templatetags/get_type.py index c16c2f1e765b79e069cc0abb1a062ebf44b339c9..4aae801abbc4c5fcef00ab06238c847c6c412079 100644 --- a/snf-pithos-app/pithos/api/templatetags/get_type.py +++ b/snf-pithos-app/pithos/api/templatetags/get_type.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django import template diff --git a/snf-pithos-app/pithos/api/test/__init__.py b/snf-pithos-app/pithos/api/test/__init__.py index cab5e3ad45abbec0f51f21f7a3b8fba8aab0dc33..87e29dddfa0d249ed9b6f8e06c0934de005ae6e9 100644 --- a/snf-pithos-app/pithos/api/test/__init__.py +++ b/snf-pithos-app/pithos/api/test/__init__.py @@ -1,38 +1,19 @@ #!/usr/bin/env python #coding=utf8 - -# Copyright 2011-2013 GRNET S.A. All rights reserved. -# -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# Copyright (C) 2010-2014 GRNET S.A. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. +# 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 General Public License for more details. # -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from urlparse import urlunsplit, urlsplit, urlparse from xml.dom import minidom @@ -47,13 +28,13 @@ from pithos.backends.migrate import initialize_db from synnefo.lib.services import get_service_path from synnefo.lib import join_urls -from synnefo.util import text from django.test import TestCase from django.test.client import Client, MULTIPART_CONTENT, FakePayload from django.test.simple import DjangoTestSuiteRunner from django.conf import settings from django.utils.http import urlencode +from django.utils.encoding import smart_unicode from django.db.backends.creation import TEST_DATABASE_PREFIX import django.utils.simplejson as json @@ -62,6 +43,7 @@ import django.utils.simplejson as json import sys import random import functools +import time pithos_test_settings = functools.partial(with_settings, pithos_settings) @@ -143,6 +125,11 @@ def filter_headers(headers, prefix): class PithosTestSuiteRunner(DjangoTestSuiteRunner): + def setup_test_environment(self, **kwargs): + pithos_settings.BACKEND_MAPFILE_PREFIX = \ + 'snf_test_pithos_app_%s_' % time.time() + super(PithosTestSuiteRunner, self).setup_test_environment(**kwargs) + def setup_databases(self, **kwargs): old_names, mirrors = super(PithosTestSuiteRunner, self).setup_databases(**kwargs) @@ -174,11 +161,11 @@ class PithosTestClient(Client): """ parsed = urlparse(path) r = { - 'CONTENT_TYPE': 'text/html; charset=utf-8', - 'PATH_INFO': self._get_path(parsed), - 'QUERY_STRING': urlencode(data, doseq=True) or parsed[4], + 'CONTENT_TYPE': 'text/html; charset=utf-8', + 'PATH_INFO': self._get_path(parsed), + 'QUERY_STRING': urlencode(data, doseq=True) or parsed[4], 'REQUEST_METHOD': 'COPY', - 'wsgi.input': FakePayload('') + 'wsgi.input': FakePayload('') } r.update(extra) @@ -194,11 +181,11 @@ class PithosTestClient(Client): """ parsed = urlparse(path) r = { - 'CONTENT_TYPE': 'text/html; charset=utf-8', - 'PATH_INFO': self._get_path(parsed), - 'QUERY_STRING': urlencode(data, doseq=True) or parsed[4], + 'CONTENT_TYPE': 'text/html; charset=utf-8', + 'PATH_INFO': self._get_path(parsed), + 'QUERY_STRING': urlencode(data, doseq=True) or parsed[4], 'REQUEST_METHOD': 'MOVE', - 'wsgi.input': FakePayload('') + 'wsgi.input': FakePayload('') } r.update(extra) @@ -230,7 +217,8 @@ class PithosAPITest(TestCase): mock_validate_token = self.create_patch( 'astakosclient.AstakosClient.validate_token') mock_validate_token.return_value = { - 'access': {'user': {'id': text.udec(self.user, 'utf8')}}} + 'access': { + 'user': {'id': smart_unicode(self.user, encoding='utf-8')}}} # patch astakosclient.AstakosClient.get_token mock_get_token = self.create_patch( @@ -368,8 +356,9 @@ class PithosAPITest(TestCase): def delete_account_meta(self, meta, user=None, verify_status=True): user = user or self.user - transform = lambda k: 'HTTP_%s' % k.replace('-', '_').upper() - kwargs = dict((transform(k), '') for k, v in meta.items()) + transform = lambda k: 'HTTP_X_ACCOUNT_META_%s' %\ + k.replace('-', '_').upper() + kwargs = dict((transform(k), '') for k in meta) url = join_urls(self.pithos_path, user) r = self.post('%s?update=' % url, user=user, **kwargs) if verify_status: @@ -382,7 +371,10 @@ class PithosAPITest(TestCase): def delete_account_groups(self, groups, user=None, verify_status=True): user = user or self.user url = join_urls(self.pithos_path, user) - r = self.post('%s?update=' % url, user=user, **groups) + transform = lambda k: 'HTTP_X_ACCOUNT_GROUP_%s' %\ + k.replace('-', '_').upper() + kwargs = dict((transform(k), '') for k in groups) + r = self.post('%s?update=' % url, user=user, **kwargs) if verify_status: self.assertEqual(r.status_code, 202) account_groups = self.get_account_groups() @@ -502,11 +494,15 @@ class PithosAPITest(TestCase): self.assertEqual(r.status_code, 204) return r - def create_container(self, cname=None, user=None, verify_status=True): + def create_container(self, cname=None, user=None, verify_status=True, + meta=None): + meta = meta or {} cname = cname or get_random_name() user = user or self.user url = join_urls(self.pithos_path, user, cname) - r = self.put(url, user=user, data='') + kwargs = dict( + ('HTTP_X_CONTAINER_META_%s' % k, str(v)) for k, v in meta.items()) + r = self.put(url, user=user, data='', **kwargs) if verify_status: self.assertTrue(r.status_code in (202, 201)) return cname, r diff --git a/snf-pithos-app/pithos/api/test/accounts.py b/snf-pithos-app/pithos/api/test/accounts.py index d22b1d231802aedde61da4d677a36fcf648a0633..d02139ddcaea7ab180bce5fe285bda5b82e33c94 100644 --- a/snf-pithos-app/pithos/api/test/accounts.py +++ b/snf-pithos-app/pithos/api/test/accounts.py @@ -1,41 +1,25 @@ #!/usr/bin/env python #coding=utf8 -# Copyright 2011-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from unittest import skipIf from pithos.api.test import (PithosAPITest, AssertMappingInvariant, - DATE_FORMATS) + DATE_FORMATS, pithos_settings) from synnefo.lib import join_urls @@ -44,6 +28,7 @@ import datetime import django.utils.simplejson as json + class AccountHead(PithosAPITest): def test_get_account_meta(self): cnames = ['apples', 'bananas', 'kiwis', 'oranges', 'pears'] @@ -169,7 +154,7 @@ class AccountGet(PithosAPITest): _time.sleep(2) self.create_container() - + url = join_urls(self.pithos_path, self.user) r = self.get('%s?until=%s' % (url, until)) self.assertEqual(r.status_code, 200) @@ -179,7 +164,6 @@ class AccountGet(PithosAPITest): self.assertEqual(containers, ['apples', 'bananas', 'kiwis', 'oranges', 'pears']) - r = self.get('%s?until=%s&format=json' % (url, until)) self.assertEqual(r.status_code, 200) try: @@ -187,7 +171,7 @@ class AccountGet(PithosAPITest): except: self.fail('json format expected') self.assertEqual([c['name'] for c in containers], - ['apples', 'bananas', 'kiwis', 'oranges', 'pears']) + ['apples', 'bananas', 'kiwis', 'oranges', 'pears']) def test_list_shared(self): # upload and publish object @@ -400,7 +384,7 @@ class AccountPost(PithosAPITest): with AssertMappingInvariant(self.get_account_groups): initial = self.get_account_meta() - meta = {'test': 'tost', 'ping': 'pong'} + meta = {'Test': 'tost', 'Ping': 'pong'} kwargs = dict(('HTTP_X_ACCOUNT_META_%s' % k, str(v)) for k, v in meta.items()) r = self.post('%s?update=' % url, **kwargs) @@ -408,8 +392,18 @@ class AccountPost(PithosAPITest): meta.update(initial) account_meta = self.get_account_meta() - (self.assertTrue(k in account_meta) for k in meta.keys()) - (self.assertEqual(account_meta[k], v) for k, v in meta.items()) + self.assertEqual(account_meta, meta) + + # test metadata limit + limit = pithos_settings.RESOURCE_MAX_METADATA + kwargs = dict(('HTTP_X_ACCOUNT_META_%s' % i, str(i)) + for i in range(limit - len(meta) + 1)) + r = self.post('%s?update=' % url, **kwargs) + self.assertEqual(r.status_code, 400) + + meta.update(initial) + account_meta = self.get_account_meta() + self.assertEqual(account_meta, meta) def test_reset_meta(self): url = join_urls(self.pithos_path, self.user) @@ -430,6 +424,13 @@ class AccountPost(PithosAPITest): (self.assertTrue(k not in account_meta) for k in meta.keys()) + # test metadata limit + limit = pithos_settings.RESOURCE_MAX_METADATA + kwargs = dict(('HTTP_X_ACCOUNT_META_%s' % i, str(i)) + for i in range(limit + 1)) + r = self.post('%s?update=' % url, **kwargs) + self.assertEqual(r.status_code, 400) + def test_delete_meta(self): url = join_urls(self.pithos_path, self.user) with AssertMappingInvariant(self.get_account_groups): @@ -445,14 +446,54 @@ class AccountPost(PithosAPITest): (self.assertTrue(k not in account_meta) for k in meta.keys()) + @skipIf(pithos_settings.BACKEND_DB_MODULE == + 'pithos.backends.lib.sqlite', + "This test is only meaningful for SQLAlchemy backend") + def test_set_account_groups_limit_exceed(self): + url = join_urls(self.pithos_path, self.user) + + # too long group name + headers = {'HTTP_X_ACCOUNT_GROUP_%s' % ('a' * 257): 'chazapis'} + r = self.post('%s?update=' % url, ** headers) + self.assertEqual(r.status_code, 400) + + # too long group member name + r = self.post('%s?update=' % url, + HTTP_X_ACCOUNT_GROUP_PITHOSDEV='%s' % 'a' * 257) + self.assertEqual(r.status_code, 400) + + # too long owner + other_user = 'a' * 257 + url = join_urls(self.pithos_path, other_user) + pithosdevs = ['verigak', 'gtsouk', 'chazapis'] + r = self.post('%s?update=' % url, + user=other_user, + HTTP_X_ACCOUNT_GROUP_PITHOSDEV=','.join(pithosdevs)) + self.assertEqual(r.status_code, 400) + def test_set_account_groups(self): url = join_urls(self.pithos_path, self.user) with AssertMappingInvariant(self.get_account_meta): - pithosdevs = ['verigak', 'gtsouk', 'chazapis'] + limit = pithos_settings.ACC_MAX_GROUPS + # too many groups + kwargs = dict(('HTTP_X_ACCOUNT_GROUP_%s' % i, unicode(i)) + for i in range(limit + 1)) + r = self.post('%s?update=' % url, **kwargs) + self.assertEqual(r.status_code, 400) + + pithosdevs = ['chazapis'] * 2 r = self.post('%s?update=' % url, HTTP_X_ACCOUNT_GROUP_PITHOSDEV=','.join(pithosdevs)) self.assertEqual(r.status_code, 202) + account_groups = self.get_account_groups() + self.assertTrue('Pithosdev' in self.get_account_groups()) + self.assertEqual(account_groups['Pithosdev'], + 'chazapis') + pithosdevs = ['verigak', 'gtsouk', 'chazapis'] + r = self.post('%s?update=' % url, + HTTP_X_ACCOUNT_GROUP_PITHOSDEV=','.join(pithosdevs)) + self.assertEqual(r.status_code, 202) account_groups = self.get_account_groups() self.assertTrue('Pithosdev' in self.get_account_groups()) self.assertEqual(account_groups['Pithosdev'], @@ -484,6 +525,11 @@ class AccountPost(PithosAPITest): self.assertEqual(account_groups['Clientsdev'], ','.join(sorted(clientdevs))) + long_list = [unicode(i) for i in xrange(33)] + r = self.post('%s?update=' % url, + HTTP_X_ACCOUNT_GROUP_TEST=','.join(long_list)) + self.assertEqual(r.status_code, 400) + def test_reset_account_groups(self): url = join_urls(self.pithos_path, self.user) with AssertMappingInvariant(self.get_account_meta): diff --git a/snf-pithos-app/pithos/api/test/containers.py b/snf-pithos-app/pithos/api/test/containers.py index 3bc605face7590cc55c20a84a52d73842c90a67b..a352dc614d1772d0f9ff38e7f350f8a75b08ed7f 100644 --- a/snf-pithos-app/pithos/api/test/containers.py +++ b/snf-pithos-app/pithos/api/test/containers.py @@ -1,38 +1,20 @@ #!/usr/bin/env python #coding=utf8 -# Copyright 2011-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from pithos.api.test import (PithosAPITest, DATE_FORMATS, o_names, pithos_settings, pithos_test_settings) @@ -139,7 +121,7 @@ class ContainerHead(PithosAPITest): len(objects)) self.assertTrue('X-Container-Bytes-Used' in container_info) self.assertEqual(int(container_info['X-Container-Bytes-Used']), - sum([len(data) for data in objects.values()])) + sum([len(dt) for dt in objects.values()])) self.assertTrue('X-Container-Object-Meta' in container_info) self.assertEqual( sorted(container_info['X-Container-Object-Meta'].split(',')), @@ -175,6 +157,8 @@ class ContainerGet(PithosAPITest): cname = self.cnames[0] self.upload_object(cname) + oname = self.objects[cname].keys()[-1] + self.delete_object(cname, oname) url = join_urls(self.pithos_path, self.user, cname) r = self.get('%s?until=%s' % (url, until)) @@ -418,12 +402,26 @@ class ContainerGet(PithosAPITest): container_url = join_urls(self.pithos_path, self.user, cname) onames = self.objects[cname].keys() + r = self.get('%s?shared=&public=&format=json' % container_url) + self.assertEqual(r.status_code, 200) + objects = json.loads(r.content) + self.assertEqual(len(objects), 0) + # publish an object public1 = onames.pop() url = join_urls(container_url, public1) r = self.post(url, content_type='', HTTP_X_OBJECT_PUBLIC='true') self.assertEqual(r.status_code, 202) + r = self.get('%s?shared=&public=&format=json' % container_url) + self.assertEqual(r.status_code, 200) + objects = json.loads(r.content) + self.assertEqual(len(objects), 1) + self.assertEqual(objects[0]['name'], public1) + self.assertEqual(objects[0]['bytes'], + len(self.objects[cname][public1])) + self.assertTrue('x_object_public' in objects[0]) + # publish another public2 = onames.pop() url = join_urls(container_url, public2) @@ -469,7 +467,7 @@ class ContainerGet(PithosAPITest): # create child object descendant = strnextling(public1) self.upload_object(cname, descendant) - # request public and assert child obejct is not listed + # request public and assert child object is not listed r = self.get('%s?shared=&public=' % container_url) objects = r.content.split('\n') if '' in objects: @@ -491,6 +489,29 @@ class ContainerGet(PithosAPITest): self.assertTrue(folder in objects) self.assertTrue(descendant not in objects) + # unpublish public1 + url = join_urls(container_url, public1) + r = self.post(url, content_type='', HTTP_X_OBJECT_PUBLIC='false') + self.assertEqual(r.status_code, 202) + + # unpublish public2 + url = join_urls(container_url, public2) + r = self.post(url, content_type='', HTTP_X_OBJECT_PUBLIC='false') + self.assertEqual(r.status_code, 202) + + # unpublish folder + url = join_urls(container_url, folder) + r = self.post(url, content_type='', HTTP_X_OBJECT_PUBLIC='false') + self.assertEqual(r.status_code, 202) + + r = self.get('%s?shared=&public=' % container_url) + self.assertEqual(r.status_code, 200) + objects = r.content.split('\n') + if '' in objects: + objects.remove('') + l = sorted([shared1, shared2]) + self.assertEqual(objects, l) + def test_list_objects(self): cname = self.cnames[0] url = join_urls(self.pithos_path, self.user, cname) @@ -687,6 +708,14 @@ class ContainerGet(PithosAPITest): objects.remove('') self.assertTrue(objects, sorted(onames[2:])) + # list objects that satisfy the in-existence criteria + r = self.get('%s?meta=!Stock' % container_url) + self.assertEqual(r.status_code, 200) + objects = r.content.split('\n') + if '' in objects: + objects.remove('') + self.assertTrue(objects, sorted(onames[:2])) + # test case insensitive existence criteria matching r = self.get('%s?meta=quality' % container_url) self.assertEqual(r.status_code, 200) @@ -862,6 +891,13 @@ class ContainerGet(PithosAPITest): class ContainerPut(PithosAPITest): def test_create(self): + # test metadata limit + limit = pithos_settings.RESOURCE_MAX_METADATA + too_many_meta = dict((i, i) for i in range(limit + 1)) + _, r = self.create_container('c1', verify_status=False, + meta=too_many_meta) + self.assertEqual(r.status_code, 400) + self.create_container('c1') self.list_containers() self.assertTrue('c1' in self.list_containers(format=None)) @@ -886,6 +922,13 @@ class ContainerPost(PithosAPITest): self.assertTrue(k in info) self.assertEqual(info[k], v) + # test metadata limit + limit = pithos_settings.RESOURCE_MAX_METADATA + too_many_meta = dict((i, i) for i in range(limit - len(meta) + 1)) + r = self.update_container_meta(cname, too_many_meta, + verify_status=False) + self.assertEqual(r.status_code, 400) + def test_quota(self): self.create_container('c1') @@ -906,6 +949,35 @@ class ContainerPost(PithosAPITest): r = self.upload_object('c1', length=1) + def test_upload_blocks(self): + cname = self.create_container()[0] + + url = join_urls(self.pithos_path, self.user, cname) + r = self.post(url, data=get_random_data()) + self.assertEqual(r.status_code, 202) + + url = join_urls(self.pithos_path, 'chuck', cname) + r = self.post(url, data=get_random_data()) + self.assertEqual(r.status_code, 403) + + # share object for read only + oname = self.upload_object(cname)[0] + url = join_urls(self.pithos_path, self.user, cname, oname) + self.post(url, content_type='', HTTP_CONTENT_RANGE='bytes */*', + HTTP_X_OBJECT_SHARING='read=*') + url = join_urls(self.pithos_path, 'chuck', cname) + r = self.post(url, data=get_random_data()) + self.assertEqual(r.status_code, 403) + + # share object for write only + oname = self.upload_object(cname)[0] + url = join_urls(self.pithos_path, self.user, cname, oname) + self.post(url, content_type='', HTTP_CONTENT_RANGE='bytes */*', + HTTP_X_OBJECT_SHARING='write=*') + url = join_urls(self.pithos_path, 'chuck', cname) + r = self.post(url, data=get_random_data()) + self.assertEqual(r.status_code, 403) + class ContainerDelete(PithosAPITest): def setUp(self): diff --git a/snf-pithos-app/pithos/api/test/listing.py b/snf-pithos-app/pithos/api/test/listing.py index 6c4ff2e01edd3ec633a329271b1a81d6144a193e..2c9764710d0e87ee9f8a0659e2fc3a6ee4246fbd 100644 --- a/snf-pithos-app/pithos/api/test/listing.py +++ b/snf-pithos-app/pithos/api/test/listing.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from pithos.api.test import PithosAPITest diff --git a/snf-pithos-app/pithos/api/test/objects.py b/snf-pithos-app/pithos/api/test/objects.py index 779f21fa6d4734d63c72402f373c01a07abdb9c1..5061155b7591e3704ac1ee65adba0ed333c9eb80 100644 --- a/snf-pithos-app/pithos/api/test/objects.py +++ b/snf-pithos-app/pithos/api/test/objects.py @@ -1,43 +1,26 @@ #!/usr/bin/env python #coding=utf8 -# Copyright 2011-2014 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from binascii import hexlify from collections import defaultdict from urllib import quote, unquote from functools import partial +from unittest import skipIf from pithos.api.test import (PithosAPITest, pithos_settings, AssertMappingInvariant, AssertUUidInvariant, @@ -142,7 +125,7 @@ class ObjectGet(PithosAPITest): m = p.match(content_disposition) self.assertTrue(m is not None) disposition_type = m.group(1) - self.assertEqual(disposition_type, 'attachment') + self.assertEqual(disposition_type, 'inline') filename = m.group(2) self.assertEqual(o, filename) @@ -174,7 +157,7 @@ class ObjectGet(PithosAPITest): m = p.match(content_disposition) self.assertTrue(m is not None) disposition_type = m.group(1) - self.assertEqual(disposition_type, 'attachment') + self.assertEqual(disposition_type, 'inline') filename = m.group(2) user_defined_disposition = content_disposition.replace( @@ -371,10 +354,6 @@ class ObjectGet(PithosAPITest): oname, odata = self.upload_object(cname, length=512)[:-1] url = join_urls(self.pithos_path, self.user, cname, oname) - # TODO - #r = self.get(url, HTTP_RANGE='bytes=50-10') - #self.assertEqual(r.status_code, 416) - offset = len(odata) + 1 r = self.get(url, HTTP_RANGE='bytes=0-%s' % offset) self.assertEqual(r.status_code, 416) @@ -721,6 +700,16 @@ class ObjectPut(PithosAPITest): self.assertEqual(r.status_code, 200) self.assertEqual(r.content, data) + def test_upload_limit_metadata(self): + cname = self.container + oname = get_random_name() + limit = pithos_settings.RESOURCE_MAX_METADATA + kwargs = dict(('HTTP_X_OBJECT_META_%s' % i, unicode(i)) for + i in range(limit + 1)) + url = join_urls(self.pithos_path, self.user, cname, oname) + r = self.put(url, content_type='application/octet-stream', **kwargs) + self.assertEqual(r.status_code, 400) + def test_maximum_upload_size_exceeds(self): cname = self.container oname = get_random_name() @@ -760,6 +749,24 @@ class ObjectPut(PithosAPITest): r = self.put(url, data=data, HTTP_ETAG='123') self.assertEqual(r.status_code, 422) + def test_upload_if_none_match(self): + cname = self.container + oname = get_random_name() + data = get_random_data() + url = join_urls(self.pithos_path, self.user, cname, oname) + r = self.put(url, data=data, HTTP_IF_NONE_MATCH='*') + self.assertEqual(r.status_code, 201) + + r = self.get(url) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.content, data) + self.assertTrue('ETag' in r) + etag = r['ETag'] + + url = join_urls(self.pithos_path, self.user, cname, oname) + r = self.put(url, data=data, HTTP_IF_NONE_MATCH=etag) + self.assertEqual(r.status_code, 412) + # def test_chunked_transfer(self): # cname = self.container # oname = '/%s' % get_random_name() @@ -819,6 +826,7 @@ class ObjectPut(PithosAPITest): def test_create_object_by_hashmap(self): cname = self.container block_size = pithos_settings.BACKEND_BLOCK_SIZE + block_hash = pithos_settings.BACKEND_HASH_ALGORITHM # upload an object oname, data = self.upload_object(cname, length=block_size + 1)[:-1] @@ -835,11 +843,35 @@ class ObjectPut(PithosAPITest): self.assertEqual(r.status_code, 200) self.assertEqual(r.content, data) + # inconsistent size; too small + d = json.loads(hashmap) + d['bytes'] = len(data) - 1 + r = self.put('%s?hashmap=' % url, data=json.dumps(d)) + self.assertEqual(r.status_code, 400) + + # inconsistent size; too big + d = json.loads(hashmap) + d['bytes'] = 2 * d['block_size'] + 1 + r = self.put('%s?hashmap=' % url, data=json.dumps(d)) + self.assertEqual(r.status_code, 400) + + # extremely big size + d = json.loads(hashmap) + d['bytes'] = 45390000000000000000000000000000000000 + r = self.put('%s?hashmap=' % url, data=json.dumps(d)) + self.assertEqual(r.status_code, 400) + + # negative size + d = json.loads(hashmap) + d['bytes'] = -1 + r = self.put('%s?hashmap=' % url, data=json.dumps(d)) + self.assertEqual(r.status_code, 400) + r = self.put('%s?hashmap=' % url, data='not json') self.assertEqual(r.status_code, 400) - d = {"block_hash": "sha1", - "block_size": TEST_BLOCK_SIZE} + d = {"block_hash": block_hash, + "block_size": block_size} hashmap = json.dumps(d) r = self.put('%s?hashmap=' % url, data=hashmap) self.assertEqual(r.status_code, 400) @@ -852,12 +884,12 @@ class ObjectPut(PithosAPITest): r = self.put('%s?hashmap=' % url, data=hashmap) self.assertEqual(r.status_code, 400) - length = random.randint(TEST_BLOCK_SIZE, 2 * TEST_BLOCK_SIZE) - data = get_random_data(length=length) - hashes = HashMap(TEST_BLOCK_SIZE, TEST_HASH_ALGORITHM) - hashes.load(data) + length = (block_size - len(data) % block_size) + more_data = ''.join([data, get_random_data(length=length)]) + hashes = HashMap(block_size, block_hash) + hashes.load(more_data) hexlified = [hexlify(h) for h in hashes] - d.update({"hashes": hexlified, "bytes": len(data)}) + d.update({"hashes": hexlified, "bytes": len(more_data)}) hashmap = json.dumps(d) r = self.put('%s?hashmap=' % url, data=hashmap) self.assertEqual(r.status_code, 409) @@ -866,7 +898,7 @@ class ObjectPut(PithosAPITest): except: self.fail("shouldn't happen") else: - self.assertEqual(sorted(missing), sorted(hexlified)) + self.assertEqual(missing, hexlified[-1:]) r = self.get('%s?hashmap=&format=xml' % url) oname = get_random_name() @@ -931,6 +963,19 @@ class ObjectPutCopy(PithosAPITest): self.assertTrue('X-Object-Hash' in r) self.assertEqual(r['X-Object-Hash'], self.etag) + def test_copy_limit_metadata(self): + with AssertMappingInvariant(self.get_object_info, self.container, + self.object): + # copy object + oname = get_random_name() + url = join_urls(self.pithos_path, self.user, self.container, oname) + limit = pithos_settings.RESOURCE_MAX_METADATA + kwargs = dict(('HTTP_X_OBJECT_META_%s' % i, unicode(i)) for + i in range(limit + 1)) + r = self.put(url, data='', HTTP_X_COPY_FROM='/%s/%s' % ( + self.container, self.object), **kwargs) + self.assertEqual(r.status_code, 400) + def test_copy_from_different_container(self): cname = 'c2' self.create_container(cname) @@ -1146,6 +1191,19 @@ class ObjectPutMove(PithosAPITest): r = self.head(url) self.assertEqual(r.status_code, 404) + def test_move_limit_metadata(self): + with AssertMappingInvariant(self.get_object_info, self.container, + self.object): + # copy object + oname = get_random_name() + url = join_urls(self.pithos_path, self.user, self.container, oname) + limit = pithos_settings.RESOURCE_MAX_METADATA + kwargs = dict(('HTTP_X_OBJECT_META_%s' % i, unicode(i)) + for i in range(limit + 1)) + r = self.put(url, data='', HTTP_X_MOVE_FROM='/%s/%s' % ( + self.container, self.object), **kwargs) + self.assertEqual(r.status_code, 400) + @pithos_test_settings(API_LIST_LIMIT=10) def test_move_dir(self): folder = self.create_folder(self.container)[0] @@ -1368,6 +1426,21 @@ class ObjectCopy(PithosAPITest): self.assertTrue('X-Object-Hash' in r) self.assertEqual(r['X-Object-Hash'], self.etag) + def test_copy_limit_metadata(self): + with AssertMappingInvariant(self.get_object_info, self.container, + self.object): + oname = get_random_name() + # copy object + url = join_urls(self.pithos_path, self.user, self.container, + self.object) + limit = pithos_settings.RESOURCE_MAX_METADATA + kwargs = dict(('HTTP_X_OBJECT_META_%s' % i, unicode(i)) + for i in range(limit + 1)) + r = self.copy(url, HTTP_DESTINATION='/%s/%s' % (self.container, + oname), + **kwargs) + self.assertEqual(r.status_code, 400) + @pithos_test_settings(API_LIST_LIMIT=10) def test_copy_dir_contents(self): folder = self.create_folder(self.container)[0] @@ -1439,7 +1512,7 @@ class ObjectCopy(PithosAPITest): # share object for write with user url = join_urls(self.pithos_path, 'alice', cname, folder) - r = self.post(url, user='alice', content_type='', + r = self.post(url, user='alice', content_type='', HTTP_CONTENT_RANGE='bytes */*', HTTP_X_OBJECT_SHARING='write=%s' % self.user) self.assertEqual(r.status_code, 202) @@ -1523,6 +1596,21 @@ class ObjectMove(PithosAPITest): r = self.head(url) self.assertEqual(r.status_code, 404) + def test_move_limit_metadata(self): + with AssertMappingInvariant(self.get_object_info, self.container, + self.object): + oname = get_random_name() + # copy object + url = join_urls(self.pithos_path, self.user, self.container, + self.object) + limit = pithos_settings.RESOURCE_MAX_METADATA + kwargs = dict(('HTTP_X_OBJECT_META_%s' % i, unicode(i)) + for i in range(limit + 1)) + r = self.move(url, HTTP_DESTINATION='/%s/%s' % (self.container, + oname), + **kwargs) + self.assertEqual(r.status_code, 400) + @pithos_test_settings(API_LIST_LIMIT=10) def test_move_dir_contents(self): folder = self.create_folder(self.container)[0] @@ -1636,7 +1724,7 @@ class ObjectMove(PithosAPITest): # share object for write with user url = join_urls(self.pithos_path, 'alice', cname, folder) - r = self.post(url, user='alice', content_type='', + r = self.post(url, user='alice', content_type='', HTTP_CONTENT_RANGE='bytes */*', HTTP_X_OBJECT_SHARING='write=%s' % self.user) self.assertEqual(r.status_code, 202) @@ -1697,6 +1785,13 @@ class ObjectPost(PithosAPITest): r = self.post(url, content_type='', **kwargs) self.assertEqual(r.status_code, 400) + # test metadata limit + limit = pithos_settings.RESOURCE_MAX_METADATA + kwargs = dict(('HTTP_X_OBJECT_META_%s' % i, unicode(i)) + for i in range(limit - len(meta) + 1)) + r = self.post('%s?update=' % url, content_type='', **kwargs) + self.assertEqual(r.status_code, 400) + # # Check utf-8 meta # d = {'α' * (114 / 2): 'β' * (256 / 2)} # kwargs = dict(('HTTP_X_OBJECT_META_%s' % quote(k), quote(v)) for @@ -1754,7 +1849,7 @@ class ObjectPost(PithosAPITest): etag = md5_hash(updated_data) else: etag = merkle(updated_data) - #self.assertEqual(r['ETag'], etag) + self.assertEqual(r['ETag'], etag) # check modified object r = self.get(url) @@ -1788,7 +1883,7 @@ class ObjectPost(PithosAPITest): etag = md5_hash(updated_data) else: etag = merkle(updated_data) - #self.assertEqual(r['ETag'], etag) + self.assertEqual(r['ETag'], etag) # check modified object r = self.get(url) @@ -1801,7 +1896,7 @@ class ObjectPost(PithosAPITest): block_size = pithos_settings.BACKEND_BLOCK_SIZE oname, odata = self.upload_object( self.container, length=random.randint( - block_size + 1, 2 * block_size))[:2] + block_size + 2, 2 * block_size))[:2] length = len(odata) first_byte_pos = random.randint(1, block_size) @@ -1911,7 +2006,7 @@ class ObjectPost(PithosAPITest): dest_meta = self.get_object_info(self.container, dest) self.assertEqual(source_data, dest_data) - #self.assertEqual(source_meta['ETag'], dest_meta['ETag']) + self.assertEqual(source_meta['ETag'], dest_meta['ETag']) self.assertEqual(source_meta['X-Object-Hash'], dest_meta['X-Object-Hash']) self.assertTrue( @@ -1935,7 +2030,7 @@ class ObjectPost(PithosAPITest): r = self.put(url, data=initial_data) self.assertEqual(r.status_code, 201) - offset = random.randint(0, source_length - 1) + offset = random.randint(0, source_length - 2) upto = random.randint(offset, source_length - 1) r = self.post(url, HTTP_CONTENT_RANGE='bytes %s-%s/*' % (offset, upto), @@ -2094,19 +2189,22 @@ class ObjectPost(PithosAPITest): content = r.content self.assertEqual(content, d2 + d3[-1]) + @skipIf(pithos_settings.BACKEND_DB_MODULE == + 'pithos.backends.lib.sqlite', + "This test is only meaningful for SQLAlchemy backend") def test_update_invalid_permissions(self): url = join_urls(self.pithos_path, self.user, self.container, self.object) r = self.post(url, content_type='', HTTP_CONTENT_RANGE='bytes */*', - HTTP_X_OBJECT_SHARING='%s' % (257*'a')) + HTTP_X_OBJECT_SHARING='%s' % (257 * 'a')) self.assertEqual(r.status_code, 400) r = self.post(url, content_type='', HTTP_CONTENT_RANGE='bytes */*', - HTTP_X_OBJECT_SHARING='read=%s' % (257*'a')) + HTTP_X_OBJECT_SHARING='read=%s' % (257 * 'a')) self.assertEqual(r.status_code, 400) r = self.post(url, content_type='', HTTP_CONTENT_RANGE='bytes */*', - HTTP_X_OBJECT_SHARING='write=%s' % (257*'a')) + HTTP_X_OBJECT_SHARING='write=%s' % (257 * 'a')) self.assertEqual(r.status_code, 400) diff --git a/snf-pithos-app/pithos/api/test/permissions.py b/snf-pithos-app/pithos/api/test/permissions.py index 0cad9758315570f474a37b3f858c36f2530ac03d..bfe0177283566705460bfc0cfef65655914f6f5f 100644 --- a/snf-pithos-app/pithos/api/test/permissions.py +++ b/snf-pithos-app/pithos/api/test/permissions.py @@ -1,38 +1,20 @@ #!/usr/bin/env python #coding=utf8 -# Copyright 2011-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from pithos.api.test import PithosAPITest from pithos.api.test.util import get_random_data, get_random_name diff --git a/snf-pithos-app/pithos/api/test/public.py b/snf-pithos-app/pithos/api/test/public.py index c5ee375f2e18e3cb0718854dc293b20b74f403de..8c377e8354b3cc81ecb53db0919dfa73e23107b6 100644 --- a/snf-pithos-app/pithos/api/test/public.py +++ b/snf-pithos-app/pithos/api/test/public.py @@ -1,38 +1,19 @@ #!/usr/bin/env python #coding=utf8 - -# Copyright 2011-2014 GRNET S.A. All rights reserved. -# -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# Copyright (C) 2010-2014 GRNET S.A. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. +# 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 General Public License for more details. # -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import random import datetime @@ -102,7 +83,7 @@ class TestPublic(PithosAPITest): m = p.match(content_disposition) self.assertTrue(m is not None) disposition_type = m.group(1) - self.assertEqual(disposition_type, 'attachment') + self.assertEqual(disposition_type, 'inline') filename = m.group(2) self.assertEqual(oname, filename) @@ -138,7 +119,7 @@ class TestPublic(PithosAPITest): m = p.match(content_disposition) self.assertTrue(m is not None) disposition_type = m.group(1) - self.assertEqual(disposition_type, 'attachment') + self.assertEqual(disposition_type, 'inline') filename = m.group(2) self.assertEqual(oname, filename) diff --git a/snf-pithos-app/pithos/api/test/top_level.py b/snf-pithos-app/pithos/api/test/top_level.py index 2c4df764401d3a652eaf669a23cc5f5dc0244d4e..6f8075266b8992ac468ce80095b9710d60e33720 100644 --- a/snf-pithos-app/pithos/api/test/top_level.py +++ b/snf-pithos-app/pithos/api/test/top_level.py @@ -1,38 +1,20 @@ #!/usr/bin/env python #coding=utf8 -# Copyright 2011-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from pithos.api.test import PithosAPITest diff --git a/snf-pithos-app/pithos/api/test/unicode.py b/snf-pithos-app/pithos/api/test/unicode.py index 478ac58f89dc5f4b6943062a77bec369bae7ab24..57bab39b9e324d26f15497daef96d2f8a9bc4758 100644 --- a/snf-pithos-app/pithos/api/test/unicode.py +++ b/snf-pithos-app/pithos/api/test/unicode.py @@ -1,38 +1,20 @@ #!/usr/bin/env python #coding=utf8 -# Copyright 2011-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from pithos.api.test import PithosAPITest, TEST_BLOCK_SIZE from pithos.api.test.util import get_random_data @@ -363,6 +345,15 @@ class TestUnicode(PithosAPITest): self.assertTrue('γκÏουπ' in groups) self.assertEqual(groups['γκÏουπ'], 'chazapis,διογÎνης') + headers = {'HTTP_X_ACCOUNT_GROUP_γκÏουπ': 'διογÎνης'} + url = join_urls(self.pithos_path, self.user) + r = self.post('%s?update=' % url, **headers) + self.assertEqual(r.status_code, 202) + + groups = self.get_account_groups() + self.assertTrue('γκÏουπ' in groups) + self.assertEqual(groups['γκÏουπ'], 'διογÎνης') + # check read access self.create_container('φάκελος') odata = self.upload_object('φάκελος', 'ο1')[1] @@ -407,6 +398,40 @@ class TestUnicode(PithosAPITest): self.assertEqual(r.status_code, 200) self.assertEqual(r.content, odata + appended_data) + gname = ('a' * 256).capitalize() + headers = {'HTTP_X_ACCOUNT_GROUP_%s' % gname: 'β'} + url = join_urls(self.pithos_path, self.user) + r = self.post(url, **headers) + self.assertEqual(r.status_code, 202) + + groups = self.get_account_groups() + self.assertTrue(gname in groups) + self.assertEqual(groups[gname], 'β') + + gname = ('a' * 257).capitalize() + headers = {'HTTP_X_ACCOUNT_GROUP_%s' % gname: 'β'} + url = join_urls(self.pithos_path, self.user) + r = self.post(url, **headers) + self.assertEqual(r.status_code, 400) + + def test_group_delete(self): + # create a group + headers = {'HTTP_X_ACCOUNT_GROUP_γκÏουπ': 'chazapis,διογÎνης'} + url = join_urls(self.pithos_path, self.user) + r = self.post(url, **headers) + self.assertEqual(r.status_code, 202) + + groups = self.get_account_groups() + self.assertTrue('γκÏουπ' in groups) + self.assertEqual(groups['γκÏουπ'], 'chazapis,διογÎνης') + + headers = {'HTTP_X_ACCOUNT_GROUP_γκÏουπ': ''} + r = self.post('%s?update=' % url, **headers) + self.assertEqual(r.status_code, 202) + + groups = self.get_account_groups() + self.assertTrue('γκÏουπ' not in groups) + def test_manifestation(self): self.create_container('κουβάς') prefix = 'μÎÏη/' diff --git a/snf-pithos-app/pithos/api/test/util/__init__.py b/snf-pithos-app/pithos/api/test/util/__init__.py index 7bdb90b2963bfebef00bd9d2ecd1086ef008e8c5..6bd8c8714976d7c1fc092f1fa666c73439ec9662 100644 --- a/snf-pithos-app/pithos/api/test/util/__init__.py +++ b/snf-pithos-app/pithos/api/test/util/__init__.py @@ -1,48 +1,29 @@ #!/usr/bin/env python #coding=utf8 -# Copyright 2011-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import re import hashlib import random +import string from binascii import hexlify from StringIO import StringIO -from pithos.backends.random_word import get_random_word - from pithos.api import settings as pithos_settings @@ -91,7 +72,7 @@ def get_random_data(length=None): length = length or random.randint( pithos_settings.BACKEND_BLOCK_SIZE, 2 * pithos_settings.BACKEND_BLOCK_SIZE) - return get_random_word(length)[:length] + return "".join([random.choice(string.letters) for i in xrange(length)]) def get_random_name(length=8): diff --git a/snf-pithos-app/pithos/api/test/views.py b/snf-pithos-app/pithos/api/test/views.py index 76ce1a7f29a7829535ae1215b3514d5902ca2bb2..0a60ea6c294c882af08dbace1b2e027ac779ac86 100644 --- a/snf-pithos-app/pithos/api/test/views.py +++ b/snf-pithos-app/pithos/api/test/views.py @@ -1,38 +1,20 @@ #!/usr/bin/env python #coding=utf8 -# Copyright 2011-2014 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from pithos.api import settings as pithos_settings from pithos.api.test import PithosAPITest, DATE_FORMATS @@ -148,7 +130,7 @@ class ObjectGetView(PithosAPITest): m = p.match(content_disposition) self.assertTrue(m is not None) disposition_type = m.group(1) - self.assertEqual(disposition_type, 'attachment') + self.assertEqual(disposition_type, 'inline') filename = m.group(2) self.assertEqual(self.oname, filename) @@ -187,7 +169,7 @@ class ObjectGetView(PithosAPITest): m = p.match(content_disposition) self.assertTrue(m is not None) disposition_type = m.group(1) - self.assertEqual(disposition_type, 'attachment') + self.assertEqual(disposition_type, 'inline') filename = m.group(2) self.assertEqual(self.oname, filename) diff --git a/snf-pithos-app/pithos/api/tests.py b/snf-pithos-app/pithos/api/tests.py index 8e2b2fd3f46bbc327239129a3f23ecaa030ad7e5..c9d50aa62f523cc47658782777c892bb3e634816 100644 --- a/snf-pithos-app/pithos/api/tests.py +++ b/snf-pithos-app/pithos/api/tests.py @@ -1,35 +1,17 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # Import TestCases from pithos.api.test.accounts import * diff --git a/snf-pithos-app/pithos/api/urls.py b/snf-pithos-app/pithos/api/urls.py index c327f56393d560fae08104b8a6955b2670b114b6..4131f9334e6e180bc20f49e1ba2c51d89d6f3984 100644 --- a/snf-pithos-app/pithos/api/urls.py +++ b/snf-pithos-app/pithos/api/urls.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from functools import partial from django.conf.urls import include, patterns diff --git a/snf-pithos-app/pithos/api/util.py b/snf-pithos-app/pithos/api/util.py index ccb5d4bf493481c25e1fec48b45d42fb48d023cc..aa9ba75a43d929f9a2dd90d1e1f910d5d179bbf2 100644 --- a/snf-pithos-app/pithos/api/util.py +++ b/snf-pithos-app/pithos/api/util.py @@ -1,35 +1,17 @@ -# Copyright 2011-2014 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from functools import wraps from datetime import datetime @@ -52,8 +34,7 @@ from snf_django.lib import api from snf_django.lib.api import faults, utils from pithos.api.settings import (BACKEND_DB_MODULE, BACKEND_DB_CONNECTION, - BACKEND_BLOCK_MODULE, BACKEND_BLOCK_PATH, - BACKEND_BLOCK_UMASK, + BACKEND_BLOCK_MODULE, BACKEND_QUEUE_MODULE, BACKEND_QUEUE_HOSTS, BACKEND_QUEUE_EXCHANGE, ASTAKOSCLIENT_POOLSIZE, @@ -64,17 +45,22 @@ from pithos.api.settings import (BACKEND_DB_MODULE, BACKEND_DB_CONNECTION, BACKEND_VERSIONING, BACKEND_FREE_VERSIONING, BACKEND_POOL_ENABLED, BACKEND_POOL_SIZE, BACKEND_BLOCK_SIZE, BACKEND_HASH_ALGORITHM, - RADOS_POOL_BLOCKS, + BACKEND_ARCHIPELAGO_CONF, + BACKEND_XSEG_POOL_SIZE, + BACKEND_MAP_CHECK_INTERVAL, + BACKEND_MAPFILE_PREFIX, + RADOS_STORAGE, RADOS_POOL_BLOCKS, RADOS_POOL_MAPS, TRANSLATE_UUIDS, PUBLIC_URL_SECURITY, PUBLIC_URL_ALPHABET, BASE_HOST, UPDATE_MD5, VIEW_PREFIX, OAUTH2_CLIENT_CREDENTIALS, UNSAFE_DOMAIN, - BACKEND_STORAGE, RADOS_CEPH_CONF) + RESOURCE_MAX_METADATA, ACC_MAX_GROUPS, + ACC_MAX_GROUP_MEMBERS) -from pithos.api.resources import resources from pithos.backends import connect_backend from pithos.backends.base import (NotAllowedError, QuotaError, ItemNotExists, - VersionNotExists) + VersionNotExists, IllegalOperationError, + LimitExceeded, BrokenSnapshot) from synnefo.lib import join_urls @@ -153,7 +139,7 @@ def get_account_headers(request): n = k[16:].lower() if '-' in n or '_' in n: raise faults.BadRequest('Bad characters in group name') - groups[n] = v.replace(' ', '').split(',') + groups[n] = list(set(v.replace(' ', '').split(','))) while '' in groups[n]: groups[n].remove('') return meta, groups @@ -241,6 +227,7 @@ def put_object_headers(response, meta, restricted=False, token=None, response.override_serialization = True response['Content-Type'] = meta.get('type', 'application/octet-stream') response['Last-Modified'] = http_date(int(meta['modified'])) + response['Available'] = meta['available'] if not restricted: response['X-Object-Hash'] = meta['hash'] response['X-Object-UUID'] = meta['uuid'] @@ -271,7 +258,7 @@ def put_object_headers(response, meta, restricted=False, token=None, if user_defined and not valid_disposition_type: return if not valid_disposition_type: - disposition_type = 'attachment' + disposition_type = 'inline' response['Content-Disposition'] = smart_str('%s; filename="%s"' % ( disposition_type, meta['name']), strings_only=True) @@ -341,7 +328,8 @@ def retrieve_displaynames(token, uuids, return_dict=False, fail_silently=True): catalog = astakos.get_usernames(uuids) or {} missing = list(set(uuids) - set(catalog)) if missing and not fail_silently: - raise ItemNotExists('Unknown displaynames: %s' % ', '.join(missing)) + raise ItemNotExists('Unknown displaynames: %s' % + ', '.join(map(smart_str, missing))) return catalog if return_dict else [catalog.get(i) for i in uuids] @@ -366,7 +354,8 @@ def retrieve_uuids(token, displaynames, return_dict=False, fail_silently=True): catalog = astakos.get_uuids(displaynames) or {} missing = list(set(displaynames) - set(catalog)) if missing and not fail_silently: - raise ItemNotExists('Unknown uuids: %s' % ', '.join(missing)) + raise ItemNotExists('Unknown uuids: %s' % + ', '.join(map(smart_str, missing))) return catalog if return_dict else [catalog.get(i) for i in displaynames] @@ -457,9 +446,7 @@ def validate_modification_preconditions(request, meta): def validate_matching_preconditions(request, meta): """Check that the ETag conforms with the preconditions set.""" - etag = meta['hash'] if not UPDATE_MD5 else meta['checksum'] - if not etag: - etag = None + etag = meta.get('hash') if not UPDATE_MD5 else meta.get('checksum') if_match = request.META.get('HTTP_IF_MATCH') if if_match is not None: @@ -522,6 +509,7 @@ def copy_or_move_object(request, src_account, src_container, src_name, raise faults.BadRequest('Invalid sharing header') except QuotaError, e: raise faults.RequestEntityTooLarge('Quota error: %s' % e) + if public is not None: try: request.backend.update_object_public( @@ -765,8 +753,8 @@ def socket_read_iterator(request, length=0, blocksize=4096): try: chunk_length = int(chunk_length, 16) except Exception: + # TODO: Change to something more appropriate. raise faults.BadRequest('Bad chunk size') - # TODO: Change to something more appropriate. # Check if done. if chunk_length == 0: if len(data) > 0: @@ -840,13 +828,14 @@ class ObjectWrapper(object): in each entry of the range list. """ - def __init__(self, backend, ranges, sizes, hashmaps, boundary): + def __init__(self, backend, ranges, sizes, hashmaps, boundary, meta): self.backend = backend self.ranges = ranges self.sizes = sizes self.hashmaps = hashmaps self.boundary = boundary self.size = sum(self.sizes) + self.meta = meta self.file_index = 0 self.block_index = 0 @@ -961,7 +950,8 @@ def object_data_response(request, sizes, hashmaps, meta, public=False): boundary = uuid.uuid4().hex else: boundary = '' - wrapper = ObjectWrapper(request.backend, ranges, sizes, hashmaps, boundary) + wrapper = ObjectWrapper(request.backend, ranges, sizes, hashmaps, + boundary, meta) response = HttpResponse(wrapper, status=ret) put_object_headers( response, meta, restricted=public, @@ -982,14 +972,20 @@ def object_data_response(request, sizes, hashmaps, meta, public=False): return response -def put_object_block(request, hashmap, data, offset): +def put_object_block(request, hashmap, data, offset, is_snapshot): """Put one block of data at the given offset.""" bi = int(offset / request.backend.block_size) bo = offset % request.backend.block_size bl = min(len(data), request.backend.block_size - bo) if bi < len(hashmap): - hashmap[bi] = request.backend.update_block(hashmap[bi], data[:bl], bo) + try: + hashmap[bi] = request.backend.update_block(hashmap[bi], + data[:bl], + offset=bo, + is_snapshot=is_snapshot) + except IllegalOperationError, e: + raise faults.Forbidden(e[0]) else: hashmap.append(request.backend.put_block(('\x00' * bo) + data[:bl])) return bl # Return ammount of data written. @@ -1021,7 +1017,7 @@ def simple_list_response(request, l): from pithos.backends.util import PithosBackendPool -if BACKEND_STORAGE == 'rados': +if RADOS_STORAGE: BLOCK_PARAMS = {'mappool': RADOS_POOL_MAPS, 'blockpool': RADOS_POOL_BLOCKS, } else: @@ -1032,8 +1028,6 @@ BACKEND_KWARGS = dict( db_module=BACKEND_DB_MODULE, db_connection=BACKEND_DB_CONNECTION, block_module=BACKEND_BLOCK_MODULE, - block_path=BACKEND_BLOCK_PATH, - block_umask=BACKEND_BLOCK_UMASK, block_size=BACKEND_BLOCK_SIZE, hash_algorithm=BACKEND_HASH_ALGORITHM, queue_module=BACKEND_QUEUE_MODULE, @@ -1049,8 +1043,13 @@ BACKEND_KWARGS = dict( account_quota_policy=BACKEND_ACCOUNT_QUOTA, container_quota_policy=BACKEND_CONTAINER_QUOTA, container_versioning_policy=BACKEND_VERSIONING, - backend_storage=BACKEND_STORAGE, - rados_ceph_conf=RADOS_CEPH_CONF) + archipelago_conf_file=BACKEND_ARCHIPELAGO_CONF, + xseg_pool_size=BACKEND_XSEG_POOL_SIZE, + map_check_interval=BACKEND_MAP_CHECK_INTERVAL, + mapfile_prefix=BACKEND_MAPFILE_PREFIX, + resource_max_metadata=RESOURCE_MAX_METADATA, + acc_max_groups=ACC_MAX_GROUPS, + acc_max_group_members=ACC_MAX_GROUP_MEMBERS) _pithos_backend_pool = PithosBackendPool(size=BACKEND_POOL_SIZE, **BACKEND_KWARGS) @@ -1076,8 +1075,8 @@ def update_request_headers(request): v.decode('ascii') if '%' in k or '%' in v: del(request.META[k]) - request.META[unquote(k)] = smart_unicode(unquote( - v), strings_only=True) + request.META[smart_unicode(unquote(k), strings_only=True)] = \ + smart_unicode(unquote(v), strings_only=True) except UnicodeDecodeError: raise faults.BadRequest('Bad character in headers.') @@ -1126,6 +1125,10 @@ def api_method(http_method=None, token_required=True, user_required=True, success_status = True return response + except LimitExceeded, le: + raise faults.BadRequest(le.args[0]) + except BrokenSnapshot, bs: + raise faults.BadRequest(bs.args[0]) finally: # Always close PithosBackend connection if getattr(request, "backend", None) is not None: diff --git a/snf-pithos-app/pithos/api/views.py b/snf-pithos-app/pithos/api/views.py index 08e607eb873d7654fb52aad7e59eb3c2d2070e1c..33aa8474bdd71a1fbcfb99b312aa373917019820 100755 --- a/snf-pithos-app/pithos/api/views.py +++ b/snf-pithos-app/pithos/api/views.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.views.decorators.csrf import csrf_exempt diff --git a/snf-pithos-app/setup.py b/snf-pithos-app/setup.py index 1102a09a4587e1a6c9920e75ee57c9f3148103d5..428f67f0fbfc51c97fdd3a2b02b74f45b07aed67 100644 --- a/snf-pithos-app/setup.py +++ b/snf-pithos-app/setup.py @@ -1,35 +1,17 @@ -# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # import distribute_setup @@ -65,7 +47,7 @@ INSTALL_REQUIRES = [ 'astakosclient', 'snf-django-lib', 'snf-webproject', - 'snf-branding', + 'snf-branding' ] EXTRAS_REQUIRES = { @@ -171,7 +153,7 @@ def find_package_data( setup( name='snf-pithos-app', version=VERSION, - license='BSD', + license='GNU GPLv3', url='http://www.synnefo.org/', description=SHORT_DESCRIPTION, classifiers=CLASSIFIERS, diff --git a/snf-pithos-app/tools/pithos-sync-rados.sh b/snf-pithos-app/tools/pithos-sync-rados.sh index 8eb6d7b961201f4b745db7654f70e5e34a5d213e..5a29ca1c68b43096e651a3e561a23e9069f41335 100755 --- a/snf-pithos-app/tools/pithos-sync-rados.sh +++ b/snf-pithos-app/tools/pithos-sync-rados.sh @@ -1,37 +1,19 @@ #!/bin/bash - -# Copyright 2014 GRNET S.A. All rights reserved. -# -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# Copyright (C) 2010-2014 GRNET S.A. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. +# 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 General Public License for more details. # -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. ' +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + DRYRUN="n" INFO="n" diff --git a/snf-pithos-backend/MANIFEST.in b/snf-pithos-backend/MANIFEST.in index bbd482925dc833ff5d9c750d5855d5548091bf62..8963c167a8ec3c72f7f3f815c68be2b6086e7cf4 100644 --- a/snf-pithos-backend/MANIFEST.in +++ b/snf-pithos-backend/MANIFEST.in @@ -1,4 +1,4 @@ recursive-include pithos *.json *.html *.json *.xml *.txt recursive-include pithos/backends/lib/sqlalchemy/alembic * -include distribute_setup.py +include distribute_setup.py README.md diff --git a/snf-pithos-backend/README.md b/snf-pithos-backend/README.md new file mode 100644 index 0000000000000000000000000000000000000000..5c44828e8e095501955617020535d9fe287b7467 --- /dev/null +++ b/snf-pithos-backend/README.md @@ -0,0 +1,27 @@ +snf-pithos-backend +================== + +Overview +-------- + +This is Synnefo's snf-pithos-backend component. Please see the [official +Synnefo site](http://www.synnefo.org) for more information. + + +Copyright and license +===================== + +Copyright (C) 2010-2014 GRNET S.A. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. diff --git a/snf-pithos-backend/pithos/__init__.py b/snf-pithos-backend/pithos/__init__.py index c78fbe672324992f6c941a7828e4827928b270c9..5cda842903d0bfec117db1c3c5a43281d46eb551 100644 --- a/snf-pithos-backend/pithos/__init__.py +++ b/snf-pithos-backend/pithos/__init__.py @@ -1,35 +1,17 @@ -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # this is a namespace package try: diff --git a/snf-pithos-backend/pithos/backends/__init__.py b/snf-pithos-backend/pithos/backends/__init__.py index a272c52c4a15dfa053b0687f2e8c166b982589ff..f9b0348078370a51f26cd4c8fdf7bb5771612690 100644 --- a/snf-pithos-backend/pithos/backends/__init__.py +++ b/snf-pithos-backend/pithos/backends/__init__.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import warnings diff --git a/snf-pithos-backend/pithos/backends/base.py b/snf-pithos-backend/pithos/backends/base.py index 8746818cc96b2dad8102bf6cf25b6d3cadf0ddf7..f77891b7a0217b1452fb0106090f77178110fe47 100644 --- a/snf-pithos-backend/pithos/backends/base.py +++ b/snf-pithos-backend/pithos/backends/base.py @@ -1,46 +1,33 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # Default setting for new accounts. DEFAULT_ACCOUNT_QUOTA = 0 # No quota. DEFAULT_CONTAINER_QUOTA = 0 # No quota. DEFAULT_CONTAINER_VERSIONING = 'auto' +(MAP_ERROR, MAP_UNAVAILABLE, MAP_AVAILABLE) = range(-1, 2) class NotAllowedError(Exception): pass +class IllegalOperationError(NotAllowedError): + pass + + class QuotaError(Exception): pass @@ -73,6 +60,21 @@ class InvalidHash(TypeError): pass +class InconsistentContentSize(ValueError): + pass + + +class InvalidPolicy(ValueError): + pass + + +class LimitExceeded(Exception): + pass + + +class BrokenSnapshot(Exception): + pass + class BaseBackend(object): """Abstract backend class. @@ -114,8 +116,8 @@ class BaseBackend(object): """ return [] - def get_account_meta(self, user, account, domain, until=None, - include_user_defined=True, external_quota=None): + def get_account_meta(self, user, account, domain=None, until=None, + include_user_defined=True): """Return a dictionary with the account metadata for the domain. The keys returned are all user-defined, except: @@ -129,11 +131,10 @@ class BaseBackend(object): 'until_timestamp': Last modification until the timestamp provided - 'external_quota': The quota computed from external quota holder - mechanism - Raises: NotAllowedError: Operation not permitted + + ValueError: if domain is None and include_user_defined==True """ return {} @@ -150,6 +151,7 @@ class BaseBackend(object): Raises: NotAllowedError: Operation not permitted + LimitExceeded: if the metadata number exceeds the allowed limit. """ return @@ -168,6 +170,8 @@ class BaseBackend(object): NotAllowedError: Operation not permitted ValueError: Invalid data in groups + + LimitExceeded: if the group number exceeds the allowed limit. """ return @@ -200,7 +204,7 @@ class BaseBackend(object): Raises: NotAllowedError: Operation not permitted - ValueError: Invalid policy defined + InvalidPolicy: Invalid policy defined """ return @@ -244,7 +248,8 @@ class BaseBackend(object): """ return [] - def get_container_meta(self, user, account, container, domain, until=None, + def get_container_meta(self, user, account, container, domain=None, + until=None, include_user_defined=True): """Return a dictionary with the container metadata for the domain. @@ -263,6 +268,8 @@ class BaseBackend(object): NotAllowedError: Operation not permitted ItemNotExists: Container does not exist + + ValueError: if domain is None and include_user_defined==True """ return {} @@ -282,6 +289,8 @@ class BaseBackend(object): NotAllowedError: Operation not permitted ItemNotExists: Container does not exist + + LimitExceeded: if the metadata number exceeds the allowed limit. """ return @@ -309,7 +318,7 @@ class BaseBackend(object): ItemNotExists: Container does not exist - ValueError: Invalid policy defined + InvalidPolicy: Invalid policy defined """ return @@ -321,7 +330,7 @@ class BaseBackend(object): ContainerExists: Container already exists - ValueError: Invalid policy defined + InvalidPolicy: Invalid policy defined """ return @@ -413,7 +422,7 @@ class BaseBackend(object): """Return a mapping of object paths to public ids under a container.""" return {} - def get_object_meta(self, user, account, container, name, domain, + def get_object_meta(self, user, account, container, name, domain=None, version=None, include_user_defined=True): """Return a dictionary with the object metadata for the domain. @@ -446,6 +455,8 @@ class BaseBackend(object): ItemNotExists: Container/object does not exist VersionNotExists: Version does not exist + + ValueError: if domain is None and include_user_defined==True """ return {} @@ -464,6 +475,8 @@ class BaseBackend(object): NotAllowedError: Operation not permitted ItemNotExists: Container/object does not exist + + LimitExceeded: if the metadata number exceeds the allowed limit. """ return '' @@ -523,6 +536,45 @@ class BaseBackend(object): """ return + def register_object_map(self, user, account, container, name, size, type, + mapfile, checksum='', domain='pithos', meta=None, + replace_meta=False, permissions=None): + """Register an object mapfile without providing any data. + + Lock the container path, create a node pointing to the object path, + create a version pointing to the mapfile + and issue the size change in the quotaholder. + + :param user: the user account which performs the action + + :param account: the account under which the object resides + + :param container: the container under which the object resides + + :param name: the object name + + :param size: the object size + + :param type: the object mimetype + + :param mapfile: the mapfile pointing to the object data + + :param checkcum: the md5 checksum (optional) + + :param domain: the object domain + + :param meta: a dict with custom object metadata + + :param replace_meta: replace existing metadata or not + + :param permissions: a dict with the read and write object permissions + + :returns: the new object uuid + + :raises: ItemNotExists, NotAllowedError, QuotaError, LimitExceeded + """ + return + def get_object_hashmap(self, user, account, container, name, version=None): """Return the object's size and a list with partial hashes. @@ -557,6 +609,8 @@ class BaseBackend(object): ValueError: Invalid users/groups in permissions QuotaError: Account or container quota exceeded + + LimitExceeded: if the metadata number exceeds the allowed limit. """ return '' @@ -596,6 +650,8 @@ class BaseBackend(object): ValueError: Invalid users/groups in permissions QuotaError: Account or container quota exceeded + + LimitExceeded: if the metadata number exceeds the allowed limit. """ return '' @@ -684,7 +740,7 @@ class BaseBackend(object): """Store a block and return the hash.""" return 0 - def update_block(self, hash, data, offset=0): + def update_block(self, hash, data, offset=0, is_snapshot=False): """Update a known block and return the hash. Raises: diff --git a/snf-pithos-backend/pithos/backends/filter.py b/snf-pithos-backend/pithos/backends/filter.py index 76549f884a6d0bcc51597bc7aaefb053d12780b8..49a0617e43c14d19fc27b6bc04180cbd3e0e3401 100644 --- a/snf-pithos-backend/pithos/backends/filter.py +++ b/snf-pithos-backend/pithos/backends/filter.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import re diff --git a/snf-pithos-backend/pithos/backends/lib/hashfiler/__init__.py b/snf-pithos-backend/pithos/backends/lib/hashfiler/__init__.py index 8007c585c66a8a49b405726ebd367811132d86b0..6e166cadbe1846e9cb366eff5fd1521b682721be 100644 --- a/snf-pithos-backend/pithos/backends/lib/hashfiler/__init__.py +++ b/snf-pithos-backend/pithos/backends/lib/hashfiler/__init__.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from store import Store diff --git a/snf-pithos-backend/pithos/backends/lib/hashfiler/radosblocker.py b/snf-pithos-backend/pithos/backends/lib/hashfiler/archipelagoblocker.py similarity index 55% rename from snf-pithos-backend/pithos/backends/lib/hashfiler/radosblocker.py rename to snf-pithos-backend/pithos/backends/lib/hashfiler/archipelagoblocker.py index 831a9af1cd97c37d75a43732ef6b9d3348a09677..060e38a5507a01d7ac9d0bc828f000a0b6612de1 100644 --- a/snf-pithos-backend/pithos/backends/lib/hashfiler/radosblocker.py +++ b/snf-pithos-backend/pithos/backends/lib/hashfiler/archipelagoblocker.py @@ -1,67 +1,50 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from hashlib import new as newhasher from binascii import hexlify -from rados import * +import ConfigParser + +from context_archipelago import ArchipelagoObject, file_sync_read_chunks +from archipelago.common import ( + Request, + xseg_reply_info, + string_at, + ) + +from pithos.workers import ( + glue, + monkey, + ) -from context_object import RadosObject, file_sync_read_chunks +monkey.patch_Request() -class RadosBlocker(object): +class ArchipelagoBlocker(object): """Blocker. - Required constructor parameters: blocksize, blockpath, hashtype. + Required constructor parameters: blocksize, hashtype. """ blocksize = None blockpool = None hashtype = None - rados = None - rados_ctx = None - - @classmethod - def get_rados_ctx(cls, pool, conf): - if cls.rados_ctx is None: - cls.rados = Rados(conffile=conf) - cls.rados.connect() - cls.rados_ctx = cls.rados.open_ioctx(pool) - return cls.rados_ctx def __init__(self, **params): + cfg = ConfigParser.ConfigParser() + cfg.readfp(open(params['archipelago_cfile'])) blocksize = params['blocksize'] - blockpool = params['blockpool'] - rados_ceph_conf = params['rados_ceph_conf'] - hashtype = params['hashtype'] try: hasher = newhasher(hashtype) @@ -73,9 +56,8 @@ class RadosBlocker(object): emptyhash = hasher.digest() self.blocksize = blocksize - self.blockpool = blockpool - self.ceph_conf = rados_ceph_conf - self.ioctx = RadosBlocker.get_rados_ctx(self.blockpool, self.ceph_conf) + self.ioctx_pool = glue.WorkerGlue.ioctx_pool + self.dst_port = int(cfg.getint('mapperd', 'blockerb_port')) self.hashtype = hashtype self.hashlen = len(emptyhash) self.emptyhash = emptyhash @@ -83,16 +65,22 @@ class RadosBlocker(object): def _pad(self, block): return block + ('\x00' * (self.blocksize - len(block))) - def _get_rear_block(self, blkhash): + def _get_rear_block(self, blkhash, create=0): name = hexlify(blkhash) - return RadosObject(name, self.ioctx) + return ArchipelagoObject(name, self.ioctx_pool, self.dst_port, create) def _check_rear_block(self, blkhash): filename = hexlify(blkhash) - try: - self.ioctx.stat(filename) + ioctx = self.ioctx_pool.pool_get() + req = Request.get_info_request(ioctx, self.dst_port, filename) + req.submit() + req.wait() + ret = req.success() + req.put() + self.ioctx_pool.pool_put(ioctx) + if ret: return True - except ObjectNotFound: + else: return False def block_hash(self, data): @@ -125,7 +113,7 @@ class RadosBlocker(object): if h == self.emptyhash: append(self._pad('')) continue - with self._get_rear_block(h) as rbl: + with self._get_rear_block(h, 0) as rbl: if not rbl: break for block in rbl.sync_read_chunks(blocksize, 1, 0): @@ -136,6 +124,45 @@ class RadosBlocker(object): return blocks + def block_retr_archipelago(self, hashes): + """Retrieve blocks from storage by their hashes""" + blocks = [] + append = blocks.append + + ioctx = self.ioctx_pool.pool_get() + archip_emptyhash = hexlify(self.emptyhash) + + for h in hashes: + if h == archip_emptyhash: + append(self._pad('')) + continue + req = Request.get_info_request(ioctx, self.dst_port, h) + req.submit() + req.wait() + ret = req.success() + if ret: + info = req.get_data(_type=xseg_reply_info) + size = info.contents.size + req.put() + req_data = Request.get_read_request(ioctx, self.dst_port, h, + size=size) + req_data.submit() + req_data.wait() + ret_data = req_data.success() + if ret_data: + append(self._pad(string_at(req_data.get_data(), size))) + req_data.put() + else: + req_data.put() + self.ioctx_pool.pool_put(ioctx) + raise Exception("Cannot retrieve Archipelago data.") + else: + req.put() + self.ioctx_pool.pool_put(ioctx) + raise Exception("Bad block file.") + self.ioctx_pool.pool_put(ioctx) + return blocks + def block_stor(self, blocklist): """Store a bunch of blocks and return (hashes, missing). Hashes is a list of the hashes of the blocks, @@ -147,7 +174,7 @@ class RadosBlocker(object): missing = [i for i, h in enumerate(hashlist) if not self._check_rear_block(h)] for i in missing: - with self._get_rear_block(hashlist[i]) as rbl: + with self._get_rear_block(hashlist[i], 1) as rbl: rbl.sync_write(blocklist[i]) # XXX: verify? return hashlist, missing @@ -176,7 +203,7 @@ class RadosBlocker(object): h, a = self.block_stor((newblock,)) return h[0], 1 if a else 0 - def block_hash_file(self, radosobject): + def block_hash_file(self, archipelagoobject): """Return the list of hashes (hashes map) for the blocks in a buffered file. Helper method, does not affect store. @@ -185,12 +212,13 @@ class RadosBlocker(object): append = hashes.append block_hash = self.block_hash - for block in file_sync_read_chunks(radosobject, self.blocksize, 1, 0): + for block in file_sync_read_chunks(archipelagoobject, + self.blocksize, 1, 0): append(block_hash(block)) return hashes - def block_stor_file(self, radosobject): + def block_stor_file(self, archipelagoobject): """Read blocks from buffered file object and store them. Return: (bytes read, list of hashes, list of hashes that were missing) """ @@ -202,7 +230,7 @@ class RadosBlocker(object): sextend = storedlist.extend lastsize = 0 - for block in file_sync_read_chunks(radosobject, blocksize, 1, 0): + for block in file_sync_read_chunks(archipelagoobject, blocksize, 1, 0): hl, sl = block_stor((block,)) hextend(hl) sextend(sl) diff --git a/snf-pithos-backend/pithos/backends/lib/hashfiler/archipelagomapper.py b/snf-pithos-backend/pithos/backends/lib/hashfiler/archipelagomapper.py new file mode 100644 index 0000000000000000000000000000000000000000..f105ac698f9d90fae7e7c9996c7cafdc8780516c --- /dev/null +++ b/snf-pithos-backend/pithos/backends/lib/hashfiler/archipelagomapper.py @@ -0,0 +1,113 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import ctypes +import ConfigParser +import logging + +from archipelago.common import ( + Request, + xseg_reply_map, + xseg_reply_map_scatterlist, + string_at, + XF_ASSUMEV0, + XF_MAPFLAG_READONLY, + ) + +from pithos.workers import ( + glue, + monkey, + ) + +monkey.patch_Request() + +logger = logging.getLogger(__name__) + + +class ArchipelagoMapper(object): + """Mapper. + Required constructor parameters: namelen. + """ + + namelen = None + + def __init__(self, **params): + self.params = params + self.namelen = params['namelen'] + cfg = ConfigParser.ConfigParser() + cfg.readfp(open(params['archipelago_cfile'])) + self.ioctx_pool = glue.WorkerGlue.ioctx_pool + self.dst_port = int(cfg.getint('mapperd', 'blockerm_port')) + self.mapperd_port = int(cfg.getint('vlmcd', 'mapper_port')) + + def map_retr(self, maphash, size): + """Return as a list, part of the hashes map of an object + at the given block offset. + By default, return the whole hashes map. + """ + hashes = () + ioctx = self.ioctx_pool.pool_get() + req = Request.get_mapr_request(ioctx, self.mapperd_port, + maphash, offset=0, size=size) + flags = req.get_flags() + flags |= XF_ASSUMEV0 + req.set_flags(flags) + req.set_v0_size(size) + req.submit() + req.wait() + ret = req.success() + if ret: + data = req.get_data(xseg_reply_map) + Segsarray = xseg_reply_map_scatterlist * data.contents.cnt + segs = Segsarray.from_address(ctypes.addressof(data.contents.segs)) + hashes = [string_at(segs[idx].target, segs[idx].targetlen) + for idx in xrange(len(segs))] + req.put() + else: + req.put() + self.ioctx_pool.pool_put(ioctx) + raise Exception("Could not retrieve Archipelago mapfile.") + req = Request.get_close_request(ioctx, self.mapperd_port, + maphash) + req.submit() + req.wait() + ret = req.success() + if ret is False: + logger.warning("Could not close map %s" % maphash) + pass + req.put() + self.ioctx_pool.pool_put(ioctx) + return hashes + + def map_stor(self, maphash, hashes, size, block_size): + """Store hashes in the given hashes map.""" + objects = list() + for h in hashes: + objects.append({'name': h, 'flags': XF_MAPFLAG_READONLY}) + ioctx = self.ioctx_pool.pool_get() + req = Request.get_create_request(ioctx, self.mapperd_port, + maphash, + mapflags=XF_MAPFLAG_READONLY, + objects=objects, blocksize=block_size, + size=size) + req.submit() + req.wait() + ret = req.success() + if ret is False: + req.put() + self.ioctx_pool.pool_put(ioctx) + raise IOError("Could not write map %s" % maphash) + req.put() + self.ioctx_pool.pool_put(ioctx) diff --git a/snf-pithos-backend/pithos/backends/lib/hashfiler/blocker.py b/snf-pithos-backend/pithos/backends/lib/hashfiler/blocker.py index be8bd529d35d486dfc092f93174f1893a0731775..30c34142242d30720f805b2161afe6ff44ea070e 100644 --- a/snf-pithos-backend/pithos/backends/lib/hashfiler/blocker.py +++ b/snf-pithos-backend/pithos/backends/lib/hashfiler/blocker.py @@ -1,37 +1,19 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. -from store_helpers import get_blocker +from archipelagoblocker import ArchipelagoBlocker class Blocker(object): @@ -41,35 +23,28 @@ class Blocker(object): """ def __init__(self, **params): - fblocker, rblocker, hashlen, blocksize = get_blocker(**params) - self.fblocker = fblocker - self.rblocker = rblocker - self.hashlen = hashlen - self.blocksize = blocksize + self.archip_blocker = ArchipelagoBlocker(**params) + self.hashlen = self.archip_blocker.hashlen + self.blocksize = params['blocksize'] def block_hash(self, data): - """Hash a block of data.""" - if self.fblocker: - return self.fblocker.block_hash(data) - elif self.rblocker: - return self.rblocker.block_hash(data) + """Hash a block of data""" + return self.archip_blocker.block_hash(data) def block_ping(self, hashes): """Check hashes for existence and return those missing from block storage. """ - if self.rblocker: - return self.rblocker.block_ping(hashes) - elif self.fblocker: - return self.fblocker.block_ping(hashes) + return self.archip_blocker.block_ping(hashes) def block_retr(self, hashes): """Retrieve blocks from storage by their hashes.""" - if self.rblocker: - return self.rblocker.block_retr(hashes) - elif self.fblocker: - return self.fblocker.block_retr(hashes) + return self.archip_blocker.block_retr(hashes) + + def block_retr_archipelago(self, hashes): + """Retrieve blocks from storage by theri hashes.""" + return self.archip_blocker.block_retr_archipelago(hashes) def block_stor(self, blocklist): """Store a bunch of blocks and return (hashes, missing). @@ -78,10 +53,8 @@ class Blocker(object): which blocks were missing from the store. """ - if self.fblocker: - (hashes, missing) = self.fblocker.block_stor(blocklist) - elif self.rblocker: - (hashes, missing) = self.rblocker.block_stor(blocklist) + + (hashes, missing) = self.archip_blocker.block_stor(blocklist) return (hashes, missing) def block_delta(self, blkhash, offset, data): @@ -90,26 +63,16 @@ class Blocker(object): (the hash of the new block, if the block already existed) """ blocksize = self.blocksize - if self.fblocker: - (bhash, existed) = self.fblocker.block_delta(blkhash, offset, data) - elif self.rblocker: - (bhash, existed) = self.rblocker.block_delta(blkhash, offset, - data) - if not bhash: + archip_hash = None + archip_existed = True + (archip_hash, archip_existed) = \ + self.archip_blocker.block_delta(blkhash, offset, data) + + if not archip_hash: return None, None - if self.rblocker and not bhash: - block = self.rblocker.block_retr((blkhash,)) - if not block: - return None, None - block = block[0] - newblock = block[:offset] + data - if len(newblock) > blocksize: - newblock = newblock[:blocksize] - elif len(newblock) < blocksize: - newblock += block[len(newblock):] - bhash, existed = self.rblocker.block_stor((newblock,)) - elif self.fblocker and not bhash: - block = self.fblocker.block_retr((blkhash,)) + + if self.archip_blocker and not archip_hash: + block = self.archip_blocker.block_retr((blkhash,)) if not block: return None, None block = block[0] @@ -118,6 +81,6 @@ class Blocker(object): newblock = newblock[:blocksize] elif len(newblock) < blocksize: newblock += block[len(newblock):] - bhash, existed = self.fblocker.block_stor((newblock,)) + archip_hash, archip_existed = self.rblocker.block_stor((newblock,)) - return bhash, 1 if existed else 0 + return archip_hash, 1 if archip_existed else 0 diff --git a/snf-pithos-backend/pithos/backends/lib/hashfiler/context_archipelago.py b/snf-pithos-backend/pithos/backends/lib/hashfiler/context_archipelago.py new file mode 100644 index 0000000000000000000000000000000000000000..e1b2a476b2d1b84bc7abcd0965d1adb06af85e76 --- /dev/null +++ b/snf-pithos-backend/pithos/backends/lib/hashfiler/context_archipelago.py @@ -0,0 +1,169 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from os import SEEK_CUR, SEEK_SET +from archipelago.common import ( + Request, + string_at, + ) +from pithos.workers import monkey +monkey.patch_Request() + +_zeros = '' + + +def zeros(nr): + global _zeros + size = len(_zeros) + if nr == size: + return _zeros + + if nr > size: + _zeros += '\0' * (nr - size) + return _zeros + + if nr < size: + _zeros = _zeros[:nr] + return _zeros + + +def file_sync_write_chunks(archipelagoobject, chunksize, offset, + chunks, size=None): + """Write given chunks to the given buffered file object. + Writes never span across chunk boundaries. + If size is given stop after or pad until size bytes have been written. + """ + padding = 0 + cursize = chunksize * offset + archipelagoobject.seek(cursize) + for chunk in chunks: + if padding: + archipelagoobject.sync_write(buffer(zeros(chunksize), 0, padding)) + if size is not None and cursize + chunksize >= size: + chunk = chunk[:chunksize - (cursize - size)] + archipelagoobject.sync_write(chunk) + cursize += len(chunk) + break + archipelagoobject.sync_write(chunk) + padding = chunksize - len(chunk) + + padding = size - cursize if size is not None else 0 + if padding <= 0: + return + + q, r = divmod(padding, chunksize) + for x in xrange(q): + archipelagoobject.sync_write(zeros(chunksize)) + archipelagoobject.sync_write(buffer(zeros(chunksize), 0, r)) + + +def file_sync_read_chunks(archipelagoobject, chunksize, nr, offset=0): + """Read and yield groups of chunks from a buffered file object at offset. + Reads never span accros chunksize boundaries. + """ + archipelagoobject.seek(offset * chunksize) + while nr: + remains = chunksize + chunk = '' + while 1: + s = archipelagoobject.sync_read(remains) + if not s: + if chunk: + yield chunk + return + chunk += s + remains -= len(s) + if remains <= 0: + break + yield chunk + nr -= 1 + + +class ArchipelagoObject(object): + __slots__ = ("name", "ioctx_pool", "dst_port", "create", "offset") + + def __init__(self, name, ioctx_pool, dst_port=None, create=0): + self.name = name + self.ioctx_pool = ioctx_pool + self.create = create + self.dst_port = dst_port + self.offset = 0 + + def __enter__(self): + return self + + def __exit__(self, exc, arg, trace): + return False + + def seek(self, offset, whence=SEEK_SET): + if whence == SEEK_CUR: + offset += self.offset + self.offset = offset + return offset + + def tell(self): + return self.offset + + def truncate(self, size): + raise NotImplementedError("File truncation is not implemented yet \ + in archipelago") + + def sync_write(self, data): + ioctx = self.ioctx_pool.pool_get() + req = Request.get_write_request(ioctx, self.dst_port, self.name, + data=data, offset=self.offset, + datalen=len(data)) + req.submit() + req.wait() + ret = req.success() + req.put() + self.ioctx_pool.pool_put(ioctx) + if ret: + self.offset += len(data) + else: + raise IOError("archipelago: Write request error") + + def sync_write_chunks(self, chunksize, offset, chunks, size=None): + return file_sync_write_chunks(self, chunksize, offset, chunks, size) + + def sync_read(self, size): + read = Request.get_read_request + data = '' + datalen = 0 + dsize = size + while 1: + ioctx = self.ioctx_pool.pool_get() + req = read(ioctx, self.dst_port, + self.name, size=dsize - datalen, offset=self.offset) + req.submit() + req.wait() + ret = req.success() + if ret: + s = string_at(req.get_data(), dsize - datalen) + else: + s = None + req.put() + self.ioctx_pool.pool_put(ioctx) + if not s: + break + data += s + datalen += len(s) + self.offset += len(s) + if datalen >= size: + break + return data + + def sync_read_chunks(self, chunksize, nr, offset=0): + return file_sync_read_chunks(self, chunksize, nr, offset) diff --git a/snf-pithos-backend/pithos/backends/lib/hashfiler/context_file.py b/snf-pithos-backend/pithos/backends/lib/hashfiler/context_file.py deleted file mode 100644 index 7b29bd89a2e1e64a3da05630a44419861dd30178..0000000000000000000000000000000000000000 --- a/snf-pithos-backend/pithos/backends/lib/hashfiler/context_file.py +++ /dev/null @@ -1,197 +0,0 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. -# -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: -# -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. -# -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. - -from os import ( - SEEK_CUR, - SEEK_SET, - O_RDONLY, - O_WRONLY, - O_RDWR -) - -_zeros = '' - - -def zeros(nr): - global _zeros - size = len(_zeros) - if nr == size: - return _zeros - - if nr > size: - _zeros += '\0' * (nr - size) - return _zeros - - if nr < size: - _zeros = _zeros[:nr] - return _zeros - - -def file_sync_write_chunks(openfile, chunksize, offset, chunks, size=None): - """Write given chunks to the given buffered file object. - Writes never span across chunk boundaries. - If size is given stop after or pad until size bytes have been written. - """ - fwrite = openfile.write - seek = openfile.seek - padding = 0 - - try: - seek(offset * chunksize) - except IOError: - seek = None - for x in xrange(offset): - fwrite(zeros(chunksize)) - - cursize = offset * chunksize - - for chunk in chunks: - if padding: - if seek: - seek(padding - 1, SEEK_CUR) - fwrite("\x00") - else: - fwrite(buffer(zeros(chunksize), 0, padding)) - if size is not None and cursize + chunksize >= size: - chunk = chunk[:chunksize - (cursize - size)] - fwrite(chunk) - cursize += len(chunk) - break - fwrite(chunk) - padding = chunksize - len(chunk) - - padding = size - cursize if size is not None else 0 - if padding <= 0: - return - - q, r = divmod(padding, chunksize) - for x in xrange(q): - fwrite(zeros(chunksize)) - fwrite(buffer(zeros(chunksize), 0, r)) - - -def file_sync_read_chunks(openfile, chunksize, nr, offset=0): - """Read and yield groups of chunks from a buffered file object at offset. - Reads never span accros chunksize boundaries. - """ - fread = openfile.read - remains = offset * chunksize - seek = openfile.seek - try: - seek(remains) - except IOError: - seek = None - while 1: - s = fread(remains) - remains -= len(s) - if remains <= 0: - break - - while nr: - remains = chunksize - chunk = '' - while 1: - s = fread(remains) - if not s: - if chunk: - yield chunk - return - chunk += s - remains -= len(s) - if remains <= 0: - break - yield chunk - nr -= 1 - - -class ContextFile(object): - __slots__ = ("name", "fdesc", "oflag") - - def __init__(self, name, oflag): - self.name = name - self.fdesc = None - self.oflag = oflag - # self.dirty = 0 - - def __enter__(self): - name = self.name - if self.oflag == O_RDONLY: - fdesc = open(name, 'rb') - elif self.oflag == O_WRONLY: - fdesc = open(name, 'wb') - elif self.oflag == O_RDWR: - fdesc = open(name, 'wb+') - else: - raise Exception("Wrong file acccess mode.") - - self.fdesc = fdesc - return self - - def __exit__(self, exc, arg, trace): - fdesc = self.fdesc - if fdesc is not None: - # if self.dirty: - # fsync(fdesc.fileno()) - fdesc.close() - return False # propagate exceptions - - def seek(self, offset, whence=SEEK_SET): - return self.fdesc.seek(offset, whence) - - def tell(self): - return self.fdesc.tell() - - def truncate(self, size): - self.fdesc.truncate(size) - - def sync_write(self, data): - # self.dirty = 1 - self.fdesc.write(data) - - def sync_write_chunks(self, chunksize, offset, chunks, size=None): - # self.dirty = 1 - return file_sync_write_chunks(self.fdesc, chunksize, offset, chunks, - size) - - def sync_read(self, size): - read = self.fdesc.read - data = '' - while 1: - s = read(size) - if not s: - break - data += s - return data - - def sync_read_chunks(self, chunksize, nr, offset=0): - return file_sync_read_chunks(self.fdesc, chunksize, nr, offset) diff --git a/snf-pithos-backend/pithos/backends/lib/hashfiler/context_object.py b/snf-pithos-backend/pithos/backends/lib/hashfiler/context_object.py deleted file mode 100644 index 74b8fb77b9c49931898b9fe95bcf093ba5004621..0000000000000000000000000000000000000000 --- a/snf-pithos-backend/pithos/backends/lib/hashfiler/context_object.py +++ /dev/null @@ -1,161 +0,0 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. -# -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: -# -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. -# -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. - -from os import SEEK_CUR, SEEK_SET -from rados import ObjectNotFound - -_zeros = '' - - -def zeros(nr): - global _zeros - size = len(_zeros) - if nr == size: - return _zeros - - if nr > size: - _zeros += '\0' * (nr - size) - return _zeros - - if nr < size: - _zeros = _zeros[:nr] - return _zeros - - -def file_sync_write_chunks(radosobject, chunksize, offset, chunks, size=None): - """Write given chunks to the given buffered file object. - Writes never span across chunk boundaries. - If size is given stop after or pad until size bytes have been written. - """ - padding = 0 - cursize = chunksize * offset - radosobject.seek(cursize) - for chunk in chunks: - if padding: - radosobject.sync_write(buffer(zeros(chunksize), 0, padding)) - if size is not None and cursize + chunksize >= size: - chunk = chunk[:chunksize - (cursize - size)] - radosobject.sync_write(chunk) - cursize += len(chunk) - break - radosobject.sync_write(chunk) - padding = chunksize - len(chunk) - - padding = size - cursize if size is not None else 0 - if padding <= 0: - return - - q, r = divmod(padding, chunksize) - for x in xrange(q): - radosobject.sunc_write(zeros(chunksize)) - radosobject.sync_write(buffer(zeros(chunksize), 0, r)) - - -def file_sync_read_chunks(radosobject, chunksize, nr, offset=0): - """Read and yield groups of chunks from a buffered file object at offset. - Reads never span accros chunksize boundaries. - """ - radosobject.seek(offset * chunksize) - while nr: - remains = chunksize - chunk = '' - while 1: - s = radosobject.sync_read(remains) - if not s: - if chunk: - yield chunk - return - chunk += s - remains -= len(s) - if remains <= 0: - break - yield chunk - nr -= 1 - - -class RadosObject(object): - __slots__ = ("name", "ioctx", "offset") - - def __init__(self, name, ioctx): - self.name = name - self.ioctx = ioctx - self.offset = 0 - # self.dirty = 0 - - def __enter__(self): - return self - - def __exit__(self, exc, arg, trace): - return False - - def seek(self, offset, whence=SEEK_SET): - if whence == SEEK_CUR: - offset += self.offset - self.offset = offset - return offset - - def tell(self): - return self.offset - - def truncate(self, size): - self.ioctx.trunc(self.name, size) - - def sync_write(self, data): - # self.dirty = 1 - self.ioctx.write(self.name, data, self.offset) - self.offset += len(data) - - def sync_write_chunks(self, chunksize, offset, chunks, size=None): - # self.dirty = 1 - return file_sync_write_chunks(self, chunksize, offset, chunks, size) - - def sync_read(self, size): - read = self.ioctx.read - data = '' - datalen = 0 - while 1: - try: - s = read(self.name, size - datalen, self.offset) - except ObjectNotFound: - s = None - if not s: - break - data += s - datalen += len(s) - self.offset += len(s) - if datalen >= size: - break - return data - - def sync_read_chunks(self, chunksize, nr, offset=0): - return file_sync_read_chunks(self, chunksize, nr, offset) diff --git a/snf-pithos-backend/pithos/backends/lib/hashfiler/fileblocker.py b/snf-pithos-backend/pithos/backends/lib/hashfiler/fileblocker.py deleted file mode 100644 index 0e07d0f936f13e6c9aa0d2e202d328bf0a307dc6..0000000000000000000000000000000000000000 --- a/snf-pithos-backend/pithos/backends/lib/hashfiler/fileblocker.py +++ /dev/null @@ -1,216 +0,0 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. -# -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: -# -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. -# -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. - -from os import makedirs -from os.path import isdir, realpath, exists, join -from hashlib import new as newhasher -from binascii import hexlify - -from context_file import ContextFile, file_sync_read_chunks -from os import O_RDONLY, O_WRONLY - - -class FileBlocker(object): - """Blocker. - Required constructor parameters: blocksize, blockpath, hashtype. - """ - - blocksize = None - blockpath = None - hashtype = None - - def __init__(self, **params): - blocksize = params['blocksize'] - blockpath = params['blockpath'] - blockpath = realpath(blockpath) - if not isdir(blockpath): - if not exists(blockpath): - makedirs(blockpath) - else: - raise ValueError("Variable blockpath '%s' is not a directory" % - (blockpath,)) - - hashtype = params['hashtype'] - try: - hasher = newhasher(hashtype) - except ValueError: - msg = "Variable hashtype '%s' is not available from hashlib" - raise ValueError(msg % (hashtype,)) - - hasher.update("") - emptyhash = hasher.digest() - - self.blocksize = blocksize - self.blockpath = blockpath - self.hashtype = hashtype - self.hashlen = len(emptyhash) - self.emptyhash = emptyhash - - def _pad(self, block): - return block + ('\x00' * (self.blocksize - len(block))) - - def _read_rear_block(self, blkhash): - filename = hexlify(blkhash) - dir = join(self.blockpath, filename[0:2], filename[2:4], filename[4:6]) - name = join(dir, filename) - return ContextFile(name, O_RDONLY) - - def _write_rear_block(self, blkhash): - filename = hexlify(blkhash) - dir = join(self.blockpath, filename[0:2], filename[2:4], filename[4:6]) - if not exists(dir): - makedirs(dir) - name = join(dir, filename) - return ContextFile(name, O_WRONLY) - - def _check_rear_block(self, blkhash): - filename = hexlify(blkhash) - dir = join(self.blockpath, filename[0:2], filename[2:4], filename[4:6]) - name = join(dir, filename) - return exists(name) - - def block_hash(self, data): - """Hash a block of data""" - hasher = newhasher(self.hashtype) - hasher.update(data.rstrip('\x00')) - return hasher.digest() - - def block_ping(self, hashes): - """Check hashes for existence and - return those missing from block storage. - """ - notfound = [] - append = notfound.append - - for h in hashes: - if h not in notfound and not self._check_rear_block(h): - append(h) - - return notfound - - def block_retr(self, hashes): - """Retrieve blocks from storage by their hashes.""" - blocksize = self.blocksize - blocks = [] - append = blocks.append - block = None - - for h in hashes: - if h == self.emptyhash: - append(self._pad('')) - continue - with self._read_rear_block(h) as rbl: - if not rbl: - break - for block in rbl.sync_read_chunks(blocksize, 1, 0): - break # there should be just one block there - if not block: - break - append(self._pad(block)) - - return blocks - - def block_stor(self, blocklist): - """Store a bunch of blocks and return (hashes, missing). - Hashes is a list of the hashes of the blocks, - missing is a list of indices in that list indicating - which blocks were missing from the store. - """ - block_hash = self.block_hash - hashlist = [block_hash(b) for b in blocklist] - missing = [i for i, h in enumerate(hashlist) if not - self._check_rear_block(h)] - for i in missing: - with self._write_rear_block(hashlist[i]) as rbl: - rbl.sync_write(blocklist[i]) # XXX: verify? - - return hashlist, missing - - def block_delta(self, blkhash, offset, data): - """Construct and store a new block from a given block - and a data 'patch' applied at offset. Return: - (the hash of the new block, if the block already existed) - """ - - blocksize = self.blocksize - if offset >= blocksize or not data: - return None, None - - block = self.block_retr((blkhash,)) - if not block: - return None, None - - block = block[0] - newblock = block[:offset] + data - if len(newblock) > blocksize: - newblock = newblock[:blocksize] - elif len(newblock) < blocksize: - newblock += block[len(newblock):] - - h, a = self.block_stor((newblock,)) - return h[0], 1 if a else 0 - - def block_hash_file(self, openfile): - """Return the list of hashes (hashes map) - for the blocks in a buffered file. - Helper method, does not affect store. - """ - hashes = [] - append = hashes.append - block_hash = self.block_hash - - for block in file_sync_read_chunks(openfile, self.blocksize, 1, 0): - append(block_hash(block)) - - return hashes - - def block_stor_file(self, openfile): - """Read blocks from buffered file object and store them. Return: - (bytes read, list of hashes, list of hashes that were missing) - """ - blocksize = self.blocksize - block_stor = self.block_stor - hashlist = [] - hextend = hashlist.extend - storedlist = [] - sextend = storedlist.extend - lastsize = 0 - - for block in file_sync_read_chunks(openfile, blocksize, 1, 0): - hl, sl = block_stor((block,)) - hextend(hl) - sextend(sl) - lastsize = len(block) - - size = (len(hashlist) - 1) * blocksize + lastsize if hashlist else 0 - return size, hashlist, storedlist diff --git a/snf-pithos-backend/pithos/backends/lib/hashfiler/filemapper.py b/snf-pithos-backend/pithos/backends/lib/hashfiler/filemapper.py deleted file mode 100644 index 8cb5f6f746450115b15351cfdbb7d969a7a1ac24..0000000000000000000000000000000000000000 --- a/snf-pithos-backend/pithos/backends/lib/hashfiler/filemapper.py +++ /dev/null @@ -1,101 +0,0 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. -# -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: -# -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. -# -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. - -from os import makedirs -from os.path import isdir, realpath, exists, join -from binascii import hexlify - -from context_file import ContextFile -from os import O_RDONLY, O_WRONLY - - -class FileMapper(object): - """Mapper. - Required constructor parameters: mappath, namelen. - """ - - mappath = None - namelen = None - - def __init__(self, **params): - self.params = params - self.namelen = params['namelen'] - mappath = realpath(params['mappath']) - if not isdir(mappath): - if not exists(mappath): - makedirs(mappath) - else: - raise ValueError("Variable mappath '%s' is not a directory" % - (mappath,)) - self.mappath = mappath - - def _read_rear_map(self, maphash): - filename = hexlify(maphash) - dir = join(self.mappath, filename[0:2], filename[2:4], filename[4:6]) - name = join(dir, filename) - return ContextFile(name, O_RDONLY) - - def _write_rear_map(self, maphash): - filename = hexlify(maphash) - dir = join(self.mappath, filename[0:2], filename[2:4], filename[4:6]) - if not exists(dir): - makedirs(dir) - name = join(dir, filename) - return ContextFile(name, O_WRONLY) - - def _check_rear_map(self, maphash): - filename = hexlify(maphash) - dir = join(self.mappath, filename[0:2], filename[2:4], filename[4:6]) - name = join(dir, filename) - return exists(name) - - def map_retr(self, maphash, blkoff=0, nr=100000000000000): - """Return as a list, part of the hashes map of an object - at the given block offset. - By default, return the whole hashes map. - """ - namelen = self.namelen - hashes = () - - with self._read_rear_map(maphash) as rmap: - if rmap: - hashes = list(rmap.sync_read_chunks(namelen, nr, blkoff)) - return hashes - - def map_stor(self, maphash, hashes=(), blkoff=0): - """Store hashes in the given hashes map.""" - namelen = self.namelen - if self._check_rear_map(maphash): - return - with self._write_rear_map(maphash) as rmap: - rmap.sync_write_chunks(namelen, blkoff, hashes, None) diff --git a/snf-pithos-backend/pithos/backends/lib/hashfiler/mapper.py b/snf-pithos-backend/pithos/backends/lib/hashfiler/mapper.py index 513c30d4955e7b389ef2e54c6f954b42fcfa5e90..70b6e92d704f22af72f1c0f6969fe0bc050413f3 100644 --- a/snf-pithos-backend/pithos/backends/lib/hashfiler/mapper.py +++ b/snf-pithos-backend/pithos/backends/lib/hashfiler/mapper.py @@ -1,38 +1,19 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. -from filemapper import FileMapper -from store_helpers import get_mapper +from archipelagomapper import ArchipelagoMapper class Mapper(object): @@ -42,23 +23,15 @@ class Mapper(object): """ def __init__(self, **params): - fmap, rmap = get_mapper(**params) - self.rmap = rmap - self.fmap = fmap + self.archip_map = ArchipelagoMapper(**params) - def map_retr(self, maphash, blkoff=0, nr=100000000000000): + def map_retr(self, maphash, size): """Return as a list, part of the hashes map of an object at the given block offset. By default, return the whole hashes map. """ - if self.fmap: - return self.fmap.map_retr(maphash, blkoff, nr) - elif self.rmap: - return self.rmap.map_retr(maphash, blkoff, nr) + return self.archip_map.map_retr(maphash, size) - def map_stor(self, maphash, hashes=(), blkoff=0): + def map_stor(self, maphash, hashes, size, blocksize): """Store hashes in the given hashes map.""" - if self.rmap: - return self.rmap.map_stor(maphash, hashes, blkoff) - elif self.fmap: - return self.fmap.map_stor(maphash, hashes, blkoff) + self.archip_map.map_stor(maphash, hashes, size, blocksize) diff --git a/snf-pithos-backend/pithos/backends/lib/hashfiler/radosmapper.py b/snf-pithos-backend/pithos/backends/lib/hashfiler/radosmapper.py deleted file mode 100644 index 34884be6adb8f107394c1aae4d2805e4b9105d35..0000000000000000000000000000000000000000 --- a/snf-pithos-backend/pithos/backends/lib/hashfiler/radosmapper.py +++ /dev/null @@ -1,98 +0,0 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. -# -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: -# -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. -# -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. - -from binascii import hexlify - -from context_object import RadosObject -from rados import * - - -class RadosMapper(object): - """Mapper. - Required constructor parameters: mappath, namelen. - """ - - mappool = None - namelen = None - rados = None - rados_ctx = None - - @classmethod - def get_rados_ctx(cls, pool, conf): - if cls.rados_ctx is None: - cls.rados = Rados(conffile=conf) - cls.rados.connect() - cls.rados_ctx = cls.rados.open_ioctx(pool) - return cls.rados_ctx - - def __init__(self, **params): - self.params = params - self.namelen = params['namelen'] - mappool = params['mappool'] - self.ceph_conf = params['rados_ceph_conf'] - - self.mappool = mappool - self.ioctx = RadosMapper.get_rados_ctx(mappool, self.ceph_conf) - - def _get_rear_map(self, maphash): - name = hexlify(maphash) - return RadosObject(name, self.ioctx) - - def _check_rear_map(self, maphash): - name = hexlify(maphash) - try: - self.ioctx.stat(name) - return True - except ObjectNotFound: - return False - - def map_retr(self, maphash, blkoff=0, nr=100000000000000): - """Return as a list, part of the hashes map of an object - at the given block offset. - By default, return the whole hashes map. - """ - namelen = self.namelen - hashes = () - - with self._get_rear_map(maphash) as rmap: - if rmap: - hashes = list(rmap.sync_read_chunks(namelen, nr, blkoff)) - return hashes - - def map_stor(self, maphash, hashes=(), blkoff=0): - """Store hashes in the given hashes map.""" - namelen = self.namelen - if self._check_rear_map(maphash): - return - with self._get_rear_map(maphash) as rmap: - rmap.sync_write_chunks(namelen, blkoff, hashes, None) diff --git a/snf-pithos-backend/pithos/backends/lib/hashfiler/store.py b/snf-pithos-backend/pithos/backends/lib/hashfiler/store.py index 59dd77ec8b7ecdf000a5d931c16292dfc06d0530..49eedfbe1e33a8c9aa7e297f5e44af9d1f6fc54d 100644 --- a/snf-pithos-backend/pithos/backends/lib/hashfiler/store.py +++ b/snf-pithos-backend/pithos/backends/lib/hashfiler/store.py @@ -1,59 +1,46 @@ -# Copyright 2011-2014 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import os from blocker import Blocker from mapper import Mapper -from store_helpers import bootstrap_backend_storage class Store(object): """Store. Required constructor parameters: path, block_size, hash_algorithm, - umask, blockpool, mappool. + blockpool, mappool. """ def __init__(self, **params): - (pb, pm) = bootstrap_backend_storage(**params) + pb = {'blocksize': params['block_size'], + 'hashtype': params['hash_algorithm'], + 'archipelago_cfile': params['archipelago_cfile'], + } self.blocker = Blocker(**pb) + pm = {'namelen': self.blocker.hashlen, + 'archipelago_cfile': params['archipelago_cfile'], + } self.mapper = Mapper(**pm) - def map_get(self, name): - return self.mapper.map_retr(name) + def map_get(self, name, size): + return self.mapper.map_retr(name, size) - def map_put(self, name, map): - self.mapper.map_stor(name, map) + def map_put(self, name, map, size, block_size): + self.mapper.map_stor(name, map, size, block_size) def map_delete(self, name): pass @@ -64,6 +51,12 @@ class Store(object): return None return blocks[0] + def block_get_archipelago(self, hash): + blocks = self.blocker.block_retr_archipelago((hash,)) + if not blocks: + return None + return blocks[0] + def block_put(self, data): hashes, absent = self.blocker.block_stor((data,)) return hashes[0] diff --git a/snf-pithos-backend/pithos/backends/lib/hashfiler/store_helpers.py b/snf-pithos-backend/pithos/backends/lib/hashfiler/store_helpers.py deleted file mode 100644 index d95862d527bc52f56585363b87017de8d3380cf7..0000000000000000000000000000000000000000 --- a/snf-pithos-backend/pithos/backends/lib/hashfiler/store_helpers.py +++ /dev/null @@ -1,132 +0,0 @@ -# Copyright 2014 GRNET S.A. All rights reserved. -# -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: -# -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. -# -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. - -import os -from hashlib import new as newhasher - - -def bootstrap_backend_storage(**params): - umask = params['umask'] - path = params['path'] - storageType = params['backend_storage'] - if umask is not None: - os.umask(umask) - if storageType.lower() == 'nfs': - if path and not os.path.exists(path): - os.makedirs(path) - if not os.path.isdir(path): - raise RuntimeError("Cannot open path '%s'" % (path,)) - elif storageType.lower() == 'rados': - import rados - cluster = rados.Rados(conffile=params['rados_ceph_conf']) - cluster.connect() - if not cluster.pool_exists(params['blockpool']): - try: - cluster.create_pool(params['blockpool']) - except Exception as err: - err_msg = "Cannot create %s RADOS Pool" - raise RuntimeError(err_msg % params['blockpool']) - if not cluster.pool_exists(params['mappool']): - try: - cluster.create_pool(params['mappool']) - except Exception as err: - err_msg = "Cannot create %s RADOS Pool" - raise RuntimeError(err_msg % params['mappool']) - cluster.shutdown() - else: - raise RuntimeError("Wrong Pithos+ backend storage, '%s'" % storageType) - hashtype = params['hash_algorithm'] - try: - hasher = newhasher(hashtype) - except ValueError: - msg = "Variable hashtype '%s' is not available from hashlib" - raise ValueError(msg % (hashtype,)) - - hasher.update("") - emptyhash = hasher.digest() - - pb = {'blocksize': params['block_size'], - 'blockpath': os.path.join(path + '/blocks'), - 'hashtype': params['hash_algorithm'], - 'blockpool': params['blockpool'], - 'backend_storage': params['backend_storage'], - 'rados_ceph_conf': params['rados_ceph_conf']} - pm = {'mappath': os.path.join(path + '/maps'), - 'namelen': len(emptyhash), - 'mappool': params['mappool'], - 'backend_storage': params['backend_storage'], - 'rados_ceph_conf': params['rados_ceph_conf']} - - return (pb, pm) - - -def get_blocker(**params): - rblocker = None - fblocker = None - hashlen = None - blocksize = None - storageType = params['backend_storage'] - if storageType.lower() == 'rados': - if params['blockpool']: - from radosblocker import RadosBlocker - rblocker = RadosBlocker(**params) - hashlen = rblocker.hashlen - blocksize = params['blocksize'] - else: - raise RuntimeError("Undefined RADOS block pool") - elif storageType.lower() == 'nfs': - from fileblocker import FileBlocker - fblocker = FileBlocker(**params) - hashlen = fblocker.hashlen - blocksize = params['blocksize'] - else: - raise RuntimeError("Wrong Pithos+ backend storage, '%s'" % storageType) - return (fblocker, rblocker, hashlen, blocksize) - - -def get_mapper(**params): - rmap = None - fmap = None - storageType = params['backend_storage'] - if storageType.lower() == 'rados': - if params['mappool']: - from radosmapper import RadosMapper - rmap = RadosMapper(**params) - else: - raise RuntimeError("Undefined RADOS map pool") - elif storageType.lower() == 'nfs': - from filemapper import FileMapper - fmap = FileMapper(**params) - else: - raise RuntimeError("Wrong Pithos+ backend storage, '%s'" % storageType) - return (fmap, rmap) diff --git a/snf-pithos-backend/pithos/backends/lib/rabbitmq/__init__.py b/snf-pithos-backend/pithos/backends/lib/rabbitmq/__init__.py index 521945b65ac5046f7a61fd3c89debe889d803654..8d0eca413157340d7082fb7a9ecc5afc867df71a 100644 --- a/snf-pithos-backend/pithos/backends/lib/rabbitmq/__init__.py +++ b/snf-pithos-backend/pithos/backends/lib/rabbitmq/__init__.py @@ -1,35 +1,17 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from queue import Queue diff --git a/snf-pithos-backend/pithos/backends/lib/rabbitmq/queue.py b/snf-pithos-backend/pithos/backends/lib/rabbitmq/queue.py index 072f6ae00da657ea25d7c736c919260dea37194e..5a40acd9c8ce6a02fddbb7d9e3d65c4d68f60edc 100644 --- a/snf-pithos-backend/pithos/backends/lib/rabbitmq/queue.py +++ b/snf-pithos-backend/pithos/backends/lib/rabbitmq/queue.py @@ -1,35 +1,17 @@ -# Copyright 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import json from hashlib import sha1 diff --git a/snf-pithos-backend/pithos/backends/lib/sqlalchemy/__init__.py b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/__init__.py index 7289fd2abb24a9c13b1a339101413223390439b4..49c1f70e9e107a8f3cd8e6895e3e42623f5302b9 100644 --- a/snf-pithos-backend/pithos/backends/lib/sqlalchemy/__init__.py +++ b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/__init__.py @@ -1,45 +1,24 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from dbwrapper import DBWrapper -from node import (Node, ROOTNODE, SERIAL, NODE, HASH, SIZE, TYPE, MTIME, MUSER, - UUID, CHECKSUM, CLUSTER, MATCH_PREFIX, MATCH_EXACT) +from node import (Node, ROOTNODE, MATCH_PREFIX, MATCH_EXACT) from permissions import Permissions, READ, WRITE from config import Config from quotaholder_serials import QuotaholderSerial __all__ = ["DBWrapper", - "Node", "ROOTNODE", "NODE", "SERIAL", "HASH", "SIZE", "TYPE", - "MTIME", "MUSER", "UUID", "CHECKSUM", "CLUSTER", "MATCH_PREFIX", - "MATCH_EXACT", "Permissions", "READ", "WRITE", "Config", - "QuotaholderSerial"] + "Node", "ROOTNODE", "MATCH_PREFIX", "MATCH_EXACT", "Permissions", + "READ", "WRITE", "Config", "QuotaholderSerial"] diff --git a/snf-pithos-backend/pithos/backends/lib/sqlalchemy/alembic/versions/1d40ec1ccc4f_check_fix_version_si.py b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/alembic/versions/1d40ec1ccc4f_check_fix_version_si.py new file mode 100644 index 0000000000000000000000000000000000000000..3360874162f1c8051dc62d3dcb16be846fb3b22e --- /dev/null +++ b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/alembic/versions/1d40ec1ccc4f_check_fix_version_si.py @@ -0,0 +1,36 @@ +"""Check/fix version size + +Revision ID: 1d40ec1ccc4f +Revises: f05c4de8cd7 +Create Date: 2014-08-04 12:22:59.710166 + +""" + +# revision identifiers, used by Alembic. +revision = '1d40ec1ccc4f' +down_revision = 'f05c4de8cd7' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + v = sa.sql.table('versions', + sa.sql.column('serial', sa.Integer), + sa.sql.column('size', sa.Integer)) + + s = sa.select([v.c.serial], v.c.size < 0) + c = op.get_bind() + rp = c.execute(s) + serials = list(r.serial for r in rp.fetchall()) + + if serials: + print('Negative object sizes are found for the following serials: %s\n' + 'Their size will be set to 0.' % + ','.join([unicode(srl) for srl in serials])) + u = v.update().where(v.c.serial.in_(serials)).values({'size': 0}) + op.execute(u) + + +def downgrade(): + pass diff --git a/snf-pithos-backend/pithos/backends/lib/sqlalchemy/alembic/versions/2be04d9180dd_attributes_table_idx.py b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/alembic/versions/2be04d9180dd_attributes_table_idx.py new file mode 100644 index 0000000000000000000000000000000000000000..9b8e4e157f8f5300d6dfa3d662a9d88f57f1b778 --- /dev/null +++ b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/alembic/versions/2be04d9180dd_attributes_table_idx.py @@ -0,0 +1,48 @@ +"""attributes table idxs + +Revision ID: 2be04d9180dd +Revises: 1d40ec1ccc4f +Create Date: 2014-08-02 10:48:15.925018 + +""" + +# revision identifiers, used by Alembic. +revision = '2be04d9180dd' +down_revision = '1d40ec1ccc4f' + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.sql import text + + +def upgrade(): + op.create_index('idx_attributes_node', 'attributes',['node']) + op.create_index('idx_attributes_islatest_domain_plankton', 'attributes', + ['is_latest', 'domain'], + postgresql_where=text("attributes.is_latest=true and " + "attributes.domain='plankton'")) + op.create_index('idx_attributes_key_domain_plankton', 'attributes', + ['key', 'domain'], + postgresql_where=text("attributes.domain='plankton'")) + op.create_index('idx_attributes_key_domain_pithos', 'attributes', + ['key', 'domain'], + postgresql_where=text("attributes.domain='pithos'")) + op.create_index('idx_attributes_serial_domain_pithos', 'attributes', + ['serial', 'domain'], + postgresql_where=text("attributes.domain='pithos'")) + op.create_index('idx_attributes_serial_domain_plankton', 'attributes', + ['serial', 'domain'], + postgresql_where=text("attributes.domain='plankton'")) + +def downgrade(): + op.drop_index('idx_attributes_node', tablename='attributes') + op.drop_index('idx_attributes_islatest_domain_plankton', + tablename='attributes') + op.drop_index('idx_attributes_key_domain_plankton', + tablename='attributes') + op.drop_index('idx_attributes_key_domain_pithos', + tablename='attributes') + op.drop_index('idx_attributes_serial_domain_pithos', + tablename='attributes') + op.drop_index('idx_attributes_serial_domain_plankton', + tablename='attributes') diff --git a/snf-pithos-backend/pithos/backends/lib/sqlalchemy/alembic/versions/2efddde15abf_diefferentiate_hashm.py b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/alembic/versions/2efddde15abf_diefferentiate_hashm.py new file mode 100644 index 0000000000000000000000000000000000000000..e34e7155c9c704bda15717c1f67e1bdb1c61e664 --- /dev/null +++ b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/alembic/versions/2efddde15abf_diefferentiate_hashm.py @@ -0,0 +1,41 @@ +"""Differentiate hashmap from mapfile + +Revision ID: 2efddde15abf +Revises: e6edec1b499 +Create Date: 2014-06-11 10:46:04.116321 + +""" + +# revision identifiers, used by Alembic. +revision = '2efddde15abf' +down_revision = 'e6edec1b499' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.execute(sa.schema.CreateSequence(sa.schema.Sequence("mapfile_seq"))) + + op.add_column('versions', sa.Column('mapfile', sa.String(256))) + op.add_column('versions', sa.Column('is_snapshot', sa.Boolean, + nullable=False, default=False, + server_default='False')) + + v = sa.sql.table( + 'versions', + sa.sql.column('hash', sa.String), + sa.sql.column('mapfile', sa.String), + sa.sql.column('is_snapshot', sa.Boolean)) + + u = v.update().values({'mapfile': v.c.hash, + 'is_snapshot': sa.case([(v.c.hash.like('archip:%'), + True)], else_=False)}) + op.execute(u) + + +def downgrade(): + op.drop_column('versions', 'is_snapshot') + op.drop_column('versions', 'mapfile') + + op.execute(sa.schema.DropSequence(sa.schema.Sequence("mapfile_seq"))) diff --git a/snf-pithos-backend/pithos/backends/lib/sqlalchemy/alembic/versions/3eb839abac44_nodes_table_idx.py b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/alembic/versions/3eb839abac44_nodes_table_idx.py new file mode 100644 index 0000000000000000000000000000000000000000..336215af39eaab212063708e2fd72367510da143 --- /dev/null +++ b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/alembic/versions/3eb839abac44_nodes_table_idx.py @@ -0,0 +1,27 @@ +"""nodes table idx + +Revision ID: 3eb839abac44 +Revises: 2be04d9180dd +Create Date: 2014-08-02 11:54:08.902956 + +""" + +# revision identifiers, used by Alembic. +revision = '3eb839abac44' +down_revision = '2be04d9180dd' + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.sql import text + + +def upgrade(): + op.create_index('idx_nodes_parent0','nodes', ['parent'], + postgresql_where=text('nodes.parent=0')) + op.create_index('idx_nodes_parent0_path','nodes', ['parent', 'path'], + postgresql_where=text('nodes.parent=0')) + + +def downgrade(): + op.drop_index('idx_nodes_parent0', tablename='nodes') + op.drop_index('idx_nodes_parent0_path', tablename='nodes') diff --git a/snf-pithos-backend/pithos/backends/lib/sqlalchemy/alembic/versions/4451e165da19_set_container_quota_.py b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/alembic/versions/4451e165da19_set_container_quota_.py new file mode 100644 index 0000000000000000000000000000000000000000..1ba3207c0e8387c6740001e820b44077ce540392 --- /dev/null +++ b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/alembic/versions/4451e165da19_set_container_quota_.py @@ -0,0 +1,46 @@ +"""Set container quota source + +Revision ID: 4451e165da19 +Revises: 301fba21d9b8 +Create Date: 2013-09-27 13:36:27.477141 + +""" + +# revision identifiers, used by Alembic. +revision = '4451e165da19' +down_revision = '301fba21d9b8' + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.sql import table, column, select + +ROOTNODE = 0 + + +def upgrade(): + connection = op.get_bind() + + nodes = table('nodes', + column('path', sa.String(2048)), + column('node', sa.Integer), + column('parent', sa.Integer)) + n1 = nodes.alias('n1') + n2 = nodes.alias('n2') + policy = table('policy', + column('node', sa.Integer), + column('key', sa.String(128)), + column('value', sa.String(256))) + + s = select([n2.c.node, n1.c.path]) + s = s.where(n2.c.parent == n1.c.node) + s = s.where(n1.c.parent == ROOTNODE) + s = s.where(n1.c.node != ROOTNODE) + r = connection.execute(s) + rows = r.fetchall() + op.bulk_insert(policy, [{'node': node, + 'key': 'project', + 'value': path} for node, path in rows]) + + +def downgrade(): + pass diff --git a/snf-pithos-backend/pithos/backends/lib/sqlalchemy/alembic/versions/4a3e7cb388d9_add_versions_node_in.py b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/alembic/versions/4a3e7cb388d9_add_versions_node_in.py new file mode 100644 index 0000000000000000000000000000000000000000..41f4015b5e7514c7fc1fcc0f602a146c5b5b4466 --- /dev/null +++ b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/alembic/versions/4a3e7cb388d9_add_versions_node_in.py @@ -0,0 +1,22 @@ +"""Add versions node index + +Revision ID: 4a3e7cb388d9 +Revises: 2efddde15abf +Create Date: 2014-07-22 13:21:02.392998 + +""" + +# revision identifiers, used by Alembic. +revision = '4a3e7cb388d9' +down_revision = '2efddde15abf' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.create_index('idx_versions_node', 'versions', ['node']) + + +def downgrade(): + op.drop_index('idx_versions_node', tablename='versions') diff --git a/snf-pithos-backend/pithos/backends/lib/sqlalchemy/alembic/versions/5adc52055209_alter_versions_avail.py b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/alembic/versions/5adc52055209_alter_versions_avail.py new file mode 100644 index 0000000000000000000000000000000000000000..d2bde872f1da65657d8c65ec346aa62342e51c3b --- /dev/null +++ b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/alembic/versions/5adc52055209_alter_versions_avail.py @@ -0,0 +1,56 @@ +"""Alter versions available possible states + +Revision ID: 5adc52055209 +Revises: 7107eaecd8c +Create Date: 2014-09-12 18:42:27.307379 + +""" + +# revision identifiers, used by Alembic. +revision = '5adc52055209' +down_revision = '7107eaecd8c' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.add_column('versions', sa.Column('temp', sa.INTEGER)) + + v = sa.sql.table( + 'versions', + sa.sql.column('available', sa.Boolean), + sa.sql.column('temp', sa.Integer)) + + u = v.update().values({'temp': sa.case([(v.c.available == + sa.sql.expression.true(), 1)], + else_=0)}) + op.execute(u) + + op.drop_column('versions', 'available') + op.add_column('versions', sa.Column('available', sa.INTEGER)) + u = v.update().values({'available': v.c.temp}) + op.execute(u) + + op.drop_column('versions', 'temp') + + +def downgrade(): + op.add_column('versions', sa.Column('temp', sa.Boolean)) + + v = sa.sql.table( + 'versions', + sa.sql.column('available', sa.Boolean), + sa.sql.column('temp', sa.Integer)) + + u = v.update().values({'temp': sa.case([(v.c.available == 1, + sa.sql.expression.true())], + else_=False)}) + op.execute(u) + + op.drop_column('versions', 'available') + op.add_column('versions', sa.Column('available', sa.Boolean)) + u = v.update().values({'available': v.c.temp}) + op.execute(u) + + op.drop_column('versions', 'temp') diff --git a/snf-pithos-backend/pithos/backends/lib/sqlalchemy/alembic/versions/7107eaecd8c_versions_table_idx.py b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/alembic/versions/7107eaecd8c_versions_table_idx.py new file mode 100644 index 0000000000000000000000000000000000000000..cde98d20b2748771df4012f14500878105ac1de1 --- /dev/null +++ b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/alembic/versions/7107eaecd8c_versions_table_idx.py @@ -0,0 +1,44 @@ +"""versions table idx + +Revision ID: 7107eaecd8c +Revises: 3eb839abac44 +Create Date: 2014-08-02 11:44:37.072969 + +""" + +# revision identifiers, used by Alembic. +revision = '7107eaecd8c' +down_revision = '3eb839abac44' + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.sql import text + + +def upgrade(): + op.create_index('idx_versions_node_cluster0', 'versions', + ['node', 'cluster'], + postgresql_where=text("versions.cluster=0")) + op.create_index('idx_versions_node_cluster1', 'versions', + ['node', 'cluster'], + postgresql_where=text("versions.cluster=1")) + op.create_index('idx_versions_node_cluster2', 'versions', + ['node', 'cluster'], + postgresql_where=text("versions.cluster=2")) + op.create_index('idx_versions_serial_cluster0', 'versions', + ['serial', 'cluster'], + postgresql_where=text("versions.cluster=0")) + op.create_index('idx_versions_serial_cluster1', 'versions', + ['serial', 'cluster'], + postgresql_where=text("versions.cluster=1")) + op.create_index('idx_versions_serial_cluster2', 'versions', + ['serial', 'cluster'], + postgresql_where=text("versions.cluster=2")) + +def downgrade(): + op.drop_index('idx_versions_node_cluster0', tablename='versions') + op.drop_index('idx_versions_node_cluster1', tablename='versions') + op.drop_index('idx_versions_node_cluster2', tablename='versions') + op.drop_index('idx_versions_serial_cluster0', tablename='versions') + op.drop_index('idx_versions_serial_cluster1', tablename='versions') + op.drop_index('idx_versions_serial_cluster2', tablename='versions') diff --git a/snf-pithos-backend/pithos/backends/lib/sqlalchemy/alembic/versions/e6edec1b499_add_columns_for_snap.py b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/alembic/versions/e6edec1b499_add_columns_for_snap.py new file mode 100644 index 0000000000000000000000000000000000000000..db7a3c3c2743f60c45510c1a4cfc7e698f0c2a52 --- /dev/null +++ b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/alembic/versions/e6edec1b499_add_columns_for_snap.py @@ -0,0 +1,28 @@ +"""Add columns for snapshots + +Revision ID: e6edec1b499 +Revises: 4451e165da19 +Create Date: 2014-01-27 15:33:21.058484 + +""" + +# revision identifiers, used by Alembic. +revision = 'e6edec1b499' +down_revision = '4451e165da19' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.add_column('versions', + sa.Column('available', sa.Boolean, nullable=False, + server_default='true')) + op.add_column('versions', + sa.Column('map_check_timestamp', + sa.DECIMAL(precision=16, scale=6))) + + +def downgrade(): + op.drop_column('versions', 'available') + op.drop_column('versions', 'map_check_timestamp') diff --git a/snf-pithos-backend/pithos/backends/lib/sqlalchemy/alembic/versions/f05c4de8cd7_create_cte_index.py b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/alembic/versions/f05c4de8cd7_create_cte_index.py new file mode 100644 index 0000000000000000000000000000000000000000..87419a6fddf049283d27a5cf04126b41bd7793cd --- /dev/null +++ b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/alembic/versions/f05c4de8cd7_create_cte_index.py @@ -0,0 +1,26 @@ +"""create_cte_index + +Revision ID: f05c4de8cd7 +Revises: 4a3e7cb388d9 +Create Date: 2014-07-29 11:27:55.421353 + +""" + +# revision identifiers, used by Alembic. +revision = 'f05c4de8cd7' +down_revision = '4a3e7cb388d9' + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.sql import text + + +def upgrade(): + op.create_index('idx_versions_serial_cluster_n2', 'versions', + ['serial', 'cluster'], + postgresql_where=text("versions.cluster != 2")) + + +def downgrade(): + op.drop_index('idx_versions_serial_cluster_n2', tablename='versions') + pass diff --git a/snf-pithos-backend/pithos/backends/lib/sqlalchemy/config.py b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/config.py index 2ecad51cd9f69996abf5623311e45970bea3874a..18c4ae138c7b57303a374737a36f18d7c436a88d 100644 --- a/snf-pithos-backend/pithos/backends/lib/sqlalchemy/config.py +++ b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/config.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from sqlalchemy import Table, Column, String, MetaData from sqlalchemy.sql import select diff --git a/snf-pithos-backend/pithos/backends/lib/sqlalchemy/dbworker.py b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/dbworker.py index e62d79f6ea5590ea5aade8066947efcdb88addc7..3db924eecf666c6817c1e7f3463452fec1431e7d 100644 --- a/snf-pithos-backend/pithos/backends/lib/sqlalchemy/dbworker.py +++ b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/dbworker.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. ESCAPE_CHAR = '@' diff --git a/snf-pithos-backend/pithos/backends/lib/sqlalchemy/dbwrapper.py b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/dbwrapper.py index cf9e1ff66ac330f228e4bb5f32c0582768666867..1cd0e3b0d8bf830f83e2e5fd9d3f5ffdecc4bb41 100644 --- a/snf-pithos-backend/pithos/backends/lib/sqlalchemy/dbwrapper.py +++ b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/dbwrapper.py @@ -1,38 +1,19 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from sqlalchemy import create_engine -#from sqlalchemy.event import listen from sqlalchemy.pool import NullPool from sqlalchemy.interfaces import PoolListener diff --git a/snf-pithos-backend/pithos/backends/lib/sqlalchemy/groups.py b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/groups.py index 6214ebe6931c2e1c3825416c9eb797a141056bc7..7443a1b9ac1c7cf24855077a508be19346a2e0f0 100644 --- a/snf-pithos-backend/pithos/backends/lib/sqlalchemy/groups.py +++ b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/groups.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from collections import defaultdict from sqlalchemy import Table, Column, String, MetaData @@ -104,12 +86,19 @@ class Groups(DBWorker): r = self.conn.execute(s, owner=owner, name=group, member=member) r.close() - def group_addmany(self, owner, group, members): - """Add members to a group.""" - - #TODO: more efficient way to do it - for member in members: - self.group_add(owner, group, member) + def group_addmany(self, owner, groups): + """Add members to a group. + Receive groups as a mapping object. + """ + + values = list({'owner': owner, + 'name': k, + 'member': m} + for k, members in groups.iteritems() + for m in sorted(set(members)) if m) + if values: + ins = self.groups.insert() + self.conn.execute(ins, values) def group_remove(self, owner, group, member): """Remove a member from a group.""" diff --git a/snf-pithos-backend/pithos/backends/lib/sqlalchemy/node.py b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/node.py index 0d3e892a8a781104b15ef2938dabe48555bd89ff..c6a761e810c563618e1790f8ddace5f8c7e774cf 100644 --- a/snf-pithos-backend/pithos/backends/lib/sqlalchemy/node.py +++ b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/node.py @@ -1,57 +1,39 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from time import time from operator import itemgetter from itertools import groupby +from collections import defaultdict from sqlalchemy import (Table, Integer, BigInteger, DECIMAL, Boolean, Column, String, MetaData, ForeignKey) -from sqlalchemy.schema import Index -from sqlalchemy.sql import func, and_, or_, not_, select, bindparam, exists -from sqlalchemy.sql.expression import true -from sqlalchemy.exc import NoSuchTableError +from sqlalchemy.schema import Index, Sequence +from sqlalchemy.sql import (func, and_, or_, not_, select, bindparam, exists, + functions) +from sqlalchemy.sql.expression import true, literal, type_coerce +from sqlalchemy.exc import NoSuchTableError, IntegrityError from dbworker import DBWorker, ESCAPE_CHAR +from pithos.backends.base import MAP_AVAILABLE from pithos.backends.filter import parse_filters - +DEFAULT_DISKSPACE_RESOURCE = 'pithos.diskspace' ROOTNODE = 0 -(SERIAL, NODE, HASH, SIZE, TYPE, SOURCE, MTIME, MUSER, UUID, CHECKSUM, - CLUSTER) = range(11) - (MATCH_PREFIX, MATCH_EXACT) = range(2) inf = float('inf') @@ -92,20 +74,6 @@ def strprevling(prefix): s += unichr(c - 1) + unichr(0xffff) return s -_propnames = { - 'serial': 0, - 'node': 1, - 'hash': 2, - 'size': 3, - 'type': 4, - 'source': 5, - 'mtime': 6, - 'muser': 7, - 'uuid': 8, - 'checksum': 9, - 'cluster': 10, -} - def create_tables(engine): metadata = MetaData() @@ -124,6 +92,10 @@ def create_tables(engine): Index('idx_nodes_path', nodes.c.path, unique=True) Index('idx_nodes_parent', nodes.c.parent) Index('idx_latest_version', nodes.c.latest_version) + Index('idx_nodes_parent0', nodes.c.parent, + postgresql_where=nodes.c.parent == 0) + Index('idx_nodes_parent0_path', nodes.c.parent, nodes.c.path, + postgresql_where=nodes.c.parent == 0) #create policy table columns = [] @@ -166,9 +138,31 @@ def create_tables(engine): columns.append(Column('uuid', String(64), nullable=False, default='')) columns.append(Column('checksum', String(256), nullable=False, default='')) columns.append(Column('cluster', Integer, nullable=False, default=0)) + columns.append(Column('available', Integer, nullable=False, default=1)) + columns.append(Column('map_check_timestamp', DECIMAL(precision=16, + scale=6))) + columns.append(Column('mapfile', String(256))) + columns.append(Column('is_snapshot', Boolean, nullable=False, + default=False)) + versions = Table('versions', metadata, *columns, mysql_engine='InnoDB') Index('idx_versions_node_mtime', versions.c.node, versions.c.mtime) + Index('idx_versions_node', versions.c.node) Index('idx_versions_node_uuid', versions.c.uuid) + Index('idx_versions_serial_cluster_n2', versions.c.serial, + versions.c.cluster, postgresql_where=versions.c.cluster != 2) + Index('idx_versions_node_cluster0', versions.c.node, + versions.c.cluster, postgresql_where=versions.c.cluster == 0) + Index('idx_versions_node_cluster1', versions.c.node, + versions.c.cluster, postgresql_where=versions.c.cluster == 1) + Index('idx_versions_node_cluster2', versions.c.node, + versions.c.cluster, postgresql_where=versions.c.cluster == 2) + Index('idx_versions_serial_cluster0', versions.c.serial, + versions.c.cluster, postgresql_where=versions.c.cluster == 0) + Index('idx_versions_serial_cluster1', versions.c.serial, + versions.c.cluster, postgresql_where=versions.c.cluster == 1) + Index('idx_versions_serial_cluster2', versions.c.serial, + versions.c.cluster, postgresql_where=versions.c.cluster == 2) #create attributes table columns = [] @@ -185,9 +179,29 @@ def create_tables(engine): attributes = Table('attributes', metadata, *columns, mysql_engine='InnoDB') Index('idx_attributes_domain', attributes.c.domain) Index('idx_attributes_serial_node', attributes.c.serial, attributes.c.node) + Index('idx_attributes_node', attributes.c.node) + Index('idx_attributes_islatest_domain_plankton', attributes.c.is_latest, + attributes.c.domain, postgresql_where=and_( + attributes.c.is_latest == True, + attributes.c.domain == "plankton")) + Index('idx_attributes_key_domain_plankton', attributes.c.key, + attributes.c.domain, postgresql_where=\ + attributes.c.domain == "plankton") + Index('idx_attributes_key_domain_pithos', attributes.c.key, + attributes.c.domain, postgresql_where=\ + attributes.c.domain == "pithos") + Index('idx_attributes_serial_domain_pithos', attributes.c.serial, + attributes.c.domain, postgresql_where=\ + attributes.c.domain == "pithos") + Index('idx_attributes_serial_domain_plankton', attributes.c.serial, + attributes.c.domain, postgresql_where=\ + attributes.c.domain == "plankton") + + # TODO: handle backends not supporting sequences + mapfile_seq = Sequence('mapfile_seq', metadata=metadata) metadata.create_all(engine) - return metadata.sorted_tables + return metadata.sorted_tables + [mapfile_seq] class Node(DBWorker): @@ -199,6 +213,8 @@ class Node(DBWorker): # TODO: Provide an interface for included and excluded clusters. def __init__(self, **params): + self._props = params.pop('props') + self.mapfile_prefix = params.pop('mapfile_prefix', 'snf_file_') DBWorker.__init__(self, **params) try: metadata = MetaData(self.engine) @@ -207,6 +223,7 @@ class Node(DBWorker): self.statistics = Table('statistics', metadata, autoload=True) self.versions = Table('versions', metadata, autoload=True) self.attributes = Table('attributes', metadata, autoload=True) + self.mapfile_seq = Sequence('mapfile_seq', metadata) except NoSuchTableError: tables = create_tables(self.engine) map(lambda t: self.__setattr__(t.name, t), tables) @@ -230,12 +247,18 @@ class Node(DBWorker): """Create a new node from the given properties. Return the node identifier of the new node. """ - #TODO catch IntegrityError? + t = self.conn.begin_nested() # create savepoint s = self.nodes.insert().values(parent=parent, path=path) - r = self.conn.execute(s) - inserted_primary_key = r.inserted_primary_key[0] - r.close() - return inserted_primary_key + try: + r = self.conn.execute(s) + except IntegrityError: + t.rollback() + raise ValueError('Attempt to create a node which already exists.') + else: + t.commit() + inserted_primary_key = r.inserted_primary_key[0] + r.close() + return inserted_primary_key def node_lookup(self, path, for_update=False): """Lookup the current node of the given path. @@ -280,13 +303,30 @@ class Node(DBWorker): r.close() return l - def node_get_versions(self, node, keys=(), propnames=_propnames): + def node_get_parent_path(self, node): + """Return the node's parent path. + Return None if the node is not found. + """ + + n1 = self.nodes.alias('n1') + n2 = self.nodes.alias('n2') + + s = select([n2.c.path]) + s = s.where(n2.c.node == n1.c.parent) + s = s.where(n1.c.node == node) + r = self.conn.execute(s) + l = r.fetchone() + r.close() + return l[0] if l is not None else None + + def node_get_versions(self, node, keys=(), props=None): """Return the properties of all versions at node. If keys is empty, return all properties in the order (serial, node, hash, size, type, source, mtime, muser, uuid, - checksum, cluster). + checksum, cluster, available, map_check_timestamp). """ + props = props or self._props s = select([self.versions.c.serial, self.versions.c.node, self.versions.c.hash, @@ -297,7 +337,12 @@ class Node(DBWorker): self.versions.c.muser, self.versions.c.uuid, self.versions.c.checksum, - self.versions.c.cluster], self.versions.c.node == node) + self.versions.c.cluster, + self.versions.c.available, + self.versions.c.map_check_timestamp, + self.versions.c.mapfile, + self.versions.c.is_snapshot], + self.versions.c.node == node) s = s.order_by(self.versions.c.serial) r = self.conn.execute(s) rows = r.fetchall() @@ -308,7 +353,7 @@ class Node(DBWorker): if not keys: return rows - return [[p[propnames[k]] for k in keys if k in propnames] for + return [[p[props[k]] for k in keys if k in props] for p in rows] def node_count_children(self, node): @@ -485,11 +530,12 @@ class Node(DBWorker): r.close() return dict(rows) - def node_account_usage(self, account=None, cluster=0): - """Return usage for a specific account. + def node_account_usage(self, account=None, project=None, cluster=0): + """Return a dict of dicts with the project usage for a specific account Keyword arguments: - account -- (default None: list usage for all the accounts) + account -- (default None: list usage for all accounts) + project -- (default None: list usage for all projects) cluster -- list current, history or deleted usage (default 0: normal) """ @@ -497,20 +543,61 @@ class Node(DBWorker): n2 = self.nodes.alias('n2') n3 = self.nodes.alias('n3') - s = select([n3.c.path, func.sum(self.versions.c.size)]) + s = select([n3.c.path, self.policy.c.value, + func.sum(self.versions.c.size)]) + s = s.where(self.policy.c.key == 'project') + s = s.where(self.policy.c.node == n2.c.node) s = s.where(n1.c.node == self.versions.c.node) s = s.where(self.versions.c.cluster == cluster) s = s.where(n1.c.parent == n2.c.node) s = s.where(n2.c.parent == n3.c.node) s = s.where(n3.c.parent == 0) s = s.where(n3.c.node != 0) + s = s.group_by(n3.c.path, self.policy.c.value) if account: s = s.where(n3.c.path == account) - s = s.group_by(n3.c.path) + if project: + s = s.where(self.policy.c.value == project) r = self.conn.execute(s) - usage = r.fetchall() + rows = r.fetchall() r.close() - return dict(usage) + d = defaultdict(lambda: defaultdict(dict)) + for account, project, usage in rows: + d[account][project][DEFAULT_DISKSPACE_RESOURCE] = usage + return d + + def node_project_usage(self, project=None, cluster=0): + """Return a dict of dicts with the project usage for a specific account + + Keyword arguments: + project -- (default None: list usage for all projects) + cluster -- list current, history or deleted usage (default 0: normal) + """ + + n1 = self.nodes.alias('n1') + n2 = self.nodes.alias('n2') + n3 = self.nodes.alias('n3') + + s = select([self.policy.c.value, + func.sum(self.versions.c.size)]) + s = s.where(self.policy.c.key == 'project') + s = s.where(self.policy.c.node == n2.c.node) + s = s.where(n1.c.node == self.versions.c.node) + s = s.where(self.versions.c.cluster == cluster) + s = s.where(n1.c.parent == n2.c.node) + s = s.where(n2.c.parent == n3.c.node) + # s = s.where(n3.c.parent == 0) + # s = s.where(n3.c.node != 0) + s = s.group_by(self.policy.c.value) + if project: + s = s.where(self.policy.c.value == project) + r = self.conn.execute(s) + rows = r.fetchall() + r.close() + d = defaultdict(dict) + for project, usage in rows: + d[project][DEFAULT_DISKSPACE_RESOURCE] = usage + return d def policy_get(self, node): s = select([self.policy.c.key, self.policy.c.value], @@ -595,7 +682,7 @@ class Node(DBWorker): while True: if node == ROOTNODE: break - if recursion_depth and recursion_depth <= i: + if recursion_depth is not None and recursion_depth <= i: break props = self.node_get_properties(node) if props is None: @@ -629,7 +716,11 @@ class Node(DBWorker): self.versions.c.muser, self.versions.c.uuid, self.versions.c.checksum, - self.versions.c.cluster]) + self.versions.c.cluster, + self.versions.c.available, + self.versions.c.map_check_timestamp, + self.versions.c.mapfile, + self.versions.c.is_snapshot]) if before != inf: filtered = select([func.max(self.versions.c.serial)], self.versions.c.node == node) @@ -644,7 +735,7 @@ class Node(DBWorker): r.close() if not props: return None - mtime = props[MTIME] + mtime = props.mtime # First level, just under node (get population). v = self.versions.alias('v') @@ -676,36 +767,40 @@ class Node(DBWorker): # This is why the full path is stored. if before != inf: s = select([func.count(v.c.serial), - func.sum(v.c.size), - func.max(v.c.mtime)]) + func.sum(v.c.size), + func.max(v.c.mtime)]) c1 = select([func.max(self.versions.c.serial)], and_(self.versions.c.mtime < before, self.versions.c.node == v.c.node)) else: - inner_join = self.versions.join( - self.nodes, - onclause=self.versions.c.serial == self.nodes.c.latest_version) + d2 = select([self.nodes.c.node, self.nodes.c.latest_version], + self.nodes.c.path.like(self.escape_like(path) + '%', + escape=ESCAPE_CHAR)).cte("d2") + inner_join = \ + self.versions.join(d2, + onclause=(self.versions.c.serial == + d2.c.latest_version)) s = select([func.count(self.versions.c.serial), func.sum(self.versions.c.size), - func.max(self.versions.c.mtime)], from_obj=[inner_join]) + func.max(self.versions.c.mtime)], + from_obj=[inner_join]) c2 = select([self.nodes.c.node], self.nodes.c.path.like(self.escape_like(path) + '%', escape=ESCAPE_CHAR)) if before != inf: s = s.where(and_(v.c.serial == c1, - v.c.cluster != except_cluster, - v.c.node.in_(c2))) + v.c.cluster != except_cluster, + v.c.node.in_(c2))) else: - s = s.where(and_(self.versions.c.cluster != except_cluster, - self.versions.c.node.in_(c2))) + s = s.where(and_(self.versions.c.cluster != except_cluster)) rp = self.conn.execute(s) r = rp.fetchone() rp.close() if not r: return None - size = long(r[1] - props[SIZE]) + size = long(r[1] - props.size) mtime = max(mtime, r[2]) return (count, size, mtime) @@ -716,40 +811,75 @@ class Node(DBWorker): def version_create(self, node, hash, size, type, source, muser, uuid, checksum, cluster=0, - update_statistics_ancestors_depth=None): + update_statistics_ancestors_depth=None, + available=MAP_AVAILABLE, map_check_timestamp=None, + mapfile=None, is_snapshot=False): """Create a new version from the given properties. - Return the (serial, mtime) of the new version. + Return the (serial, mtime, mapfile) of the new version. + + If mapfile is not None, set mapfile to this value. + Otherwise, assign to the mapfile a new unique identifier. + + :raises DatabaseError """ mtime = time() - s = self.versions.insert().values( + if size == 0: + mapfile = None + elif mapfile is None: + mapfile = literal(self.mapfile_prefix) + \ + type_coerce(functions.next_value(self.mapfile_seq), String) + s = self.versions.insert().returning(self.versions.c.serial, + self.versions.c.mtime, + self.versions.c.mapfile) + s = s.values( node=node, hash=hash, size=size, type=type, source=source, mtime=mtime, muser=muser, uuid=uuid, checksum=checksum, - cluster=cluster) - serial = self.conn.execute(s).inserted_primary_key[0] + cluster=cluster, available=available, + map_check_timestamp=map_check_timestamp, + mapfile=mapfile, + is_snapshot=is_snapshot) + r = self.conn.execute(s) + serial, mtime, mapfile = r.fetchone() + r.close() self.statistics_update_ancestors(node, 1, size, mtime, cluster, update_statistics_ancestors_depth) self.nodes_set_latest_version(node, serial) - return serial, mtime + return serial, mtime, mapfile - def version_lookup(self, node, before=inf, cluster=0, all_props=True): + def version_lookup(self, node, before=inf, cluster=0, all_props=True, + keys=()): """Lookup the current version of the given node. - Return a list with its properties: - (serial, node, hash, size, type, source, mtime, - muser, uuid, checksum, cluster) - or None if the current version is not found in the given cluster. + If the current version is not found in the given cluster, + return None. + If all_props is False, return the version's serial. + Otherwise: + If keys is not empty, return only the specific properties + (by filtering out the invalid ones). + + If keys is empty, return all properties in the order + (serial, node, hash, size, type, source, mtime, muser, uuid, + checksum, cluster, available, map_check_timestamp) + This is bad tactic, since it may have considerable + impact on the performance. """ v = self.versions.alias('v') if not all_props: s = select([v.c.serial]) else: - s = select([v.c.serial, v.c.node, v.c.hash, + if keys: + cols = [getattr(v.c, col) for col in keys if hasattr(v.c, col)] + else: + cols = [v.c.serial, v.c.node, v.c.hash, v.c.size, v.c.type, v.c.source, v.c.mtime, v.c.muser, v.c.uuid, - v.c.checksum, v.c.cluster]) + v.c.checksum, v.c.cluster, + v.c.available, v.c.map_check_timestamp, + v.c.mapfile, v.c.is_snapshot] + s = select(cols) if before != inf: c = select([func.max(self.versions.c.serial)], self.versions.c.node == node) @@ -767,11 +897,12 @@ class Node(DBWorker): return None def version_lookup_bulk(self, nodes, before=inf, cluster=0, - all_props=True, order_by_path=False): + all_props=True, order_by_path=False, + keys=()): """Lookup the current versions of the given nodes. Return a list with their properties: (serial, node, hash, size, type, source, mtime, muser, uuid, - checksum, cluster). + checksum, cluster, available, map_check_timestamp). """ if not nodes: return () @@ -780,10 +911,16 @@ class Node(DBWorker): if not all_props: s = select([v.c.serial]) else: - s = select([v.c.serial, v.c.node, v.c.hash, + if keys: + cols = [getattr(v.c, col) for col in keys if hasattr(v.c, col)] + else: + cols = [v.c.serial, v.c.node, v.c.hash, v.c.size, v.c.type, v.c.source, v.c.mtime, v.c.muser, v.c.uuid, - v.c.checksum, v.c.cluster]) + v.c.checksum, v.c.cluster, + v.c.available, v.c.map_check_timestamp, + v.c.mapfile, v.c.is_snapshot] + s = select(cols) if before != inf: c = select([func.max(self.versions.c.serial)], self.versions.c.node.in_(nodes)) @@ -804,36 +941,32 @@ class Node(DBWorker): r.close() return (tuple(row.values()) for row in rproxy) - def version_get_properties(self, serial, keys=(), propnames=_propnames, + def version_get_properties(self, serial, keys=(), props=None, node=None): """Return a sequence of values for the properties of the version specified by serial and the keys, in the order given. If keys is empty, return all properties in the order (serial, node, hash, size, type, source, mtime, muser, uuid, - checksum, cluster). + checksum, cluster, available, map_check_timestamp). """ + props = props or self._props + keys = keys or props.keys() v = self.versions.alias() - s = select([v.c.serial, v.c.node, v.c.hash, - v.c.size, v.c.type, v.c.source, - v.c.mtime, v.c.muser, v.c.uuid, - v.c.checksum, v.c.cluster], v.c.serial == serial) + cols = [getattr(v.c, p) for p in keys if hasattr(v.c, p)] + s = select(cols, v.c.serial == serial) if node is not None: s = s.where(v.c.node == node) rp = self.conn.execute(s) r = rp.fetchone() rp.close() - if r is None: - return r - - if not keys: - return r - return [r[propnames[k]] for k in keys if k in propnames] + return r - def version_put_property(self, serial, key, value): + def version_put_property(self, serial, key, value, props=None): """Set value for the property of version specified by key.""" - if key not in _propnames: + props = props or self._props + if key not in props: return s = self.versions.update() s = s.where(self.versions.c.serial == serial) @@ -847,9 +980,9 @@ class Node(DBWorker): props = self.version_get_properties(serial) if not props: return - node = props[NODE] - size = props[SIZE] - oldcluster = props[CLUSTER] + node = props.node + size = props.size + oldcluster = props.cluster if cluster == oldcluster: return @@ -870,10 +1003,10 @@ class Node(DBWorker): props = self.version_get_properties(serial) if not props: return - node = props[NODE] - hash = props[HASH] - size = props[SIZE] - cluster = props[CLUSTER] + node = props.node + hash = props.hash + size = props.size + cluster = props.cluster mtime = time() self.statistics_update_ancestors(node, -1, -size, mtime, cluster, @@ -888,6 +1021,17 @@ class Node(DBWorker): return hash, size + def attribute_get_domains(self, serial, node=None): + node = node or select([self.versions.c.node], + self.versions.c.serial == serial) + s = select([self.attributes.c.domain], + and_(self.attributes.c.serial == serial, + self.attributes.c.node == node)).distinct() + r = self.conn.execute(s) + l = r.fetchall() + r.close() + return [d[0] for d in l] + def attribute_get(self, serial, domain, keys=()): """Return a list of (key, value) pairs of the specific version. @@ -913,23 +1057,21 @@ class Node(DBWorker): def attribute_set(self, serial, domain, node, items, is_latest=True): """Set the attributes of the version specified by serial. - Receive attributes as an iterable of (key, value) pairs. + Receive attributes as a mapping object. + + Raises: sqlalchemy.exc.IntegrityError """ - #insert or replace - #TODO better upsert - for k, v in items: - s = self.attributes.update() - s = s.where(and_(self.attributes.c.serial == serial, - self.attributes.c.domain == domain, - self.attributes.c.key == k)) - s = s.values(value=v) - rp = self.conn.execute(s) - rp.close() - if rp.rowcount == 0: - s = self.attributes.insert() - s = s.values(serial=serial, domain=domain, node=node, - is_latest=is_latest, key=k, value=v) - self.conn.execute(s).close() + + if not items: + return + s = self.attributes.insert() + values = [{'serial': serial, + 'domain': domain, + 'node': node, + 'is_latest': is_latest, + 'key': k, + 'value': v} for k, v in items.iteritems()] + self.conn.execute(s, values).close() def attribute_del(self, serial, domain, keys=()): """Delete attributes of the version specified by serial. @@ -938,13 +1080,11 @@ class Node(DBWorker): """ if keys: - #TODO more efficient way to do this? - for key in keys: - s = self.attributes.delete() - s = s.where(and_(self.attributes.c.serial == serial, - self.attributes.c.domain == domain, - self.attributes.c.key == key)) - self.conn.execute(s).close() + s = self.attributes.delete() + s = s.where(and_(self.attributes.c.serial == serial, + self.attributes.c.domain == domain, + self.attributes.c.key.in_(keys))) + self.conn.execute(s).close() else: s = self.attributes.delete() s = s.where(and_(self.attributes.c.serial == serial, @@ -1001,9 +1141,9 @@ class Node(DBWorker): v.c.node == self.versions.c.node)) else: filtered = select([self.nodes.c.latest_version]) - filtered = filtered.where( - self.nodes.c.node == self.versions.c.node).\ - correlate(self.versions) + filtered = filtered.where(self.nodes.c.node == + self.versions.c.node + ).correlate(self.versions) s = s.where(self.versions.c.serial == filtered) s = s.where(self.versions.c.cluster != except_cluster) s = s.where(self.versions.c.node.in_(select([self.nodes.c.node], @@ -1016,8 +1156,8 @@ class Node(DBWorker): conjb = [] for path, match in pathq: if match == MATCH_PREFIX: - conja.append(self.nodes.c.path.like( - self.escape_like(path) + '%', escape=ESCAPE_CHAR)) + conja.append(self.nodes.c.path.like(self.escape_like(path) + + '%', escape=ESCAPE_CHAR)) elif match == MATCH_EXACT: conjb.append(path) if conja or conjb: @@ -1088,53 +1228,65 @@ class Node(DBWorker): v = self.versions.alias('v') if before != inf: - filtered = select([func.max(v.c.serial)], - and_(v.c.mtime < before, - v.c.node == self.versions.c.node)) - inner_join = self.nodes.join( - self.versions, - onclause=self.versions.c.serial == filtered) + d4_insub = select([self.nodes.c.node], + self.nodes.c.parent==parent) + d4_insub = d4_insub.where(and_( + self.nodes.c.path > bindparam('start'), + self.nodes.c.path < nextling)) + + d4 = select([func.max(v.c.serial).label("vmax"), + v.c.node, + self.nodes.c.path]).where(v.c.mtime < before) + d4 = d4.where(v.c.node.in_(d4_insub)) + d4 = d4.where(v.c.node==self.nodes.c.node) + d4 = d4.group_by(v.c.node, self.nodes.c.path).cte("d4") + inner_join = \ + d4.join(self.versions, + onclause=self.versions.c.serial == d4.c.vmax) else: - filtered = select([self.nodes.c.latest_version]) - filtered = filtered.where( - self.nodes.c.node == self.versions.c.node).\ - correlate(self.versions) - inner_join = self.nodes.join( - self.versions, - onclause=self.versions.c.serial == filtered) + d4 = select([self.nodes.c.path, + self.nodes.c.node, + self.nodes.c.latest_version]).where( + self.nodes.c.parent == parent) + d4 = d4.where(and_(self.nodes.c.path > bindparam('start'), + self.nodes.c.path < nextling)).cte("d4") + + inner_join = \ + d4.join(self.versions, + onclause= + self.versions.c.serial == d4.c.latest_version) if not all_props: - s = select([self.nodes.c.path, self.versions.c.serial], + s = select([d4.c.path, + self.versions.c.serial], from_obj=[inner_join]).distinct() else: - s = select([self.nodes.c.path, - self.versions.c.serial, self.versions.c.node, - self.versions.c.hash, - self.versions.c.size, self.versions.c.type, - self.versions.c.source, - self.versions.c.mtime, self.versions.c.muser, - self.versions.c.uuid, - self.versions.c.checksum, - self.versions.c.cluster], + s = select([d4.c.path, + self.versions.c.serial, self.versions.c.node, + self.versions.c.hash, + self.versions.c.size, self.versions.c.type, + self.versions.c.source, + self.versions.c.mtime, self.versions.c.muser, + self.versions.c.uuid, + self.versions.c.checksum, + self.versions.c.cluster, + self.versions.c.available, + self.versions.c.map_check_timestamp, + self.versions.c.mapfile, + self.versions.c.is_snapshot], from_obj=[inner_join]).distinct() s = s.where(self.versions.c.cluster != except_cluster) - s = s.where(self.versions.c.node.in_(select([self.nodes.c.node], - self.nodes.c.parent == parent))) - - s = s.where(self.versions.c.node == self.nodes.c.node) - s = s.where(and_(self.nodes.c.path > bindparam('start'), - self.nodes.c.path < nextling)) + s = s.where(self.versions.c.node == d4.c.node) conja = [] conjb = [] for path, match in pathq: if match == MATCH_PREFIX: - conja.append( - self.nodes.c.path.like(self.escape_like(path) + '%', - escape=ESCAPE_CHAR)) + conja.append(d4.c.path.like(self.escape_like(path) + + '%', escape=ESCAPE_CHAR)) elif match == MATCH_EXACT: conjb.append(path) if conja or conjb: - s = s.where(or_(self.nodes.c.path.in_(conjb), *conja)) + s = s.where(or_(d4.c.path.in_(conjb), *conja)) if sizeq and len(sizeq) == 2: if sizeq[0]: @@ -1146,35 +1298,35 @@ class Node(DBWorker): included, excluded, opers = parse_filters(filterq) if included: subs = select([1]) - subs = subs.where( - self.attributes.c.serial == - self.versions.c.serial).correlate(self.versions) + subs = subs.where(self.attributes.c.serial == + self.versions.c.serial + ).correlate(self.versions) subs = subs.where(self.attributes.c.domain == domain) - subs = subs.where(or_(*[self.attributes.c.key.op('=')(x) for - x in included])) + subs = subs.where(or_(*[self.attributes.c.key.op('=')(x) + for x in included])) s = s.where(exists(subs)) if excluded: subs = select([1]) - subs = subs.where( - self.attributes.c.serial == self.versions.c.serial).\ - correlate(self.versions) + subs = subs.where(self.attributes.c.serial == + self.versions.c.serial + ).correlate(self.versions) subs = subs.where(self.attributes.c.domain == domain) - subs = subs.where(or_(*[self.attributes.c.key.op('=')(x) for - x in excluded])) + subs = subs.where(or_(*[self.attributes.c.key.op('=')(x) + for x in excluded])) s = s.where(not_(exists(subs))) if opers: for k, o, val in opers: subs = select([1]) - subs = subs.where( - self.attributes.c.serial == self.versions.c.serial).\ - correlate(self.versions) + subs = subs.where(self.attributes.c.serial == + self.versions.c.serial + ).correlate(self.versions) subs = subs.where(self.attributes.c.domain == domain) subs = subs.where( and_(self.attributes.c.key.op('=')(k), self.attributes.c.value.op(o)(val))) s = s.where(exists(subs)) - s = s.order_by(self.nodes.c.path) + s = s.order_by(d4.c.path) if not delimiter: s = s.limit(limit) @@ -1252,9 +1404,13 @@ class Node(DBWorker): n = self.nodes.alias('n') a = self.attributes.alias('a') - s = select([n.c.path, v.c.serial, v.c.node, v.c.hash, v.c.size, - v.c.type, v.c.source, v.c.mtime, v.c.muser, v.c.uuid, - v.c.checksum, v.c.cluster, a.c.key, a.c.value]) + props = [n.c.path, v.c.serial, v.c.node, v.c.hash, v.c.size, v.c.type, + v.c.source, v.c.mtime, v.c.muser, v.c.uuid, v.c.checksum, + v.c.cluster, v.c.available, v.c.map_check_timestamp, + v.c.mapfile, v.c.is_snapshot] + cols = props + [a.c.key, a.c.value] + + s = select(cols) if cluster: s = s.where(v.c.cluster == cluster) s = s.where(v.c.serial == a.c.serial) @@ -1268,17 +1424,17 @@ class Node(DBWorker): rows = r.fetchall() r.close() - group_by = itemgetter(slice(12)) + group_by = itemgetter(slice(len(props))) rows.sort(key=group_by) groups = groupby(rows, group_by) - return [(k[0], k[1:], dict([i[12:] for i in data])) for + return [(k[0], k[1:], dict([i[len(props):] for i in data])) for (k, data) in groups] def get_props(self, paths): inner_join = \ - self.nodes.join( - self.versions, - onclause=self.versions.c.serial == self.nodes.c.latest_version) + self.nodes.join(self.versions, + onclause=self.versions.c.serial == + self.nodes.c.latest_version) cc = self.nodes.c.path.in_(paths) s = select([self.nodes.c.path, self.versions.c.type], from_obj=[inner_join]).where(cc).distinct() diff --git a/snf-pithos-backend/pithos/backends/lib/sqlalchemy/permissions.py b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/permissions.py index 1d709e06fee269a785f58b747a8bf27df4caa377..7b32add8ffba1efcc64852575c4f62d6f2b3eebb 100644 --- a/snf-pithos-backend/pithos/backends/lib/sqlalchemy/permissions.py +++ b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/permissions.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from sqlalchemy.sql import select, literal, or_, and_ from sqlalchemy.sql.expression import join, union @@ -159,13 +141,17 @@ class Permissions(XFeatures, Groups, Public, Node): def access_check_bulk(self, paths, member): rows = None - xfeatures_xfeaturevals = self.xfeaturevals.join(self.xfeatures, - onclause=and_(self.xfeatures.c.feature_id == - self.xfeaturevals.c.feature_id, self.xfeatures.c.path.in_(paths))) + xfeatures_xfeaturevals = \ + self.xfeaturevals.join(self.xfeatures, + onclause= + and_(self.xfeatures.c.feature_id == + self.xfeaturevals.c.feature_id, + self.xfeatures.c.path.in_(paths))) s = select([self.xfeatures.c.path, - self.xfeaturevals.c.value, - self.xfeaturevals.c.feature_id, - self.xfeaturevals.c.key], from_obj=[xfeatures_xfeaturevals]) + self.xfeaturevals.c.value, + self.xfeaturevals.c.feature_id, + self.xfeaturevals.c.key], + from_obj=[xfeatures_xfeaturevals]) r = self.conn.execute(s) rows = r.fetchall() r.close() diff --git a/snf-pithos-backend/pithos/backends/lib/sqlalchemy/public.py b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/public.py index 809ebad554782a8a9d0e9f216aed727b297f2830..d8e9d0fb449867315d866b0870fdea1e56b331ac 100644 --- a/snf-pithos-backend/pithos/backends/lib/sqlalchemy/public.py +++ b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/public.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from dbworker import DBWorker from sqlalchemy import Table, Column, String, Integer, Boolean, MetaData diff --git a/snf-pithos-backend/pithos/backends/lib/sqlalchemy/quotaholder_serials.py b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/quotaholder_serials.py index 410eb9b822ba52c34e1f0117f22809babeb1c9cb..51a5c3cfb9ba7623133f418849567c164336a23c 100644 --- a/snf-pithos-backend/pithos/backends/lib/sqlalchemy/quotaholder_serials.py +++ b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/quotaholder_serials.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from sqlalchemy import Table, Column, MetaData from sqlalchemy.types import BigInteger diff --git a/snf-pithos-backend/pithos/backends/lib/sqlalchemy/xfeatures.py b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/xfeatures.py index 8ee5e6f1e084df9890e7830d0dfd06bff28d9320..2f1b804351461664d27a8a390d89551c8b479d1f 100644 --- a/snf-pithos-backend/pithos/backends/lib/sqlalchemy/xfeatures.py +++ b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/xfeatures.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from collections import defaultdict from sqlalchemy import Table, Column, String, Integer, MetaData, ForeignKey diff --git a/snf-pithos-backend/pithos/backends/lib/sqlite/__init__.py b/snf-pithos-backend/pithos/backends/lib/sqlite/__init__.py index 1754cff4cec478c277f05dcd77b5a991cf94133f..6faff9170fdb79ffdc9844117f933e9c1a60f9fe 100644 --- a/snf-pithos-backend/pithos/backends/lib/sqlite/__init__.py +++ b/snf-pithos-backend/pithos/backends/lib/sqlite/__init__.py @@ -1,45 +1,24 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from dbwrapper import DBWrapper -from node import (Node, ROOTNODE, SERIAL, NODE, HASH, SIZE, TYPE, MTIME, MUSER, - UUID, CHECKSUM, CLUSTER, MATCH_PREFIX, MATCH_EXACT) +from node import (Node, ROOTNODE, MATCH_PREFIX, MATCH_EXACT) from permissions import Permissions, READ, WRITE from config import Config from quotaholder_serials import QuotaholderSerial -__all__ = ["DBWrapper", - "Node", "ROOTNODE", "SERIAL", "NODE", "HASH", "SIZE", "TYPE", - "MTIME", "MUSER", "UUID", "CHECKSUM", "CLUSTER", "MATCH_PREFIX", - "MATCH_EXACT", "Permissions", "READ", "WRITE", "Config", +__all__ = ["DBWrapper", "Node", "ROOTNODE", "MATCH_PREFIX", "MATCH_EXACT", + "Permissions", "READ", "WRITE", "Config", "QuotaholderSerial"] diff --git a/snf-pithos-backend/pithos/backends/lib/sqlite/config.py b/snf-pithos-backend/pithos/backends/lib/sqlite/config.py index ae4666091262855fdc929c2184cc4c4b86a42858..b92134a3e619cc8b2afbce527cb369c7310272b0 100644 --- a/snf-pithos-backend/pithos/backends/lib/sqlite/config.py +++ b/snf-pithos-backend/pithos/backends/lib/sqlite/config.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # from collections import defaultdict # diff --git a/snf-pithos-backend/pithos/backends/lib/sqlite/dbworker.py b/snf-pithos-backend/pithos/backends/lib/sqlite/dbworker.py index f0ca3ba3946a604eaf3b7b1b5793a94c71e3466f..4390e47b164a989e4631c6d7492a6cd2d8d63b62 100644 --- a/snf-pithos-backend/pithos/backends/lib/sqlite/dbworker.py +++ b/snf-pithos-backend/pithos/backends/lib/sqlite/dbworker.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. class DBWorker(object): diff --git a/snf-pithos-backend/pithos/backends/lib/sqlite/dbwrapper.py b/snf-pithos-backend/pithos/backends/lib/sqlite/dbwrapper.py index e13aba2a381ab50d930f59ae0f6bbd3e2b09eb4a..99d985c390ac597923af7fb2da928c161d107059 100644 --- a/snf-pithos-backend/pithos/backends/lib/sqlite/dbwrapper.py +++ b/snf-pithos-backend/pithos/backends/lib/sqlite/dbwrapper.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. try: from pysqlite2 import dbapi2 as sqlite3 diff --git a/snf-pithos-backend/pithos/backends/lib/sqlite/groups.py b/snf-pithos-backend/pithos/backends/lib/sqlite/groups.py index 83946e6016328ed13714dbfec001174dd2788e24..8c7e3f0bfa1b051d7dd939d6cfa09875032c6182 100644 --- a/snf-pithos-backend/pithos/backends/lib/sqlite/groups.py +++ b/snf-pithos-backend/pithos/backends/lib/sqlite/groups.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from collections import defaultdict @@ -75,12 +57,16 @@ class Groups(DBWorker): "values (?, ?, ?)") self.execute(q, (owner, group, member)) - def group_addmany(self, owner, group, members): - """Add members to a group.""" + def group_addmany(self, owner, groups): + """Add members to a group. + Receive groups as a mapping object. + """ q = ("insert or ignore into groups (owner, name, member) " "values (?, ?, ?)") - self.executemany(q, ((owner, group, member) for member in members)) + self.executemany(q, ((owner, group, member) + for group, members in groups.iteritems() + for member in sorted(members))) def group_remove(self, owner, group, member): """Remove a member from a group.""" diff --git a/snf-pithos-backend/pithos/backends/lib/sqlite/node.py b/snf-pithos-backend/pithos/backends/lib/sqlite/node.py index ae5bdfadbb4f4ae2bd92d3a01bf5c11bd0f5cc48..2d89816c0b1e98de6c64f818fc666d79e81e478e 100644 --- a/snf-pithos-backend/pithos/backends/lib/sqlite/node.py +++ b/snf-pithos-backend/pithos/backends/lib/sqlite/node.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from time import time from operator import itemgetter @@ -37,14 +19,12 @@ from itertools import groupby from dbworker import DBWorker +from pithos.backends.base import MAP_AVAILABLE from pithos.backends.filter import parse_filters ROOTNODE = 0 -(SERIAL, NODE, HASH, SIZE, TYPE, SOURCE, MTIME, MUSER, UUID, CHECKSUM, - CLUSTER) = range(11) - (MATCH_PREFIX, MATCH_EXACT) = range(2) inf = float('inf') @@ -86,21 +66,6 @@ def strprevling(prefix): return s -_propnames = { - 'serial': 0, - 'node': 1, - 'hash': 2, - 'size': 3, - 'type': 4, - 'source': 5, - 'mtime': 6, - 'muser': 7, - 'uuid': 8, - 'checksum': 9, - 'cluster': 10 -} - - class Node(DBWorker): """Nodes store path organization and have multiple versions. Versions store object history and have multiple attributes. @@ -110,6 +75,10 @@ class Node(DBWorker): # TODO: Provide an interface for included and excluded clusters. def __init__(self, **params): + self._props = params.pop('props') + for p in self._props: + setattr(self, p.upper(), self._props[p]) + self.mapfile_prefix = params.pop('mapfile_prefix', 'snf_file_') DBWorker.__init__(self, **params) execute = self.execute @@ -165,12 +134,18 @@ class Node(DBWorker): uuid text not null default '', checksum text not null default '', cluster integer not null default 0, + available integer not null default 1, + map_check_timestamp integer, + mapfile text, + is_snapshot boolean not null default false, foreign key (node) references nodes(node) on update cascade on delete cascade ) """) execute(""" create index if not exists idx_versions_node_mtime on versions(node, mtime) """) + execute(""" create index if not exists idx_versions_node + on versions(node) """) execute(""" create index if not exists idx_versions_node_uuid on versions(uuid) """) @@ -191,6 +166,10 @@ class Node(DBWorker): execute(""" create index if not exists idx_attributes_serial_node on attributes(serial, node) """) + execute(""" create table if not exists mapfile_seq + ( serial integer primary key, + dummy boolean default -1) """) + wrapper = self.wrapper wrapper.execute() try: @@ -245,15 +224,28 @@ class Node(DBWorker): self.execute(q, (node,)) return self.fetchone() - def node_get_versions(self, node, keys=(), propnames=_propnames): + def node_get_parent_path(self, node): + """Return the node's parent path. + Return None if the node is not found. + """ + + q = ("select n2.path from nodes as n1, nodes as n2 " + "where n2.node = n1.parent and n1.node = ?") + self.execute(q, (node,)) + l = self.fetchone() + return l[0] if l is not None else None + + def node_get_versions(self, node, keys=(), props=None): """Return the properties of all versions at node. If keys is empty, return all properties in the order (serial, node, hash, size, type, source, mtime, muser, uuid, - checksum, cluster). + checksum, cluster, available, map_check_timestamp). """ + props = props or self._props q = ("select serial, node, hash, size, type, source, mtime, muser, " - "uuid, checksum, cluster " + "uuid, checksum, cluster, available, map_check_timestamp, " + "mapfile, is_snapshot " "from versions " "where node = ?") self.execute(q, (node,)) @@ -263,7 +255,7 @@ class Node(DBWorker): if not keys: return r - return [[p[propnames[k]] for k in keys if k in propnames] for p in r] + return [[p[props[k]] for k in keys if k in props] for p in r] def node_count_children(self, node): """Return node's child count.""" @@ -493,7 +485,7 @@ class Node(DBWorker): while True: if node == ROOTNODE: break - if recursion_depth and recursion_depth <= i: + if recursion_depth is not None and recursion_depth <= i: break props = self.node_get_properties(node) if props is None: @@ -521,7 +513,8 @@ class Node(DBWorker): # The latest version. q = ("select serial, node, hash, size, type, source, mtime, muser, " - "uuid, checksum, cluster " + "uuid, checksum, cluster, available, map_check_timestamp, " + "mapfile, is_snapshot " "from versions v " "where serial = %s " "and cluster != ?") @@ -531,7 +524,8 @@ class Node(DBWorker): props = fetchone() if props is None: return None - mtime = props[MTIME] + + mtime = props[self.MTIME] # First level, just under node (get population). q = ("select count(serial), sum(size), max(mtime) " @@ -568,7 +562,7 @@ class Node(DBWorker): r = fetchone() if r is None: return None - size = r[1] - props[SIZE] + size = r[1] - props[self.SIZE] mtime = max(mtime, r[2]) return (count, size, mtime) @@ -579,31 +573,54 @@ class Node(DBWorker): def version_create(self, node, hash, size, type, source, muser, uuid, checksum, cluster=0, - update_statistics_ancestors_depth=None): + update_statistics_ancestors_depth=None, + available=MAP_AVAILABLE, map_check_timestamp=None, + mapfile=True, is_snapshot=False): """Create a new version from the given properties. - Return the (serial, mtime) of the new version. + Return the (serial, mtime, mapfile) of the new version. + + If mapfile is not None, set mapfile to this value. + Otherwise, assign to the mapfile a new unique identifier. """ + if size == 0: + mapfile = None + elif mapfile is None: + q = ("insert into mapfile_seq (dummy) values (?)") + serial = self.execute(q, (False,)).lastrowid + mapfile = ''.join([self.mapfile_prefix, unicode(serial)]) + q = ("insert into versions (node, hash, size, type, source, mtime, " - "muser, uuid, checksum, cluster) " - "values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") + "muser, uuid, checksum, cluster, available, " + "map_check_timestamp, mapfile, is_snapshot) " + "values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") mtime = time() props = (node, hash, size, type, source, mtime, muser, - uuid, checksum, cluster) + uuid, checksum, cluster, available, map_check_timestamp, + mapfile, is_snapshot) serial = self.execute(q, props).lastrowid self.statistics_update_ancestors(node, 1, size, mtime, cluster, update_statistics_ancestors_depth) self.nodes_set_latest_version(node, serial) - return serial, mtime + return serial, mtime, mapfile - def version_lookup(self, node, before=inf, cluster=0, all_props=True): + def version_lookup(self, node, before=inf, cluster=0, all_props=True, + keys=()): """Lookup the current version of the given node. - Return a list with its properties: - (serial, node, hash, size, type, source, mtime, - muser, uuid, checksum, cluster) - or None if the current version is not found in the given cluster. + If the current version is not found in the given cluster, + return None. + If all_props is False, return the version's serial. + Otherwise: + If keys is not empty, return only the specific properties + (by filtering out the invalid ones). + + If keys is empty, return all properties in the order + (serial, node, hash, size, type, source, mtime, muser, uuid, + checksum, cluster, available, map_check_timestamp) + This is bad tactic, since it may have considerable + impact on the performance. """ q = ("select %s " @@ -615,9 +632,14 @@ class Node(DBWorker): if not all_props: q = q % ("serial", subq) else: - q = q % (("serial, node, hash, size, type, source, mtime, muser, " - "uuid, checksum, cluster"), - subq) + if keys: + cols = ','.join((k for k in keys if k in self._props)) + else: + cols = ("serial, node, hash, size, type, source, mtime, " + "muser, uuid, checksum, cluster, " + "available, map_check_timestamp, " + "mapfile, is_snapshot") + q = q % (cols, subq) self.execute(q, args + [cluster]) props = self.fetchone() @@ -626,11 +648,13 @@ class Node(DBWorker): return None def version_lookup_bulk(self, nodes, before=inf, cluster=0, - all_props=True, order_by_path=False): + all_props=True, order_by_path=False, + keys=()): """Lookup the current versions of the given nodes. Return a list with their properties: (serial, node, hash, size, type, source, mtime, muser, uuid, - checksum, cluster). + checksum, cluster, available, map_check_timestamp, + mapfile, is_snapshot). """ if not nodes: @@ -645,45 +669,45 @@ class Node(DBWorker): if not all_props: q = q % ("serial", subq, '') else: - q = q % (("serial, v.node, hash, size, type, source, mtime, " - "muser, uuid, checksum, cluster"), - subq, - "order by path" if order_by_path else "") + if keys: + cols = ','.join(k for k in keys if k in self._props) + else: + cols = ("v.serial, v.node, v.hash, v.size, v.type, v.source, " + "v.mtime, v.muser, v.uuid, v.checksum, v.cluster, " + "v.available, v.map_check_timestamp,v.mapfile, " + "v.is_snapshot") + q = q % (cols, subq, "order by path" if order_by_path else "") args += [cluster] self.execute(q, args) return self.fetchall() - def version_get_properties(self, serial, keys=(), propnames=_propnames, + def version_get_properties(self, serial, keys=(), props=None, node=None): """Return a sequence of values for the properties of the version specified by serial and the keys, in the order given. If keys is empty, return all properties in the order (serial, node, hash, size, type, source, mtime, muser, uuid, - checksum, cluster). + checksum, cluster, available, map_check_timestamp). """ - q = ("select serial, node, hash, size, type, source, mtime, muser, " - "uuid, checksum, cluster " - "from versions " - "where serial = ? ") + props = props or self._props + keys = keys or props.keys() + cols = ','.join(k for k in keys if k in props) + q = ("select %s from versions where serial = ? ") % cols args = [serial] if node is not None: q += ("and node = ?") args += [node] self.execute(q, args) r = self.fetchone() - if r is None: - return r + return r - if not keys: - return r - return [r[propnames[k]] for k in keys if k in propnames] - - def version_put_property(self, serial, key, value): + def version_put_property(self, serial, key, value, props=None): """Set value for the property of version specified by key.""" - if key not in _propnames: + props = props or self._props + if key not in props: return q = "update versions set %s = ? where serial = ?" % key self.execute(q, (value, serial)) @@ -695,9 +719,9 @@ class Node(DBWorker): props = self.version_get_properties(serial) if not props: return - node = props[NODE] - size = props[SIZE] - oldcluster = props[CLUSTER] + node = props[self.NODE] + size = props[self.SIZE] + oldcluster = props[self.CLUSTER] if cluster == oldcluster: return @@ -716,10 +740,10 @@ class Node(DBWorker): props = self.version_get_properties(serial) if not props: return - node = props[NODE] - hash = props[HASH] - size = props[SIZE] - cluster = props[CLUSTER] + node = props[self.NODE] + hash = props[self.HASH] + size = props[self.SIZE] + cluster = props[self.CLUSTER] mtime = time() self.statistics_update_ancestors(node, -1, -size, mtime, cluster, @@ -733,6 +757,22 @@ class Node(DBWorker): self.nodes_set_latest_version(node, props[0]) return hash, size + + def attribute_get_domains(self, serial, node=None): + q = ("select distinct domain from attributes " + "where serial = ? ") + args = [serial] + if node is not None: + q += ("and node = ?") + args += [node] + else: + q += ("and node = " + "(select node from versions where serial = ?)") + args += [serial] + execute = self.execute + execute(q, args) + return [d[0] for d in self.fetchall()] + def attribute_get(self, serial, domain, keys=()): """Return a list of (key, value) pairs of the specific version. @@ -754,14 +794,16 @@ class Node(DBWorker): def attribute_set(self, serial, domain, node, items, is_latest=True): """Set the attributes of the version specified by serial. - Receive attributes as an iterable of (key, value) pairs. + Receive attributes as a mapping object. """ + if not items: + return q = ("insert or replace into attributes " "(serial, domain, node, is_latest, key, value) " "values (?, ?, ?, ?, ?, ?)") self.executemany(q, ((serial, domain, node, is_latest, k, v) for - k, v in items)) + k, v in items.iteritems())) def attribute_del(self, serial, domain, keys=()): """Delete attributes of the version specified by serial. @@ -771,8 +813,9 @@ class Node(DBWorker): if keys: q = ("delete from attributes " - "where serial = ? and domain = ? and key = ?") - self.executemany(q, ((serial, domain, key) for key in keys)) + "where serial = ? and domain = ? and key in (%s)" % + ','.join(keys)) + self.execute(q, (serial, domain)) else: q = "delete from attributes where serial = ? and domain = ?" self.execute(q, (serial, domain)) @@ -1020,7 +1063,9 @@ class Node(DBWorker): q = q % ("v.serial", subq) else: q = q % (("v.serial, v.node, v.hash, v.size, v.type, v.source, " - "v.mtime, v.muser, v.uuid, v.checksum, v.cluster"), + "v.mtime, v.muser, v.uuid, v.checksum, v.cluster, " + "v.available, v.map_check_timestamp, " + "mapfile, is_snapshot"), subq) args += [except_cluster, parent, start, nextling] start_index = len(args) - 2 @@ -1043,8 +1088,9 @@ class Node(DBWorker): q += " order by n.path" if not delimiter: - q += " limit ?" - args.append(limit) + if limit: + q += " limit ?" + args.append(limit) execute(q, args) return self.fetchall(), () @@ -1114,17 +1160,20 @@ class Node(DBWorker): for the objects in the specific domain and cluster. """ - q = ("select n.path, v.serial, v.node, v.hash, " - "v.size, v.type, v.source, v.mtime, v.muser, " - "v.uuid, v.checksum, v.cluster, a.key, a.value " - "from nodes n, versions v, attributes a " + props = ('n.path', 'v.serial', 'v.node', 'v.hash', 'v.size', 'v.type', + 'v.source', 'v.mtime', 'v.muser', 'v.uuid', 'v.checksum', + 'v.cluster', 'v.available', 'v.map_check_timestamp', + 'v.mapfile', 'v.is_snapshot') + cols = list(props) + ['a.key', 'a.value'] + args = [domain] + q = ("select %s from nodes n, versions v, attributes a " "where v.serial = a.serial and " "a.domain = ? and " "a.node = n.node and " - "a.is_latest = 1 and " - "n.path in (%s)") % ','.join('?' for _ in paths) - args = [domain] - map(args.append, paths) + "a.is_latest = 1 ") % ','.join(cols) + if paths: + q += ("and path in (%s) " % ','.join('?' for _ in paths)) + map(args.append, paths) if cluster is not None: q += "and v.cluster = ?" args += [cluster] @@ -1132,10 +1181,10 @@ class Node(DBWorker): self.execute(q, args) rows = self.fetchall() - group_by = itemgetter(slice(12)) + group_by = itemgetter(slice(len(props))) rows.sort(key=group_by) groups = groupby(rows, group_by) - return [(k[0], k[1:], dict([i[12:] for i in data])) for + return [(k[0], k[1:], dict([i[len(props):] for i in data])) for (k, data) in groups] def get_props(self, paths): diff --git a/snf-pithos-backend/pithos/backends/lib/sqlite/permissions.py b/snf-pithos-backend/pithos/backends/lib/sqlite/permissions.py index b9e9682e654aa34842cd459955f5c11994366bde..c19f779c744826769498a686ced8b9633e3dc650 100644 --- a/snf-pithos-backend/pithos/backends/lib/sqlite/permissions.py +++ b/snf-pithos-backend/pithos/backends/lib/sqlite/permissions.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from xfeatures import XFeatures from groups import Groups diff --git a/snf-pithos-backend/pithos/backends/lib/sqlite/public.py b/snf-pithos-backend/pithos/backends/lib/sqlite/public.py index 404f167eeac6c5318ecb4f2a2317f38bff27bae2..33f635af09cdfae6f64c0a59b392d734090d0ca5 100644 --- a/snf-pithos-backend/pithos/backends/lib/sqlite/public.py +++ b/snf-pithos-backend/pithos/backends/lib/sqlite/public.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from dbworker import DBWorker diff --git a/snf-pithos-backend/pithos/backends/lib/sqlite/quotaholder_serials.py b/snf-pithos-backend/pithos/backends/lib/sqlite/quotaholder_serials.py index 33bea63c90152965e43e305a4ad0a419ba56b8b6..70a68eb4a6a912fb273e0ada5f311c2b8f2bc36f 100644 --- a/snf-pithos-backend/pithos/backends/lib/sqlite/quotaholder_serials.py +++ b/snf-pithos-backend/pithos/backends/lib/sqlite/quotaholder_serials.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from dbworker import DBWorker diff --git a/snf-pithos-backend/pithos/backends/lib/sqlite/xfeatures.py b/snf-pithos-backend/pithos/backends/lib/sqlite/xfeatures.py index 76444fd7f3bab6e08ee4f20ab7708b0e6cd80517..2f27b9ce3e17eaa962c485832a4d19eadfbf416c 100644 --- a/snf-pithos-backend/pithos/backends/lib/sqlite/xfeatures.py +++ b/snf-pithos-backend/pithos/backends/lib/sqlite/xfeatures.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from collections import defaultdict diff --git a/snf-pithos-backend/pithos/backends/migrate.py b/snf-pithos-backend/pithos/backends/migrate.py index 7f50d916a8ce9a97e807de5953fe7b02aa3c2704..e61c80126cf2a6639081197767c1170b4bb7c4f7 100644 --- a/snf-pithos-backend/pithos/backends/migrate.py +++ b/snf-pithos-backend/pithos/backends/migrate.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. """ Alembic migration wrapper for pithos backend database. diff --git a/snf-pithos-backend/pithos/backends/modular.py b/snf-pithos-backend/pithos/backends/modular.py index 84f0e723650849eb92efaa98e61cf5910367dd5d..39953c37ffb3825edac70b9c120ba7e101a8e1a6 100644 --- a/snf-pithos-backend/pithos/backends/modular.py +++ b/snf-pithos-backend/pithos/backends/modular.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import sys import uuid as uuidlib @@ -37,20 +19,29 @@ import logging import hashlib import binascii -from collections import defaultdict +from collections import defaultdict, OrderedDict from functools import wraps, partial from traceback import format_exc +from time import time + +from pithos.workers import glue +from archipelago.common import Segment, Xseg_ctx +from objpool import ObjectPool + try: from astakosclient import AstakosClient except ImportError: AstakosClient = None -from base import (DEFAULT_ACCOUNT_QUOTA, DEFAULT_CONTAINER_QUOTA, - DEFAULT_CONTAINER_VERSIONING, NotAllowedError, QuotaError, - BaseBackend, AccountExists, ContainerExists, AccountNotEmpty, - ContainerNotEmpty, ItemNotExists, VersionNotExists, - InvalidHash) +from pithos.backends.base import ( + DEFAULT_ACCOUNT_QUOTA, DEFAULT_CONTAINER_QUOTA, + DEFAULT_CONTAINER_VERSIONING, NotAllowedError, QuotaError, + BaseBackend, AccountExists, ContainerExists, AccountNotEmpty, + ContainerNotEmpty, ItemNotExists, VersionNotExists, + InvalidHash, IllegalOperationError, InconsistentContentSize, + LimitExceeded, InvalidPolicy, BrokenSnapshot, + MAP_ERROR, MAP_UNAVAILABLE, MAP_AVAILABLE) class DisabledAstakosClient(object): @@ -97,8 +88,6 @@ class HashMap(list): DEFAULT_DB_MODULE = 'pithos.backends.lib.sqlalchemy' DEFAULT_DB_CONNECTION = 'sqlite:///backend.db' DEFAULT_BLOCK_MODULE = 'pithos.backends.lib.hashfiler' -DEFAULT_BLOCK_PATH = 'data/' -DEFAULT_BLOCK_UMASK = 0o022 DEFAULT_BLOCK_SIZE = 4 * 1024 * 1024 # 4MB DEFAULT_HASH_ALGORITHM = 'sha256' # DEFAULT_QUEUE_MODULE = 'pithos.backends.lib.rabbitmq' @@ -109,8 +98,7 @@ DEFAULT_PUBLIC_URL_ALPHABET = ('0123456789' 'abcdefghijklmnopqrstuvwxyz' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') DEFAULT_PUBLIC_URL_SECURITY = 16 -DEFAULT_BACKEND_STORAGE = 'nfs' -DEFAULT_RADOS_CEPH_CONF = '/etc/ceph/ceph.conf' +DEFAULT_ARCHIPELAGO_CONF_FILE = '/etc/archipelago/archipelago.conf' QUEUE_MESSAGE_KEY_PREFIX = 'pithos.%s' QUEUE_CLIENT_ID = 'pithos' @@ -118,15 +106,31 @@ QUEUE_INSTANCE_ID = '1' (CLUSTER_NORMAL, CLUSTER_HISTORY, CLUSTER_DELETED) = range(3) +QUOTA_POLICY = 'quota' +VERSIONING_POLICY = 'versioning' +PROJECT = 'project' + inf = float('inf') ULTIMATE_ANSWER = 42 -DEFAULT_SOURCE = 'system' DEFAULT_DISKSPACE_RESOURCE = 'pithos.diskspace' +DEFAULT_MAP_CHECK_INTERVAL = 5 # set to 5 secs + +DEFAULT_MAPFILE_PREFIX = 'snf_file_' + +DEFAULT_RESOURCE_MAX_METADATA = 32 +DEFAULT_ACC_MAX_GROUPS = 32 +DEFAULT_ACC_MAX_GROUP_MEMBERS = 32 + logger = logging.getLogger(__name__) +_propnames = ('serial', 'node', 'hash', 'size', 'type', 'source', 'mtime', + 'muser', 'uuid', 'checksum', 'cluster', 'available', + 'map_check_timestamp', 'mapfile', 'is_snapshot') +_props = lambda props: OrderedDict((props[i], i) for i in range(len(props))) + def backend_method(func): @wraps(func) @@ -219,10 +223,10 @@ class ModularBackend(BaseBackend): Uses modules for SQL functions and storage. """ + _class_version = 1 def __init__(self, db_module=None, db_connection=None, - block_module=None, block_path=None, block_umask=None, - block_size=None, hash_algorithm=None, + block_module=None, block_size=None, hash_algorithm=None, queue_module=None, queue_hosts=None, queue_exchange=None, astakos_auth_url=None, service_token=None, astakosclient_poolsize=None, @@ -232,13 +236,16 @@ class ModularBackend(BaseBackend): account_quota_policy=None, container_quota_policy=None, container_versioning_policy=None, - backend_storage=None, - rados_ceph_conf=None): + archipelago_conf_file=None, + xseg_pool_size=8, + map_check_interval=None, + mapfile_prefix=None, + resource_max_metadata=None, + acc_max_groups=None, + acc_max_group_members=None): db_module = db_module or DEFAULT_DB_MODULE db_connection = db_connection or DEFAULT_DB_CONNECTION block_module = block_module or DEFAULT_BLOCK_MODULE - block_path = block_path or DEFAULT_BLOCK_PATH - block_umask = block_umask or DEFAULT_BLOCK_UMASK block_params = block_params or DEFAULT_BLOCK_PARAMS block_size = block_size or DEFAULT_BLOCK_SIZE hash_algorithm = hash_algorithm or DEFAULT_HASH_ALGORITHM @@ -248,14 +255,24 @@ class ModularBackend(BaseBackend): or DEFAULT_CONTAINER_QUOTA container_versioning_policy = container_versioning_policy \ or DEFAULT_CONTAINER_VERSIONING - backend_storage = backend_storage or DEFAULT_BACKEND_STORAGE - rados_ceph_conf = rados_ceph_conf or DEFAULT_RADOS_CEPH_CONF - - self.default_account_policy = {'quota': account_quota_policy} + archipelago_conf_file = archipelago_conf_file \ + or DEFAULT_ARCHIPELAGO_CONF_FILE + map_check_interval = map_check_interval \ + or DEFAULT_MAP_CHECK_INTERVAL + mapfile_prefix = mapfile_prefix \ + or DEFAULT_MAPFILE_PREFIX + resource_max_metadata = resource_max_metadata\ + or DEFAULT_RESOURCE_MAX_METADATA + acc_max_groups = acc_max_groups \ + or DEFAULT_ACC_MAX_GROUPS + acc_max_group_members = acc_max_group_members\ + or DEFAULT_ACC_MAX_GROUP_MEMBERS + + self.default_account_policy = {QUOTA_POLICY: account_quota_policy} self.default_container_policy = { - 'quota': container_quota_policy, - 'versioning': container_versioning_policy - } + QUOTA_POLICY: container_quota_policy, + VERSIONING_POLICY: container_versioning_policy, + PROJECT: None} # queue_hosts = queue_hosts or DEFAULT_QUEUE_HOSTS # queue_exchange = queue_exchange or DEFAULT_QUEUE_EXCHANGE @@ -263,10 +280,14 @@ class ModularBackend(BaseBackend): DEFAULT_PUBLIC_URL_SECURITY) self.public_url_alphabet = (public_url_alphabet or DEFAULT_PUBLIC_URL_ALPHABET) - self.hash_algorithm = hash_algorithm self.block_size = block_size self.free_versioning = free_versioning + self.map_check_interval = map_check_interval + self.mapfile_prefix = mapfile_prefix + self.resource_max_metadata = resource_max_metadata + self.acc_max_groups = acc_max_groups + self.acc_max_group_members = acc_max_group_members def load_module(m): __import__(m) @@ -275,28 +296,32 @@ class ModularBackend(BaseBackend): self.db_module = load_module(db_module) self.wrapper = self.db_module.DBWrapper(db_connection) params = {'wrapper': self.wrapper} - self.permissions = self.db_module.Permissions(**params) self.config = self.db_module.Config(**params) self.commission_serials = self.db_module.QuotaholderSerial(**params) for x in ['READ', 'WRITE']: setattr(self, x, getattr(self.db_module, x)) + params.update({'mapfile_prefix': self.mapfile_prefix, + 'props': _props(_propnames)}) + self.permissions = self.db_module.Permissions(**params) self.node = self.db_module.Node(**params) - for x in ['ROOTNODE', 'SERIAL', 'NODE', 'HASH', 'SIZE', 'TYPE', - 'MTIME', 'MUSER', 'UUID', 'CHECKSUM', 'CLUSTER', - 'MATCH_PREFIX', 'MATCH_EXACT']: + for x in ['ROOTNODE', 'MATCH_PREFIX', 'MATCH_EXACT']: setattr(self, x, getattr(self.db_module, x)) + for p in _propnames: + setattr(self, p.upper(), _props(_propnames)[p]) self.ALLOWED = ['read', 'write'] + glue.WorkerGlue.setupXsegPool(ObjectPool, Segment, Xseg_ctx, + cfile=archipelago_conf_file, + pool_size=xseg_pool_size) + + self.ioctx_pool = glue.WorkerGlue.ioctx_pool self.block_module = load_module(block_module) self.block_params = block_params - params = {'path': block_path, - 'block_size': self.block_size, + params = {'block_size': self.block_size, 'hash_algorithm': self.hash_algorithm, - 'umask': block_umask} + 'archipelago_cfile': archipelago_conf_file} params.update(self.block_params) - params.update({"backend_storage": backend_storage}) - params.update({"rados_ceph_conf": rados_ceph_conf}) self.store = self.block_module.Store(**params) if queue_module and queue_hosts: @@ -340,6 +365,11 @@ class ModularBackend(BaseBackend): self._reset_allowed_paths() + @property + def empty_string_hash(self): + return binascii.hexlify(HashMap(self.block_size, + self.hash_algorithm).hash()) + def pre_exec(self, lock_container_path=False): self.lock_container_path = lock_container_path self.wrapper.execute() @@ -392,23 +422,15 @@ class ModularBackend(BaseBackend): @debug_method @backend_method + @list_method def list_accounts(self, user, marker=None, limit=10000): """Return a list of accounts the user can access.""" - allowed = self._allowed_accounts(user) - start, limit = self._list_limits(allowed, marker, limit) - return allowed[start:start + limit] - - def _get_account_quotas(self, account): - """Get account usage from astakos.""" - - quotas = self.astakosclient.service_get_quotas(account)[account] - return quotas.get(DEFAULT_SOURCE, {}).get(DEFAULT_DISKSPACE_RESOURCE, - {}) + return self._allowed_accounts(user) @debug_method @backend_method - def get_account_meta(self, user, account, domain, until=None, + def get_account_meta(self, user, account, domain=None, until=None, include_user_defined=True): """Return a dictionary with the account metadata for the domain.""" @@ -437,21 +459,32 @@ class ModularBackend(BaseBackend): else: meta = {} if props is not None and include_user_defined: + if domain is None: + raise ValueError( + 'Domain argument is obligatory for getting ' + 'user defined metadata') meta.update( dict(self.node.attribute_get(props[self.SERIAL], domain))) if until is not None: meta.update({'until_timestamp': tstamp}) meta.update({'name': account, 'count': count, 'bytes': bytes}) if self.using_external_quotaholder: - external_quota = self._get_account_quotas(account) - meta['bytes'] = external_quota.get('usage', 0) + external_quota = self.astakosclient.service_get_quotas( + account)[account] + meta['bytes'] = sum(d['pithos.diskspace']['usage'] for d in + external_quota.values()) meta.update({'modified': modified}) return meta @debug_method @backend_method def update_account_meta(self, user, account, domain, meta, replace=False): - """Update the metadata associated with the account for the domain.""" + """Update the metadata associated with the account for the domain. + + Raises: + NotAllowedError: Operation not permitted + LimitExceeded: if the metadata number exceeds the allowed limit. + """ self._can_write_account(user, account) path, node = self._lookup_account(account, True) @@ -472,18 +505,38 @@ class ModularBackend(BaseBackend): @debug_method @backend_method def update_account_groups(self, user, account, groups, replace=False): - """Update the groups associated with the account.""" + """Update the groups associated with the account. + Raises: + NotAllowedError: Operation not permitted + ValueError: Invalid data in groups + LimitExceeded: if the group number exceeds the allowed limit or + a group name or a member is too long. + """ + + # assert groups' validity before querying the db + self._check_groups(groups) self._can_write_account(user, account) self._lookup_account(account, True) - self._check_groups(groups) - if replace: - self.permissions.group_destroy(account) - for k, v in groups.iteritems(): - if not replace: # If not already deleted. - self.permissions.group_delete(account, k) - if v: - self.permissions.group_addmany(account, k, v) + if not replace: + existing = self.permissions.group_dict(account) + for k, v in groups.iteritems(): + if v == '': + existing.pop(k, None) + else: + existing[k] = v + groups = existing + + if len(groups) > self.acc_max_groups: + raise LimitExceeded('Pithos+ accounts cannot have more than %s ' + 'groups' % self.acc_max_groups) + for k in groups: + if len(groups[k]) > self.acc_max_group_members: + raise LimitExceeded('Pithos+ groups cannot have more than %s ' + 'members' % self.acc_max_group_members) + + self.permissions.group_destroy(account) + self.permissions.group_addmany(account, groups) @debug_method @backend_method @@ -496,8 +549,14 @@ class ModularBackend(BaseBackend): path, node = self._lookup_account(account, True) policy = self._get_policy(node, is_account_policy=True) if self.using_external_quotaholder: - external_quota = self._get_account_quotas(account) - policy['quota'] = external_quota.get('limit', 0) + policy[QUOTA_POLICY] = 0 + external_quota = self.astakosclient.service_get_quotas( + account)[account] + for k, v in external_quota.items(): + policy['%s-%s' % (QUOTA_POLICY, k)] = \ + v['pithos.diskspace']['limit'] + policy[QUOTA_POLICY] += v['pithos.diskspace']['limit'] + return policy @debug_method @@ -507,24 +566,24 @@ class ModularBackend(BaseBackend): self._can_write_account(user, account) path, node = self._lookup_account(account, True) - self._check_policy(policy, is_account_policy=True) - self._put_policy(node, policy, replace, is_account_policy=True) + self._put_policy(node, policy, replace, is_account_policy=True, + check=True) @debug_method @backend_method def put_account(self, user, account, policy=None): """Create a new account with the given name.""" + self._check_account(account) policy = policy or {} self._can_write_account(user, account) node = self.node.node_lookup(account) if node is not None: raise AccountExists('Account already exists') - if policy: - self._check_policy(policy, is_account_policy=True) node = self._put_path(user, self.ROOTNODE, account, update_statistics_ancestors_depth=-1) - self._put_policy(node, policy, True, is_account_policy=True) + self._put_policy(node, policy, True, is_account_policy=True, + check=True if policy else False) @debug_method @backend_method @@ -546,6 +605,7 @@ class ModularBackend(BaseBackend): @debug_method @backend_method + @list_method def list_containers(self, user, account, marker=None, limit=10000, shared=False, until=None, public=False): """Return a list of containers existing under an account.""" @@ -554,9 +614,7 @@ class ModularBackend(BaseBackend): if user != account: if until: raise NotAllowedError - allowed = self._allowed_containers(user, account) - start, limit = self._list_limits(allowed, marker, limit) - return allowed[start:start + limit] + return self._allowed_containers(user, account) if shared or public: allowed = set() if shared: @@ -565,15 +623,10 @@ class ModularBackend(BaseBackend): if public: allowed.update([x[0].split('/', 2)[1] for x in self.permissions.public_list(account)]) - allowed = sorted(allowed) - start, limit = self._list_limits(allowed, marker, limit) - return allowed[start:start + limit] + return sorted(allowed) node = self.node.node_lookup(account) - containers = [x[0] for x in self._list_object_properties( + return [x[0] for x in self._list_object_properties( node, account, '', '/', marker, limit, False, None, [], until)] - start, limit = self._list_limits( - [x[0] for x in containers], marker, limit) - return containers[start:start + limit] @debug_method @backend_method @@ -594,8 +647,8 @@ class ModularBackend(BaseBackend): @debug_method @backend_method - def get_container_meta(self, user, account, container, domain, until=None, - include_user_defined=True): + def get_container_meta(self, user, account, container, domain=None, + until=None, include_user_defined=True): """Return a dictionary with the container metadata for the domain.""" self._can_read_container(user, account, container) @@ -619,6 +672,10 @@ class ModularBackend(BaseBackend): else: meta = {} if include_user_defined: + if domain is None: + raise ValueError( + 'Domain argument is obligatory for getting ' + 'user defined metadata') meta.update( dict(self.node.attribute_get(props[self.SERIAL], domain))) if until is not None: @@ -631,7 +688,13 @@ class ModularBackend(BaseBackend): @backend_method def update_container_meta(self, user, account, container, domain, meta, replace=False): - """Update the metadata associated with the container for the domain.""" + """Update the metadata associated with the container for the domain. + + Raises: + NotAllowedError: Operation not permitted + ItemNotExists: Container does not exist + LimitExceeded: if the metadata number exceeds the allowed limit. + """ self._can_write_container(user, account, container) path, node = self._lookup_container(account, container) @@ -640,7 +703,7 @@ class ModularBackend(BaseBackend): update_statistics_ancestors_depth=0) if src_version_id is not None: versioning = self._get_policy( - node, is_account_policy=False)['versioning'] + node, is_account_policy=False)[VERSIONING_POLICY] if versioning != 'auto': self.node.version_remove(src_version_id, update_statistics_ancestors_depth=0) @@ -660,12 +723,30 @@ class ModularBackend(BaseBackend): @backend_method def update_container_policy(self, user, account, container, policy, replace=False): - """Update the policy associated with the container.""" + """Update the policy associated with the container. + + Raises: AstakosClientException + """ self._can_write_container(user, account, container) path, node = self._lookup_container(account, container) - self._check_policy(policy, is_account_policy=False) - self._put_policy(node, policy, replace, is_account_policy=False) + + if PROJECT in policy: + from_project = self._get_project(node) + to_project = policy[PROJECT] + provisions = { + (from_project, to_project, 'pithos.diskspace'): + self.get_container_meta( + user, account, container, + include_user_defined=False)['bytes']} + + if self.using_external_quotaholder: + serial = self.astakosclient.issue_resource_reassignment( + holder=account, provisions=provisions) + self.serials.append(serial) + + self._put_policy(node, policy, replace, is_account_policy=False, + default_project=account, check=True) @debug_method @backend_method @@ -680,13 +761,13 @@ class ModularBackend(BaseBackend): pass else: raise ContainerExists('Container already exists') - if policy: - self._check_policy(policy, is_account_policy=False) path = '/'.join((account, container)) node = self._put_path( user, self._lookup_account(account, True)[1], path, update_statistics_ancestors_depth=-1) - self._put_policy(node, policy, True, is_account_policy=False) + self._put_policy(node, policy, True, is_account_policy=False, + default_project=account, + check=True if policy else False) @debug_method @backend_method @@ -696,6 +777,7 @@ class ModularBackend(BaseBackend): self._can_write_container(user, account, container) path, node = self._lookup_container(account, container) + project = self._get_project(node) if until is not None: hashes, size, serials = self.node.node_purge_children( @@ -707,7 +789,7 @@ class ModularBackend(BaseBackend): update_statistics_ancestors_depth=0) if not self.free_versioning: self._report_size_change( - user, account, -size, { + user, account, -size, project, { 'action': 'container purge', 'path': path, 'versions': ','.join(str(i) for i in serials) @@ -728,7 +810,7 @@ class ModularBackend(BaseBackend): self.node.node_remove(node, update_statistics_ancestors_depth=0) if not self.free_versioning: self._report_size_change( - user, account, -size, { + user, account, -size, project, { 'action': 'container purge', 'path': path, 'versions': ','.join(str(i) for i in serials) @@ -742,28 +824,38 @@ class ModularBackend(BaseBackend): size_range=None, all_props=True, public=False, listing_limit=listing_limit) paths = [] + freed_space = 0 + dest_versions = [] for t in src_names: path = '/'.join((account, container, t[0])) node = t[2] if not self._exists(node): continue - src_version_id, dest_version_id = self._put_version_duplicate( - user, node, size=0, type='', hash=None, checksum='', - cluster=CLUSTER_DELETED, - update_statistics_ancestors_depth=1) + + # keep reference to the mapfile + # in case we will want to delete them in the future + src_version_id, dest_version_id, _ = \ + self._put_version_duplicate( + user, node, size=0, type='', hash=None, checksum='', + cluster=CLUSTER_DELETED, + update_statistics_ancestors_depth=1, + keep_src_mapfile=True) + dest_versions.append(dest_version_id) del_size = self._apply_versioning( account, container, src_version_id, update_statistics_ancestors_depth=1) - self._report_size_change( - user, account, -del_size, { - 'action': 'object delete', - 'path': path, - 'versions': ','.join([str(dest_version_id)])}) + freed_space += del_size self._report_object_change( user, account, path, details={'action': 'object delete'}) paths.append(path) self.permissions.access_clear_bulk(paths) + self._report_size_change( + user, account, -freed_space, project, { + 'action': 'object delete', + 'path': '/'.join((account, container, '')), + 'versions': ','.join([str(id_) for id_ in dest_versions])}) + # remove all the cached allowed paths # removing the specific path could be more expensive self._reset_allowed_paths() @@ -773,15 +865,16 @@ class ModularBackend(BaseBackend): size_range, all_props, public): if user != account and until: raise NotAllowedError + + objects = set() if shared and public: # get shared first shared_paths = self._list_object_permissions( user, account, container, prefix, shared=True, public=False) - objects = set() if shared_paths: path, node = self._lookup_container(account, container) shared_paths = self._get_formatted_paths(shared_paths) - objects |= set(self._list_object_properties( + objects = set(self._list_object_properties( node, path, prefix, delimiter, marker, limit, virtual, domain, keys, until, size_range, shared_paths, all_props)) @@ -791,27 +884,22 @@ class ModularBackend(BaseBackend): objects = list(objects) objects.sort(key=lambda x: x[0]) - start, limit = self._list_limits( - [x[0] for x in objects], marker, limit) - return objects[start:start + limit] elif public: objects = self._list_public_object_properties( user, account, container, prefix, all_props) - start, limit = self._list_limits( - [x[0] for x in objects], marker, limit) - return objects[start:start + limit] + else: + allowed = self._list_object_permissions( + user, account, container, prefix, shared, public=False) + if shared and not allowed: + return [] + path, node = self._lookup_container(account, container) + allowed = self._get_formatted_paths(allowed) + objects = self._list_object_properties( + node, path, prefix, delimiter, marker, limit, virtual, domain, + keys, until, size_range, allowed, all_props) - allowed = self._list_object_permissions( - user, account, container, prefix, shared, public) - if shared and not allowed: - return [] - path, node = self._lookup_container(account, container) - allowed = self._get_formatted_paths(allowed) - objects = self._list_object_properties( - node, path, prefix, delimiter, marker, limit, virtual, domain, - keys, until, size_range, allowed, all_props) - start, limit = self._list_limits( - [x[0] for x in objects], marker, limit) + # apply limits + start, limit = self._list_limits(objects, marker, limit) return objects[start:start + limit] def _list_public_object_properties(self, user, account, container, prefix, @@ -904,7 +992,9 @@ class ModularBackend(BaseBackend): 'modified': p[self.MTIME + 1] if until is None else None, 'modified_by': p[self.MUSER + 1], 'uuid': p[self.UUID + 1], - 'checksum': p[self.CHECKSUM + 1]}) + 'checksum': p[self.CHECKSUM + 1], + 'available': p[self.AVAILABLE + 1], + 'map_check_timestamp': p[self.MAP_CHECK_TIMESTAMP + 1]}) return objects @debug_method @@ -929,7 +1019,7 @@ class ModularBackend(BaseBackend): @debug_method @backend_method - def get_object_meta(self, user, account, container, name, domain, + def get_object_meta(self, user, account, container, name, domain=None, version=None, include_user_defined=True): """Return a dictionary with the object metadata for the domain.""" @@ -937,6 +1027,14 @@ class ModularBackend(BaseBackend): path, node = self._lookup_object(account, container, name) props = self._get_version(node, version) if version is None: + if props[self.AVAILABLE] == MAP_UNAVAILABLE: + try: + self._update_available(props) + except IllegalOperationError: + pass # just update the database + finally: + # get updated properties + props = self._get_version(node, version) modified = props[self.MTIME] else: try: @@ -951,6 +1049,10 @@ class ModularBackend(BaseBackend): meta = {} if include_user_defined: + if domain is None: + raise ValueError( + 'Domain argument is obligatory for getting ' + 'user defined metadata') meta.update( dict(self.node.attribute_get(props[self.SERIAL], domain))) meta.update({'name': name, @@ -962,14 +1064,24 @@ class ModularBackend(BaseBackend): 'modified': modified, 'modified_by': props[self.MUSER], 'uuid': props[self.UUID], - 'checksum': props[self.CHECKSUM]}) + 'checksum': props[self.CHECKSUM], + 'available': props[self.AVAILABLE], + 'map_check_timestamp': props[self.MAP_CHECK_TIMESTAMP], + 'mapfile': props[self.MAPFILE], + 'is_snapshot': props[self.IS_SNAPSHOT]}) return meta @debug_method @backend_method def update_object_meta(self, user, account, container, name, domain, meta, replace=False): - """Update object metadata for a domain and return the new version.""" + """Update object metadata for a domain and return the new version. + + Raises: + NotAllowedError: Operation not permitted + ItemNotExists: Container/object does not exist + LimitExceeded: if the metadata number exceeds the allowed limit. + """ self._can_write_object(user, account, container, name) @@ -978,6 +1090,8 @@ class ModularBackend(BaseBackend): src_version_id, dest_version_id = self._put_metadata( user, node, domain, meta, replace, update_statistics_ancestors_depth=1) + self._copy_metadata(src_version_id, dest_version_id, node, + exclude_domain=domain, src_node=node) self._apply_versioning(account, container, src_version_id, update_statistics_ancestors_depth=1) return dest_version_id @@ -1082,23 +1196,82 @@ class ModularBackend(BaseBackend): self.permissions.public_set( path, self.public_url_security, self.public_url_alphabet) + def _update_available(self, props): + """Checks if the object map exists and updates the database""" + + if props[self.AVAILABLE] == MAP_ERROR: + raise BrokenSnapshot('This Archipelago volume is broken.') + + if props[self.AVAILABLE] == MAP_UNAVAILABLE: + if props[self.MAP_CHECK_TIMESTAMP]: + elapsed_time = time() - float(props[self.MAP_CHECK_TIMESTAMP]) + if elapsed_time < self.map_check_interval: + raise IllegalOperationError( + 'Unable to retrieve Archipelago volume hashmap') + try: + hashmap = self.store.map_get(props[self.HASH], props[self.SIZE]) + except: # map does not exist + # Raising an exception results in db transaction rollback + # However we have to force the update of the database + self.wrapper.rollback() # rollback existing transaction + self.wrapper.execute() # start new transaction + self.node.version_put_property(props[self.SERIAL], + 'map_check_timestamp', time()) + self.wrapper.commit() # commit transaction + self.wrapper.execute() # start new transaction + raise IllegalOperationError( + 'Unable to retrieve Archipelago volume hashmap') + else: # map exists + self.node.version_put_property(props[self.SERIAL], + 'available', MAP_AVAILABLE) + self.node.version_put_property(props[self.SERIAL], + 'map_check_timestamp', time()) + return hashmap + + def _get_object_hashmap(self, props, update_available=True): + if props[self.HASH] is None: + return [] + if props[self.IS_SNAPSHOT]: + if update_available: + return self._update_available(props) + else: + size = props[self.SIZE] + if size == 0: + return [self.empty_string_hash] + return self.store.map_get(props[self.MAPFILE], props[self.SIZE]) + @debug_method @backend_method def get_object_hashmap(self, user, account, container, name, version=None): """Return the object's size and a list with partial hashes.""" - self._can_read_object(user, account, container, name) path, node = self._lookup_object(account, container, name) props = self._get_version(node, version) - if props[self.HASH] is None: - return 0, () - hashmap = self.store.map_get(self._unhexlify_hash(props[self.HASH])) - return props[self.SIZE], [binascii.hexlify(x) for x in hashmap] + return props[self.IS_SNAPSHOT], props[self.SIZE], \ + self._get_object_hashmap(props, update_available=True) + + def _copy_metadata(self, src_version, dest_version, dest_node, + exclude_domain, src_node=None): + domains = self.node.attribute_get_domains(src_version, + node=src_node) + try: + domains.remove(exclude_domain) + except ValueError: # domain is not in the list + pass + + for d in domains: + existing = dict(self.node.attribute_get(src_version, d)) + self._put_metadata_duplicate( + src_version, dest_version, d, dest_node, meta=existing, + replace=True) def _update_object_hash(self, user, account, container, name, size, type, hash, checksum, domain, meta, replace_meta, permissions, src_node=None, src_version_id=None, - is_copy=False, report_size_change=True): + is_copy=False, report_size_change=True, + available=None, keep_available=False, + force_mapfile=None, is_snapshot=False): + available = available if available is not None else MAP_AVAILABLE if permissions is not None and user != account: raise NotAllowedError self._can_write_object(user, account, container, name) @@ -1109,17 +1282,22 @@ class ModularBackend(BaseBackend): account_path, account_node = self._lookup_account(account, True) container_path, container_node = self._lookup_container( account, container) + project = self._get_project(container_node) path, node = self._put_object_node( container_path, container_node, name) - pre_version_id, dest_version_id = self._put_version_duplicate( + pre_version_id, dest_version_id, mapfile = self._put_version_duplicate( user, node, src_node=src_node, size=size, type=type, hash=hash, checksum=checksum, is_copy=is_copy, - update_statistics_ancestors_depth=1) + update_statistics_ancestors_depth=1, + available=available, keep_available=keep_available, + force_mapfile=force_mapfile, is_snapshot=is_snapshot) # Handle meta. if src_version_id is None: src_version_id = pre_version_id + self._copy_metadata(src_version_id, dest_version_id, node, + exclude_domain=domain, src_node=src_node) self._put_metadata_duplicate( src_version_id, dest_version_id, domain, node, meta, replace_meta) @@ -1130,7 +1308,7 @@ class ModularBackend(BaseBackend): # Check account quota. if not self.using_external_quotaholder: account_quota = long(self._get_policy( - account_node, is_account_policy=True)['quota']) + account_node, is_account_policy=True)[QUOTA_POLICY]) account_usage = self._get_statistics(account_node, compute=True)[1] if (account_quota > 0 and account_usage > account_quota): @@ -1140,7 +1318,7 @@ class ModularBackend(BaseBackend): # Check container quota. container_quota = long(self._get_policy( - container_node, is_account_policy=False)['quota']) + container_node, is_account_policy=False)[QUOTA_POLICY]) container_usage = self._get_statistics(container_node)[1] if (container_quota > 0 and container_usage > container_quota): # This must be executed in a transaction, so the version is @@ -1153,7 +1331,7 @@ class ModularBackend(BaseBackend): if report_size_change: self._report_size_change( - user, account, size_delta, + user, account, size_delta, project, {'action': 'object update', 'path': path, 'versions': ','.join([str(dest_version_id)])}) if permissions is not None: @@ -1165,32 +1343,133 @@ class ModularBackend(BaseBackend): self._report_object_change( user, account, path, details={'version': dest_version_id, 'action': 'object update'}) - return dest_version_id + return dest_version_id, size_delta, mapfile + + @debug_method + @backend_method + def register_object_map(self, user, account, container, name, size, type, + mapfile, checksum='', domain='pithos', meta=None, + replace_meta=False, permissions=None): + """Register an object mapfile without providing any data. + + Lock the container path, create a node pointing to the object path, + create a version pointing to the mapfile + and issue the size change in the quotaholder. + + :param user: the user account which performs the action + + :param account: the account under which the object resides + + :param container: the container under which the object resides + + :param name: the object name + + :param size: the object size + + :param type: the object mimetype + + :param mapfile: the mapfile pointing to the object data + + :param checkcum: the md5 checksum (optional) + + :param domain: the object domain + + :param meta: a dict with custom object metadata + + :param replace_meta: replace existing metadata or not + + :param permissions: a dict with the read and write object permissions + + :returns: the new object uuid + + :raises: ItemNotExists, NotAllowedError, QuotaError, + AstakosClientException, LimitExceeded + """ + + meta = meta or {} + try: + self.lock_container_path = True + self.put_container(user, account, container, policy=None) + except ContainerExists: + pass + finally: + self.lock_container_path = False + dest_version_id, _, mapfile = self._update_object_hash( + user, account, container, name, size, type, mapfile, checksum, + domain, meta, replace_meta, permissions, available=MAP_UNAVAILABLE, + force_mapfile=mapfile, is_snapshot=True) + return self.node.version_get_properties(dest_version_id, + keys=('uuid',))[0] + + @debug_method + @backend_method + def update_object_status(self, uuid, state): + assert state in (MAP_ERROR, + MAP_UNAVAILABLE, + MAP_AVAILABLE), 'Invalid mapfile state' + uuid_ = self._validate_uuid(uuid) + info = self.node.latest_uuid(uuid_, CLUSTER_NORMAL) + if info is None: + raise NameError('No object found for this UUID.') + _, serial = info + self.node.version_put_property(serial, 'available', state) @debug_method def update_object_hashmap(self, user, account, container, name, size, type, hashmap, checksum, domain, meta=None, replace_meta=False, permissions=None): - """Create/update an object's hashmap and return the new version.""" + """Create/update an object's hashmap and return the new version. + + Raises: + NotAllowedError: Operation not permitted + + ItemNotExists: Container does not exist + + ValueError: Invalid users/groups in permissions + + QuotaError: Account or container quota exceeded + + LimitExceeded: if the metadata number exceeds the allowed limit. + """ + + if not self._size_is_consistent(size, hashmap): + raise InconsistentContentSize( + 'The object\'s size does not match ' + 'with the object\'s hashmap length') + try: + path, node = self._lookup_object(account, container, name, + lock_container=True) + except: + pass + else: + try: + props = self._get_version(node) + except ItemNotExists: + pass + else: + if props[self.IS_SNAPSHOT]: + raise IllegalOperationError( + 'Cannot update Archipelago volume hashmap.') meta = meta or {} if size == 0: # No such thing as an empty hashmap. hashmap = [self.put_block('')] - map = HashMap(self.block_size, self.hash_algorithm) - map.extend([self._unhexlify_hash(x) for x in hashmap]) - missing = self.store.block_search(map) + map_ = HashMap(self.block_size, self.hash_algorithm) + map_.extend([self._unhexlify_hash(x) for x in hashmap]) + missing = self.store.block_search(map_) if missing: ie = IndexError() ie.data = [binascii.hexlify(x) for x in missing] raise ie - hash = map.hash() - hexlified = binascii.hexlify(hash) + hash_ = map_.hash() + hexlified = binascii.hexlify(hash_) # _update_object_hash() locks destination path - dest_version_id = self._update_object_hash( + dest_version_id, _, mapfile = self._update_object_hash( user, account, container, name, size, type, hexlified, checksum, - domain, meta, replace_meta, permissions) - self.store.map_put(hash, map) + domain, meta, replace_meta, permissions, is_snapshot=False) + if size != 0: + self.store.map_put(mapfile, hashmap, size, self.block_size) return dest_version_id, hexlified @debug_method @@ -1219,39 +1498,87 @@ class ModularBackend(BaseBackend): permissions=None, src_version=None, is_move=False, delimiter=None, listing_limit=10000): - report_size_change = not is_move dest_meta = dest_meta or {} - dest_version_ids = [] + dest_versions = [] + freed_space = 0 + occupied_space = 0 self._can_read_object(user, src_account, src_container, src_name) src_container_path = '/'.join((src_account, src_container)) dest_container_path = '/'.join((dest_account, dest_container)) # Lock container paths in alphabetical order if src_container_path < dest_container_path: - self._lookup_container(src_account, src_container) - self._lookup_container(dest_account, dest_container) + src_container_node = self._lookup_container(src_account, + src_container)[-1] + dest_container_node = self._lookup_container(dest_account, + dest_container)[-1] else: - self._lookup_container(dest_account, dest_container) - self._lookup_container(src_account, src_container) + dest_container_node = self._lookup_container(dest_account, + dest_container)[-1] + src_container_node = self._lookup_container(src_account, + src_container)[-1] + + cross_account = src_account != dest_account + cross_container = src_container != dest_container + src_project = None # compute it only if it is necessary + dest_project = self._get_project(dest_container_node) + + cross_project = False + if cross_container: + src_project = self._get_project(src_container_node) + cross_project = src_project != dest_project + + # do not perform bulk report size change in the other cases in order to + # catch early failures due to quota restrictions + bulk_report_size_change = is_move and not (cross_account or + cross_project) path, node = self._lookup_object(src_account, src_container, src_name) # TODO: Will do another fetch of the properties in duplicate version... props = self._get_version( - node, src_version) # Check to see if source exists. + node, src_version, + keys=_propnames) # Check to see if source exists. src_version_id = props[self.SERIAL] hash = props[self.HASH] size = props[self.SIZE] + is_snapshot = props[self.IS_SNAPSHOT] is_copy = not is_move and (src_account, src_container, src_name) != ( dest_account, dest_container, dest_name) # New uuid. - dest_version_ids.append(self._update_object_hash( + + if is_copy and props[self.AVAILABLE] != MAP_AVAILABLE: + raise NotAllowedError('Copying objects not available in the ' + 'storage backend is forbidden.') + + src_mapfile = props[self.MAPFILE] + force_mapfile = src_mapfile if not is_copy else None + + dest_version_id, size_delta, dest_mapfile = self._update_object_hash( user, dest_account, dest_container, dest_name, size, type, hash, None, dest_domain, dest_meta, replace_meta, permissions, src_node=node, src_version_id=src_version_id, is_copy=is_copy, - report_size_change=report_size_change)) - if is_move and ((src_account, src_container, src_name) != - (dest_account, dest_container, dest_name)): - self._delete_object(user, src_account, src_container, src_name, - report_size_change=report_size_change) + report_size_change=(not bulk_report_size_change), + keep_available=True, is_snapshot=is_snapshot, + force_mapfile=force_mapfile) + + # store destination mapfile + if size != 0 and src_mapfile != dest_mapfile: + try: + hashmap = self._get_object_hashmap(props, + update_available=False) + except: + raise NotAllowedError("Copy is not permitted: failed to get " + "source object's mapfile: %s" % + src_mapfile) + self.store.map_put(dest_mapfile, hashmap, size, self.block_size) + + dest_versions.append(dest_version_id) + occupied_space += size_delta + if is_move and (src_account, src_container, src_name) != ( + dest_account, dest_container, dest_name): + del_size = self._delete_object( + user, src_account, src_container, src_name, + report_size_change=(not bulk_report_size_change)) + freed_space += del_size if delimiter: prefix = (src_name + delimiter if not @@ -1266,29 +1593,67 @@ class ModularBackend(BaseBackend): nodes = [elem[2] for elem in src_names] # TODO: Will do another fetch of the properties # in duplicate version... - props = self._get_versions(nodes) # Check to see if source exists. + props = self._get_versions(nodes) for prop, path, node in zip(props, paths, nodes): src_version_id = prop[self.SERIAL] hash = prop[self.HASH] vtype = prop[self.TYPE] size = prop[self.SIZE] + is_snapshot = prop[self.IS_SNAPSHOT] dest_prefix = dest_name + delimiter if not dest_name.endswith( delimiter) else dest_name vdest_name = path.replace(prefix, dest_prefix, 1) + + if is_copy and prop[self.AVAILABLE] != MAP_AVAILABLE: + raise NotAllowedError('Copying objects not available in ' + 'the storage backend is forbidden.') + + src_mapfile = prop[self.MAPFILE] + force_mapfile = src_mapfile if not is_copy else None + # _update_object_hash() locks destination path - dest_version_ids.append(self._update_object_hash( - user, dest_account, dest_container, vdest_name, size, - vtype, hash, None, dest_domain, meta={}, - replace_meta=False, permissions=None, src_node=node, - src_version_id=src_version_id, is_copy=is_copy, - report_size_change=report_size_change)) - if is_move and ((src_account, src_container, src_name) != - (dest_account, dest_container, dest_name)): - self._delete_object(user, src_account, src_container, path, - report_size_change=report_size_change) - return (dest_version_ids[0] if len(dest_version_ids) == 1 else - dest_version_ids) + dest_version_id, size_delta, dest_mapfile = \ + self._update_object_hash( + user, dest_account, dest_container, vdest_name, size, + vtype, hash, None, dest_domain, meta={}, + replace_meta=False, permissions=None, src_node=node, + src_version_id=src_version_id, is_copy=is_copy, + report_size_change=(not bulk_report_size_change), + keep_available=True, is_snapshot=is_snapshot, + force_mapfile=force_mapfile) + + # store destination mapfile + if size != 0 and src_mapfile != dest_mapfile: + try: + hashmap = self._get_object_hashmap( + prop, update_available=False) + except: + raise NotAllowedError( + "Copy is not permitted: failed to get " + "source object's mapfile: %s" % src_mapfile) + self.store.map_put(dest_mapfile, hashmap, size, + self.block_size) + + dest_versions.append(dest_version_id) + occupied_space += size_delta + if is_move and (src_account, src_container, path) != ( + dest_account, dest_container, vdest_name): + del_size = self._delete_object( + user, src_account, src_container, path, + report_size_change=(not bulk_report_size_change)) + freed_space += del_size + + if bulk_report_size_change: # bulk report size change + dest_obj_path = '/'.join((dest_container_path, dest_name)) + size_delta = occupied_space - freed_space + self._report_size_change( + user, dest_account, size_delta, dest_project, + details={'action': 'object move', + 'path': dest_obj_path, + 'versions': ','.join([str(id_) for id_ in + dest_versions])}) + return dest_versions @debug_method @backend_method @@ -1296,15 +1661,25 @@ class ModularBackend(BaseBackend): dest_account, dest_container, dest_name, type, domain, meta=None, replace_meta=False, permissions=None, src_version=None, delimiter=None, listing_limit=None): - """Copy an object's data and metadata.""" + """Copy an object's data and metadata. + + Raises: + NotAllowedError: Operation not permitted + ItemNotExists: Container/object does not exist + VersionNotExists: Version does not exist + ValueError: Invalid users/groups in permissions + QuotaError: Account or container quota exceeded + LimitExceeded: if the metadata number exceeds the allowed limit. + """ meta = meta or {} - dest_version_id = self._copy_object( + dest_versions = self._copy_object( user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, domain, meta, replace_meta, permissions, src_version, False, delimiter, listing_limit=listing_limit) - return dest_version_id + # propagate only the first version created + return dest_versions[0] if dest_versions >= 1 else None @debug_method @backend_method @@ -1330,9 +1705,11 @@ class ModularBackend(BaseBackend): if user != account: raise NotAllowedError - # lookup object and lock container path also - path, node = self._lookup_object(account, container, name, - lock_container=True) + # lock container path + container_path, container_node = self._lookup_container(account, + container) + project = self._get_project(container_node) + path, node = self._lookup_object(account, container, name) if until is not None: if node is None: @@ -1360,28 +1737,28 @@ class ModularBackend(BaseBackend): except NameError: self.permissions.access_clear(path) self._report_size_change( - user, account, -size, { + user, account, -size, project, { 'action': 'object purge', 'path': path, 'versions': ','.join(str(i) for i in serials) } ) - return + return size if not self._exists(node): raise ItemNotExists('Object is deleted.') - src_version_id, dest_version_id = self._put_version_duplicate( + # keep reference to the mapfile + # in case we will want to delete them in the future + src_version_id, dest_version_id, _ = self._put_version_duplicate( user, node, size=0, type='', hash=None, checksum='', - cluster=CLUSTER_DELETED, update_statistics_ancestors_depth=1) + cluster=CLUSTER_DELETED, update_statistics_ancestors_depth=1, + keep_src_mapfile=True) del_size = self._apply_versioning(account, container, src_version_id, update_statistics_ancestors_depth=1) - if report_size_change: - self._report_size_change( - user, account, -del_size, - {'action': 'object delete', - 'path': path, - 'versions': ','.join([str(dest_version_id)])}) + + freed_space = del_size + dest_versions = [] self._report_object_change( user, account, path, details={'action': 'object delete'}) self.permissions.access_clear(path) @@ -1399,27 +1776,39 @@ class ModularBackend(BaseBackend): node = t[2] if not self._exists(node): continue - src_version_id, dest_version_id = self._put_version_duplicate( - user, node, size=0, type='', hash=None, checksum='', - cluster=CLUSTER_DELETED, - update_statistics_ancestors_depth=1) + + # keep reference to the mapfile + # in case we will want to delete them in the future + src_version_id, dest_version_id, _ = \ + self._put_version_duplicate( + user, node, size=0, type='', hash=None, checksum='', + cluster=CLUSTER_DELETED, + update_statistics_ancestors_depth=1, + keep_src_mapfile=True) del_size = self._apply_versioning( account, container, src_version_id, update_statistics_ancestors_depth=1) - if report_size_change: - self._report_size_change( - user, account, -del_size, - {'action': 'object delete', - 'path': path, - 'versions': ','.join([str(dest_version_id)])}) + freed_space += del_size + dest_versions.append(dest_version_id) self._report_object_change( user, account, path, details={'action': 'object delete'}) paths.append(path) self.permissions.access_clear_bulk(paths) + if report_size_change: + path = '/'.join([account, container, name]) + if delimiter: + path += '/' + self._report_size_change( + user, account, -freed_space, project, + {'action': 'object delete', + 'path': path, + 'versions': ','.join([str(id_) for id_ in dest_versions])}) + # remove all the cached allowed paths # removing the specific path could be more expensive self._reset_allowed_paths() + return freed_space @debug_method @backend_method @@ -1455,6 +1844,72 @@ class ModularBackend(BaseBackend): self._can_read_object(user, account, container, name) return (account, container, name) + @debug_method + @backend_method + def delete_by_uuid(self, user, uuid): + """Delete the object having the specific UUID. + + Args: + user: the user performing the action + uuid: the object's UUID (a string accepted by the uuid.UUID() + constructor) + Raises: + ValueError: the provided UUID is invalid. + NameError: no object is identified by the specific UUID. + NotAllowedError: the user has no write permission for the + specific object. + """ + + uuid_ = self._validate_uuid(uuid) + info = self.node.latest_uuid(uuid_, CLUSTER_NORMAL) + if info is None: + raise NameError('No object found for this UUID.') + path, serial = info + account, container, name = path.split('/', 2) + self._delete_object(user, account, container, name) + + @debug_method + @backend_method + def get_object_by_uuid(self, uuid, version=None, domain='pithos', + user=None, check_permissions=True): + """Return information for the object identified by the specific UUID + + Raises: + NameError: UUID or version was not found + NotAllowedError: if check_permissions is True and user has not + access to the object + AssertionError: if check_permissions is True but user + is provided + """ + if user is not None and not check_permissions: + raise AssertionError('Inconsistent argument combination:' + 'if user is provided ' + 'permission check should be enforced.') + + uuid_ = self._validate_uuid(uuid) + if version is None: + props = self.node.latest_uuid(uuid_, CLUSTER_NORMAL) + if props is None: + raise NameError('No object found for this UUID.') + path, _ = props + else: + props = self.node.version_get_properties(version, + keys=('uuid', 'node')) + if not props: + raise NameError('No such version was found.') + uuid_, node = props + assert uuid_ == uuid + _, path = self.node.node_get_properties(node) + account, container, name = path.split('/', 2) + if check_permissions: + self._can_read_object(user, account, container, name) + user_ = user if user is not None else account + meta = self.get_object_meta(user_, account, container, name, + domain=domain, version=version, + include_user_defined=True) + perms = self.permissions.access_get(path) + return meta, perms, path + @debug_method @backend_method def get_public(self, user, public): @@ -1471,7 +1926,7 @@ class ModularBackend(BaseBackend): """Return a block's data.""" logger.debug("get_block: %s", hash) - block = self.store.block_get(self._unhexlify_hash(hash)) + block = self.store.block_get_archipelago(hash) if not block: raise ItemNotExists('Block does not exist') return block @@ -1482,10 +1937,14 @@ class ModularBackend(BaseBackend): logger.debug("put_block: %s", len(data)) return binascii.hexlify(self.store.block_put(data)) - def update_block(self, hash, data, offset=0): + def update_block(self, hash, data, offset=0, is_snapshot=False): """Update a known block and return the hash.""" - logger.debug("update_block: %s %s %s", hash, len(data), offset) + logger.debug("update_block: %s %s %s %s", hash, len(data), + is_snapshot, offset) + if is_snapshot: + raise IllegalOperationError( + 'Cannot update an Archipelago volume block.') if offset == 0 and len(data) == self.block_size: return self.put_block(data) h = self.store.block_update(self._unhexlify_hash(hash), offset, data) @@ -1496,6 +1955,16 @@ class ModularBackend(BaseBackend): def _generate_uuid(self): return str(uuidlib.uuid4()) + def _validate_uuid(self, uuid): + if not isinstance(uuid, basestring): + raise ValueError('A string value is expected for UUID.') + try: + uuid = uuidlib.UUID(uuid) + except: + raise ValueError('Invalid UUID value.') + prefix = 'urn:uuid:' + return uuid.urn[len(prefix):] + def _put_object_node(self, path, parent, name): path = '/'.join((path, name)) node = self.node.node_lookup(path) @@ -1505,15 +1974,21 @@ class ModularBackend(BaseBackend): def _put_path(self, user, parent, path, update_statistics_ancestors_depth=None): - node = self.node.node_create(parent, path) - self.node.version_create(node, None, 0, '', None, user, - self._generate_uuid(), '', CLUSTER_NORMAL, - update_statistics_ancestors_depth) + try: + node = self.node.node_create(parent, path) + except ValueError: # integrity error + node = self.node.node_lookup(path) + else: + self.node.version_create(node, None, 0, '', None, user, + self._generate_uuid(), '', CLUSTER_NORMAL, + update_statistics_ancestors_depth) return node def _lookup_account(self, account, create=True): node = self.node.node_lookup(account) if node is None and create: + self._check_account(account) + node = self._put_path( account, self.ROOTNODE, account, update_statistics_ancestors_depth=-1) # User is account. @@ -1566,9 +2041,10 @@ class ModularBackend(BaseBackend): stats = (0, 0, 0) return stats - def _get_version(self, node, version=None): + def _get_version(self, node, version=None, keys=()): if version is None: - props = self.node.version_lookup(node, inf, CLUSTER_NORMAL) + props = self.node.version_lookup(node, inf, CLUSTER_NORMAL, + keys=keys) if props is None: raise ItemNotExists('Object does not exist') else: @@ -1576,34 +2052,70 @@ class ModularBackend(BaseBackend): version = int(version) except ValueError: raise VersionNotExists('Version does not exist') - props = self.node.version_get_properties(version, node=node) + props = self.node.version_get_properties(version, node=node, + keys=keys) if props is None or props[self.CLUSTER] == CLUSTER_DELETED: raise VersionNotExists('Version does not exist') return props - def _get_versions(self, nodes): - return self.node.version_lookup_bulk(nodes, inf, CLUSTER_NORMAL) + def _get_versions(self, nodes, keys=()): + return self.node.version_lookup_bulk(nodes, inf, CLUSTER_NORMAL, + keys=keys) def _put_version_duplicate(self, user, node, src_node=None, size=None, type=None, hash=None, checksum=None, cluster=CLUSTER_NORMAL, is_copy=False, - update_statistics_ancestors_depth=None): - """Create a new version of the node.""" - + update_statistics_ancestors_depth=None, + available=None, keep_available=True, + keep_src_mapfile=False, + force_mapfile=None, + is_snapshot=False): + """Create a new version of the node. + + If force_mapfile is not None, mapfile is set to this value. + Otherwise: + If keep_src_mapfile is True the new version will inherit + the mapfile of the source version (if such exists). + This is desirable for metadata updates and delete operations. + + If keep_src_mapfile is False (or source version does not exist) + the new version will be associated with a new mapfile. + + :raises ValueError: if it failed to create the new version + """ + available = available if available is not None else MAP_AVAILABLE props = self.node.version_lookup( - node if src_node is None else src_node, inf, CLUSTER_NORMAL) + node if src_node is None else src_node, inf, CLUSTER_NORMAL, + keys=_propnames) + if props is not None: src_version_id = props[self.SERIAL] src_hash = props[self.HASH] src_size = props[self.SIZE] src_type = props[self.TYPE] src_checksum = props[self.CHECKSUM] + src_is_snapshot = props[self.IS_SNAPSHOT] + if keep_available: + src_available = props[self.AVAILABLE] + src_map_check_timestamp = props[self.MAP_CHECK_TIMESTAMP] + else: + src_available = available + src_map_check_timestamp = None + + if keep_src_mapfile: + src_mapfile = props[self.MAPFILE] + else: + src_mapfile = None else: src_version_id = None src_hash = None src_size = 0 src_type = '' src_checksum = '' + src_is_snapshot = is_snapshot + src_available = available + src_map_check_timestamp = None + src_mapfile = None if size is None: # Set metadata. hash = src_hash # This way hash can be set to None # (account or container). @@ -1626,36 +2138,55 @@ class ModularBackend(BaseBackend): self.node.version_recluster(pre_version_id, CLUSTER_HISTORY, update_statistics_ancestors_depth) - dest_version_id, mtime = self.node.version_create( - node, hash, size, type, src_version_id, user, uuid, checksum, - cluster, update_statistics_ancestors_depth) + mapfile = force_mapfile if force_mapfile is not None else src_mapfile + try: + dest_version_id, _, mapfile = self.node.version_create( + node, hash, size, type, src_version_id, user, uuid, checksum, + cluster, update_statistics_ancestors_depth, + available=src_available, + map_check_timestamp=src_map_check_timestamp, + mapfile=mapfile, + is_snapshot=src_is_snapshot) + except Exception, e: + logger.exception(e) + # TODO handle failures + raise ValueError self.node.attribute_unset_is_latest(node, dest_version_id) - return pre_version_id, dest_version_id + return pre_version_id, dest_version_id, mapfile def _put_metadata_duplicate(self, src_version_id, dest_version_id, domain, node, meta, replace=False): - if src_version_id is not None: - self.node.attribute_copy(src_version_id, dest_version_id) if not replace: - self.node.attribute_del(dest_version_id, domain, ( - k for k, v in meta.iteritems() if v == '')) - self.node.attribute_set(dest_version_id, domain, node, ( - (k, v) for k, v in meta.iteritems() if v != '')) - else: - self.node.attribute_del(dest_version_id, domain) - self.node.attribute_set(dest_version_id, domain, node, (( - k, v) for k, v in meta.iteritems())) + if src_version_id is not None: + existing = dict(self.node.attribute_get(src_version_id, + domain)) + else: + existing = {} + for k, v in meta.iteritems(): + if v == '': + existing.pop(k, None) + else: + existing[k] = v + meta = existing + + if len(meta) > self.resource_max_metadata: + raise LimitExceeded('Pithos+ resources cannot have more than %s ' + 'metadata items per domain' % + self.resource_max_metadata) + + self.node.attribute_set(dest_version_id, domain, node, meta) def _put_metadata(self, user, node, domain, meta, replace=False, update_statistics_ancestors_depth=None): """Create a new version and store metadata.""" - src_version_id, dest_version_id = self._put_version_duplicate( + ustad = update_statistics_ancestors_depth # for pep8 repression + src_version_id, dest_version_id, _ = self._put_version_duplicate( user, node, - update_statistics_ancestors_depth=update_statistics_ancestors_depth - ) + update_statistics_ancestors_depth=ustad, + keep_src_mapfile=True) self._put_metadata_duplicate( src_version_id, dest_version_id, domain, node, meta, replace) return src_version_id, dest_version_id @@ -1697,7 +2228,12 @@ class ModularBackend(BaseBackend): @debug_method @backend_method - def _report_size_change(self, user, account, size, details=None): + def _report_size_change(self, user, account, size, source, details=None): + """Report quota modifications. + + Raises: AstakosClientException + """ + details = details or {} if size == 0: @@ -1713,17 +2249,12 @@ class ModularBackend(BaseBackend): if not self.using_external_quotaholder: return - try: - name = details['path'] if 'path' in details else '' - serial = self.astakosclient.issue_one_commission( - holder=account, - source=DEFAULT_SOURCE, - provisions={'pithos.diskspace': size}, - name=name) - except BaseException, e: - raise QuotaError(e) - else: - self.serials.append(serial) + name = details['path'] if 'path' in details else '' + serial = self.astakosclient.issue_one_commission( + holder=account, + provisions={(source, 'pithos.diskspace'): size}, + name=name) + self.serials.append(serial) @debug_method @backend_method @@ -1745,39 +2276,71 @@ class ModularBackend(BaseBackend): # Policy functions. - def _check_policy(self, policy, is_account_policy=True): - default_policy = self.default_account_policy \ - if is_account_policy else self.default_container_policy - for k in policy.keys(): - if policy[k] == '': - policy[k] = default_policy.get(k) + def _check_project(self, value): + # raise InvalidPolicy('Bad quota source policy') + pass + + def _check_policy(self, policy): for k, v in policy.iteritems(): - if k == 'quota': - q = int(v) # May raise ValueError. + if k == QUOTA_POLICY: + error_msg = ('The quota policy value ' + 'should be a positive integer.') + try: + q = int(v) # May raise ValueError. + except ValueError: + raise InvalidPolicy(error_msg) if q < 0: - raise ValueError - elif k == 'versioning': + raise InvalidPolicy(error_msg) + elif k == VERSIONING_POLICY: if v not in ['auto', 'none']: - raise ValueError + raise InvalidPolicy('The versioning policy value should ' + 'be either \'auto\' or \'none\'.') + elif k == PROJECT: + self._check_project(v) else: - raise ValueError + raise InvalidPolicy('The only allowed policies are ' + '\'quota\' or \'versioning\'.') - def _put_policy(self, node, policy, replace, is_account_policy=True): - default_policy = self.default_account_policy \ - if is_account_policy else self.default_container_policy + def _get_default_policy(self, node=None, is_account_policy=True, + default_project=None): + if is_account_policy: + default_policy = self.default_account_policy + else: + default_policy = self.default_container_policy + if default_project is None and node is not None: + # set container's account as the default quota source + default_project = self.node.node_get_parent_path(node) + default_policy[PROJECT] = default_project + return default_policy + + def _put_policy(self, node, policy, replace, + is_account_policy=True, default_project=None, + check=True): + default_policy = self._get_default_policy(node, + is_account_policy, + default_project) if replace: for k, v in default_policy.iteritems(): if k not in policy: policy[k] = v + if check: + self._check_policy(policy) + self.node.policy_set(node, policy) - def _get_policy(self, node, is_account_policy=True): - default_policy = self.default_account_policy \ - if is_account_policy else self.default_container_policy + def _get_policy(self, node, is_account_policy=True, + default_project=None): + default_policy = self._get_default_policy(node, + is_account_policy, + default_project) policy = default_policy.copy() policy.update(self.node.policy_get(node)) return policy + def _get_project(self, node): + policy = self._get_policy(node, is_account_policy=False) + return policy[PROJECT] + def _apply_versioning(self, account, container, version_id, update_statistics_ancestors_depth=None): """Delete the provided version if such is the policy. @@ -1788,7 +2351,7 @@ class ModularBackend(BaseBackend): return 0 path, node = self._lookup_container(account, container) versioning = self._get_policy( - node, is_account_policy=False)['versioning'] + node, is_account_policy=False)[VERSIONING_POLICY] if versioning != 'auto': hash, size = self.node.version_remove( version_id, update_statistics_ancestors_depth) @@ -1801,9 +2364,20 @@ class ModularBackend(BaseBackend): # Access control functions. + def _check_account(self, user): + if user is not None and len(user) > 256: + raise LimitExceeded('User identifier should be at most ' + '256 characters long.') + def _check_groups(self, groups): - # raise ValueError('Bad characters in groups') - pass + for k, members in groups.iteritems(): + if len(k) > 256: + raise LimitExceeded('Group names should be at most ' + '256 characters long.') + for m in members: + if len(m) > 256: + raise LimitExceeded('Group members should be at most ' + '256 characters long.') def _check_permissions(self, path, permissions): # raise ValueError('Bad characters in permissions') @@ -1902,6 +2476,9 @@ class ModularBackend(BaseBackend): if user != account: raise NotAllowedError + def can_write_container(self, user, account, container): + return self._can_write_container(user, account, container) + @check_allowed_paths(action=0) def _can_read_object(self, user, account, container, name): if user == account: @@ -1947,11 +2524,30 @@ class ModularBackend(BaseBackend): @debug_method @backend_method - def get_domain_objects(self, domain, user=None): - allowed_paths = self.permissions.access_list_paths( - user, include_owned=user is not None, include_containers=False) - if not allowed_paths: - return [] + def get_domain_objects(self, domain, user=None, check_permissions=True): + """List objects having metadata in the specific domain + + If user is provided list only objects accessible to the user. + Otherwise list all the objects for the specific domain + ignoring permissions (check_permissions should be False) + + Raises: + NotAllowedError: if check_permissions is True and user has not + access to the object + AssertionError: if check_permissions is True but user + is provided + """ + if check_permissions: + allowed_paths = self.permissions.access_list_paths( + user, include_owned=user is not None, include_containers=False) + if not allowed_paths: + return [] + else: + if user is not None: + raise AssertionError('Inconsistent argument combination:' + 'if user is provided ' + 'permission check should be enforced.') + allowed_paths = None obj_list = self.node.domain_object_list( domain, allowed_paths, CLUSTER_NORMAL) return [(path, @@ -1963,6 +2559,16 @@ class ModularBackend(BaseBackend): def _build_metadata(self, props, user_defined=None, include_user_defined=True): + if props[self.AVAILABLE] == MAP_UNAVAILABLE: + try: + self._update_available(props) + except IllegalOperationError: + available = MAP_UNAVAILABLE + else: + available = self.node.version_get_properties( + props[self.SERIAL], keys=('available',))[0] + else: + available = props[self.AVAILABLE] meta = {'bytes': props[self.SIZE], 'type': props[self.TYPE], 'hash': props[self.HASH], @@ -1970,7 +2576,10 @@ class ModularBackend(BaseBackend): 'version_timestamp': props[self.MTIME], 'modified_by': props[self.MUSER], 'uuid': props[self.UUID], - 'checksum': props[self.CHECKSUM]} + 'checksum': props[self.CHECKSUM], + 'available': available, + 'mapfile': props[self.MAPFILE], + 'is_snapshot': props[self.IS_SNAPSHOT]} if include_user_defined and user_defined is not None: meta.update(user_defined) return meta @@ -1988,3 +2597,18 @@ class ModularBackend(BaseBackend): return binascii.unhexlify(hash) except TypeError: raise InvalidHash(hash) + + def _size_is_consistent(self, size, hashmap): + if size < 0: + return False + elif size == 0: + if hashmap and hashmap != [self.empty_string_hash]: + return False + else: + if size % self.block_size == 0: + block_num = size / self.block_size + else: + block_num = size / self.block_size + 1 + if block_num != len(hashmap): + return False + return True diff --git a/snf-pithos-backend/pithos/backends/random_word.py b/snf-pithos-backend/pithos/backends/random_word.py index cdc577f8b37aef554d37dcea8f74b3bb74818727..26bd44e1840f2d4b917b315184883ff7b8c876c0 100644 --- a/snf-pithos-backend/pithos/backends/random_word.py +++ b/snf-pithos-backend/pithos/backends/random_word.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import random diff --git a/snf-pithos-backend/pithos/backends/test/__init__.py b/snf-pithos-backend/pithos/backends/test/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..63805dc179b78d27a81fc4b78a97ba0a7a2948e8 --- /dev/null +++ b/snf-pithos-backend/pithos/backends/test/__init__.py @@ -0,0 +1,73 @@ +# Copyright (C) 2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from .common import CommonMixin +from .quota import TestQuotaMixin +from .uuid_methods import TestUUIDMixin +from .snapshots import TestSnapshotsMixin + +from sqlalchemy import create_engine + +import os +import time + +class TestSQLAlchemyBackend(CommonMixin, TestUUIDMixin, + TestQuotaMixin, TestSnapshotsMixin): + db_module = 'pithos.backends.lib.sqlalchemy' + db_connection_str = '%(scheme)s://%(user)s:%(pwd)s@%(host)s:%(port)s/%(name)s' + scheme = os.environ.get('DB_SCHEME', 'postgres') + user = os.environ.get('DB_USER', 'synnefo') + pwd = os.environ.get('DB_PWD', 'example_passw0rd') + host = os.environ.get('DB_HOST', 'db.synnefo.live') + port = os.environ.get('DB_PORT', 5432) + name = 'test_pithos_backend' + db_connection = db_connection_str % locals() + mapfile_prefix ='snf_test_pithos_backend_sqlalchemy_%s_' % time.time() + + @classmethod + def create_db(cls): + db = cls.db_connection_str % {'scheme': cls.scheme, 'user': cls.user, + 'pwd': cls.pwd, 'host': cls.host, + 'port':cls.port, 'name': 'template1'} + e = create_engine(db) + c = e.connect() + c.connection.connection.set_isolation_level(0) + c.execute('create database %s' % cls.name) + c.connection.connection.set_isolation_level(1) + + @classmethod + def destroy_db(cls): + db = cls.db_connection_str % {'scheme': cls.scheme, 'user': cls.user, + 'pwd': cls.pwd, 'host': cls.host, + 'port':cls.port, 'name': 'template1'} + e = create_engine(db) + c = e.connect() + c.connection.connection.set_isolation_level(0) + c.execute('drop database %s' % cls.name) + c.connection.connection.set_isolation_level(1) + +class TestSQLiteBackend(CommonMixin, TestUUIDMixin, TestQuotaMixin, + TestSnapshotsMixin): + db_module = 'pithos.backends.lib.sqlite' + db_connection = location = '/tmp/test_pithos_backend.db' + mapfile_prefix ='snf_test_pithos_backend_sqlite_%s_' % time.time() + + @classmethod + def create_db(cls): + pass + + @classmethod + def destroy_db(cls): + os.remove(cls.location) diff --git a/snf-pithos-backend/pithos/backends/test/common.py b/snf-pithos-backend/pithos/backends/test/common.py new file mode 100644 index 0000000000000000000000000000000000000000..57b9f4258a6a95fb6b37f91c598fc7d5b40e8a40 --- /dev/null +++ b/snf-pithos-backend/pithos/backends/test/common.py @@ -0,0 +1,97 @@ +# Copyright (C) 2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from mock import MagicMock + +from pithos.backends.base import ItemNotExists +from pithos.backends.util import connect_backend + +from .util import get_random_data + +import random +import unittest + +class CommonMixin(unittest.TestCase): + block_size = 1024 + hash_algorithm = 'sha256' + account = 'user' + free_versioning = True + + @classmethod + def setUpClass(cls): + cls.create_db() + + @classmethod + def tearDownClass(cls): + cls.destroy_db() + + def setUp(self): + self.b = connect_backend(db_connection=self.db_connection, + db_module=self.db_module, + block_size=self.block_size, + hash_algorithm=self.hash_algorithm, + free_versioning=self.free_versioning, + mapfile_prefix=self.mapfile_prefix) + self.b.astakosclient = MagicMock() + self.b.astakosclient.issue_one_commission.return_value = 42 + self.b.commission_serials = MagicMock() + + def tearDown(self): + account = self.account + for c in self.b.list_containers(account, account): + self.b.delete_container(account, account, c, delimiter='/') + self.b.delete_container(account, account, c) + self.b.close() + + def upload_object(self, user, account, container, obj, data=None, + length=None, type_='application/octet-stream', + permissions=None): + if data is None: + if length is None: + length = length or random.randint(1, self.block_size) + data = get_random_data(length) + assert len(data) == length + hashmap = [self.b.put_block(data)] + self.b.update_object_hashmap(user, account, container, obj, + length, type_, hashmap, checksum='', + domain='pithos', + permissions=permissions) + return data + + def create_folder(self, user, account, container, folder, + permissions=None): + return self._upload_object(user, account, container, folder, + data='', length=0, + type_='application/directory', + permissions=permissions) + + def assertObjectNotExist(self, account, container, obj): + t = account, account, container, obj + self.assertRaises(ItemNotExists, self.b.get_object_meta, *t, + include_user_defined=False) + self.assertRaises(ItemNotExists, self.b.get_object_hashmap, *t) + objects = [o[0] for o in self.b.list_objects(*t[:-1])] + self.assertTrue(obj not in objects) + + def assertObjectExists(self, account, container, obj): + t = account, account, container, obj + try: + self.b.get_object_meta(*t, include_user_defined=False) + self.b.get_object_hashmap(*t) + except ItemNotExists: + self.fail('The object does not exist!') + objects = self.b.list_objects(*t[:-1]) + objects = [o[0] for o in self.b.list_objects(*t[:-1])] + self.assertTrue(obj in objects) diff --git a/snf-pithos-backend/pithos/backends/test/quota.py b/snf-pithos-backend/pithos/backends/test/quota.py new file mode 100644 index 0000000000000000000000000000000000000000..c119dece0c523baf32eace601839cd374e16bcc4 --- /dev/null +++ b/snf-pithos-backend/pithos/backends/test/quota.py @@ -0,0 +1,787 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from mock import call +from functools import wraps, partial + +import uuid as uuidlib + +from pithos.backends.random_word import get_random_word + + +serial = 0 + +get_random_data = lambda length: get_random_word(length)[:length] +get_random_name = partial(get_random_word, length=8) + + +def assert_issue_commission_calls(func): + @wraps(func) + def wrapper(tc): + assert isinstance(tc, TestQuotaMixin) + tc.expected_issue_commission_calls = [] + func(tc) + tc.assertEqual(tc.b.astakosclient.issue_one_commission.mock_calls, + tc.expected_issue_commission_calls) + return wrapper + + +class TestQuotaMixin(object): + """Challenge quota accounting. + + Each test case records the expected quota commission calls resulting from + the execution of the respective backend methods. + + Finally, it asserts that these calls have been actually made. + """ + def _upload_object(self, user, account, container, obj, data=None, + length=None, type_='application/octet-stream', + permissions=None): + data = self.upload_object(user, account, container, obj, data, length, + type_, permissions) + _, container_node = self.b._lookup_container(account, container) + project = self.b._get_project(container_node) + if len(data) != 0: + self.expected_issue_commission_calls += [ + call.issue_one_commission( + holder=account, + provisions={(project, 'pithos.diskspace'): len(data)}, + name='/'.join([account, container, obj]))] + return data + + @assert_issue_commission_calls + def test_upload_object(self): + account = self.account + container = get_random_name() + obj = get_random_name() + self.b.put_container(account, account, container) + self._upload_object(account, account, container, obj) + + @assert_issue_commission_calls + def test_copy_object(self): + account = self.account + container = get_random_name() + obj = get_random_name() + self.b.put_container(account, account, container) + data = self._upload_object(account, account, container, obj) + + other_obj = get_random_name() + self.b.copy_object(account, account, container, obj, + account, container, other_obj, + 'application/octet-stream', + domain='pithos') + self.expected_issue_commission_calls += [call.issue_one_commission( + holder=account, + provisions={(account, 'pithos.diskspace'): len(data)}, + name='/'.join([account, container, other_obj]))] + + @assert_issue_commission_calls + def test_copy_object_to_other_container(self): + account = self.account + container = get_random_name() + obj = get_random_name() + self.b.put_container(account, account, container) + data = self._upload_object(account, account, container, obj) + + other_container = get_random_name() + self.b.put_container(account, account, other_container) + other_obj = get_random_name() + self.b.copy_object(account, account, container, obj, + account, other_container, other_obj, + 'application/octet-stream', + domain='pithos') + self.expected_issue_commission_calls += [ + call.issue_one_commission( + holder=account, + provisions={(account, 'pithos.diskspace'): len(data)}, + name='/'.join([account, other_container, other_obj]))] + + @assert_issue_commission_calls + def test_copy_obj_to_other_project(self): + account = self.account + container = get_random_name() + obj = get_random_name() + self.b.put_container(account, account, container) + data = self._upload_object(account, account, container, obj) + + other_container = get_random_name() + project = unicode(uuidlib.uuid4()) + self.b.put_container(account, account, other_container, + policy={'project': project}) + self.b.copy_object(account, account, container, obj, + account, other_container, obj, + 'application/octet-stream', + domain='pithos') + self.expected_issue_commission_calls += [ + call.issue_one_commission( + holder=account, + provisions={(project, 'pithos.diskspace'): len(data)}, + name='/'.join([account, other_container, obj]))] + + @assert_issue_commission_calls + def test_copy_object_to_other_account(self): + account = self.account + container = get_random_name() + obj = get_random_name() + self.b.put_container(account, account, container) + + other_account = get_random_name() + self.b.put_container(other_account, other_account, container) + + data = self._upload_object(account, account, container, obj, + permissions={'read': [other_account]}) + + self.b.copy_object(other_account, + account, container, obj, + other_account, container, obj, + 'application/octet-stream', + domain='pithos') + + self.expected_issue_commission_calls += [call.issue_one_commission( + holder=other_account, + provisions={(other_account, 'pithos.diskspace'): len(data)}, + name='/'.join([other_account, container, obj]))] + + @assert_issue_commission_calls + def test_copy_to_existing_path(self): + account = self.account + container = get_random_name() + obj = get_random_name() + self.b.put_container(account, account, container) + data = self._upload_object(account, account, container, obj) + + other = get_random_name() + self._upload_object(account, account, container, other, + length=len(data) + 1) # upload more data + + self.b.copy_object(account, account, container, obj, + account, container, other, + 'application/octet-stream', + domain='pithos') + + self.expected_issue_commission_calls += [call.issue_one_commissions( + holder=account, + provisions={(account, 'pithos.diskspace'): -1}, + name='/'.join([account, container, other]))] + + other = get_random_name() + self._upload_object(account, account, container, other, + length=len(data) - 1) # upload less data + + self.b.copy_object(account, account, container, obj, + account, container, other, + 'application/octet-stream', + domain='pithos') + self.expected_issue_commission_calls += [call.issue_one_commissions( + holder=account, + provisions={(account, 'pithos.diskspace'): 1}, + name='/'.join([account, container, other]))] + + @assert_issue_commission_calls + def test_copy_to_same_path(self): + account = self.account + container = get_random_name() + obj = get_random_name() + self.b.put_container(account, account, container) + self._upload_object(account, account, container, obj) + + self.b.copy_object(account, account, container, obj, + account, container, obj, + 'application/octet-stream', + domain='pithos') + # No issued commissions + + @assert_issue_commission_calls + def test_copy_dir(self): + account = self.account + container = get_random_name() + self.b.put_container(account, account, container) + + folder = get_random_name() + self.create_folder(account, account, container, folder) + + obj1 = '/'.join([folder, get_random_name()]) + data1 = self._upload_object(account, account, container, obj1) + + obj2 = '/'.join([folder, get_random_name()]) + data2 = self._upload_object(account, account, container, obj2) + + other_folder = get_random_name() + self.b.copy_object(account, account, container, folder, + account, container, other_folder, + 'application/directory', + domain='pithos', + delimiter='/') + + self.expected_issue_commission_calls += [ + call.issue_one_commissions( + holder=account, + provisions={(account, 'pithos.diskspace'): len(data1)}, + name='/'.join([account, container, + obj1.replace(folder, other_folder, 1)])), + call.issue_one_commissions( + holder=account, + provisions={(account, 'pithos.diskspace'): len(data2)}, + name='/'.join([account, container, + obj2.replace(folder, other_folder, 1)]))] + + @assert_issue_commission_calls + def test_copy_dir_to_other_container(self): + account = self.account + container = get_random_name() + self.b.put_container(account, account, container) + + folder = get_random_name() + self.create_folder(account, account, container, folder) + + container2 = get_random_name() + self.b.put_container(account, account, container2) + + obj1 = '/'.join([folder, get_random_name()]) + data1 = self._upload_object(account, account, container, obj1) + + obj2 = '/'.join([folder, get_random_name()]) + data2 = self._upload_object(account, account, container, obj2) + + self.b.copy_object(account, account, container, folder, + account, container2, folder, + 'application/directory', + domain='pithos', + delimiter='/') + + self.expected_issue_commission_calls += [ + call.issue_one_commissions( + holder=account, + provisions={(account, 'pithos.diskspace'): len(data1)}, + name='/'.join([account, container2, obj1])), + call.issue_one_commissions( + holder=account, + provisions={(account, 'pithos.diskspace'): len(data2)}, + name='/'.join([account, container2, obj2]))] + + @assert_issue_commission_calls + def test_copy_dir_to_other_account(self): + account = self.account + container = get_random_name() + self.b.put_container(account, account, container) + + other_account = get_random_name() + self.b.put_container(other_account, other_account, container) + + folder = get_random_name() + self.create_folder(account, account, container, folder, + permissions={'read': [other_account]}) + + obj1 = '/'.join([folder, get_random_name()]) + data1 = self._upload_object(account, account, container, obj1) + + obj2 = '/'.join([folder, get_random_name()]) + data2 = self._upload_object(account, account, container, obj2) + + self.b.copy_object(other_account, account, container, folder, + other_account, container, folder, + 'application/directory', + domain='pithos', + delimiter='/') + + self.expected_issue_commission_calls += [ + call.issue_one_commissions( + holder=other_account, + provisions={(other_account, 'pithos.diskspace'): len(data1)}, + name='/'.join([other_account, container, obj1])), + call.issue_one_commissions( + holder=other_account, + provisions={(other_account, 'pithos.diskspace'): len(data2)}, + name='/'.join([other_account, container, obj2]))] + + @assert_issue_commission_calls + def test_copy_dir_to_existing_path(self): + account = self.account + container = get_random_name() + self.b.put_container(account, account, container) + + folder = get_random_name() + self.create_folder(account, account, container, folder) + + obj1 = '/'.join([folder, get_random_name()]) + data1 = self._upload_object(account, account, container, obj1) + + obj2 = '/'.join([folder, get_random_name()]) + data2 = self._upload_object(account, account, container, obj2) + + other_folder = get_random_name() + self.create_folder(account, account, container, other_folder) + # create object under the new folder + # having the same name as an object in the initial folder + obj3 = obj1.replace(folder, other_folder, 1) + data3 = self._upload_object(account, account, container, obj3) + + obj4 = '/'.join([other_folder, get_random_name()]) + self._upload_object(account, account, container, obj4) + + self.b.copy_object(account, account, container, folder, + account, container, other_folder, + 'application/directory', + domain='pithos', + delimiter='/') + + self.expected_issue_commission_calls += [ + call.issue_one_commissions( + holder=account, + provisions={(account, 'pithos.diskspace'): + len(data1) - len(data3)}, + name='/'.join([account, container, obj3])), + call.issue_one_commissions( + holder=account, + provisions={(account, 'pithos.diskspace'): len(data2)}, + name='/'.join([account, container, + obj2.replace(folder, other_folder, 1)]))] + + @assert_issue_commission_calls + def test_copy_dir_to_other_project(self): + account = self.account + container = get_random_name() + self.b.put_container(account, account, container) + + folder = get_random_name() + self.create_folder(account, account, container, folder) + + obj1 = '/'.join([folder, get_random_name()]) + data1 = self._upload_object(account, account, container, obj1) + + obj2 = '/'.join([folder, get_random_name()]) + data2 = self._upload_object(account, account, container, obj2) + + other_container = get_random_name() + project = unicode(uuidlib.uuid4()) + self.b.put_container(account, account, other_container, + policy={'project': project}) + + other_folder = get_random_name() + self.create_folder(account, account, other_container, other_folder) + # create object under the new folder + # having the same name as an object in the initial folder + obj3 = obj1.replace(folder, other_folder, 1) + data3 = self._upload_object(account, account, other_container, obj3) + + obj4 = '/'.join([other_folder, get_random_name()]) + self._upload_object(account, account, other_container, obj4) + self.b.copy_object(account, account, container, folder, + account, other_container, other_folder, + 'application/directory', + domain='pithos', + delimiter='/') + self.expected_issue_commission_calls += [ + call.issue_one_commissions( + holder=account, + provisions={(project, 'pithos.diskspace'): + len(data1) - len(data3)}, + name='/'.join([account, other_container, obj3])), + call.issue_one_commissions( + holder=account, + provisions={(project, 'pithos.diskspace'): len(data2)}, + name='/'.join([account, other_container, + obj2.replace(folder, other_folder, 1)]))] + + @assert_issue_commission_calls + def test_move_obj(self): + account = self.account + container = get_random_name() + obj = get_random_name() + self.b.put_container(account, account, container) + self._upload_object(account, account, container, obj) + + other_obj = get_random_name() + self.b.move_object(account, account, container, obj, + account, container, other_obj, + 'application/octet-stream', + domain='pithos') + + @assert_issue_commission_calls + def test_move_obj_to_other_container(self): + account = self.account + container = get_random_name() + obj = get_random_name() + self.b.put_container(account, account, container) + self._upload_object(account, account, container, obj) + + other_container = get_random_name() + self.b.put_container(account, account, other_container) + other_obj = get_random_name() + self.b.move_object(account, account, container, obj, + account, other_container, other_obj, + 'application/octet-stream', + domain='pithos') + + @assert_issue_commission_calls + def test_move_obj_to_other_project(self): + account = self.account + container = get_random_name() + obj = get_random_name() + self.b.put_container(account, account, container) + data = self._upload_object(account, account, container, obj) + + other_container = get_random_name() + project = unicode(uuidlib.uuid4()) + self.b.put_container(account, account, other_container, + policy={'project': project}) + self.b.move_object(account, account, container, obj, + account, other_container, obj, + 'application/octet-stream', + domain='pithos') + self.expected_issue_commission_calls += [ + call.issue_one_commission( + holder=account, + provisions={(project, 'pithos.diskspace'): len(data)}, + name='/'.join([account, other_container, obj])), + call.issue_one_commission( + holder=account, + provisions={(account, 'pithos.diskspace'): -len(data)}, + name='/'.join([account, container, obj]))] + + @assert_issue_commission_calls + def test_move_object_to_other_account(self): + account = self.account + container = get_random_name() + obj = get_random_name() + self.b.put_container(account, account, container) + + other_account = get_random_name() + self.b.put_container(other_account, other_account, container) + + folder = 'shared' + self.create_folder(other_account, other_account, container, folder, + permissions={'write': [account]}) + + data = self._upload_object(account, account, container, obj) + + other_obj = '/'.join([folder, obj]) + self.b.move_object(account, + account, container, obj, + other_account, container, other_obj, + 'application/octet-stream', + domain='pithos') + + self.expected_issue_commission_calls += [ + call.issue_one_commission( + holder=other_account, + provisions={(other_account, 'pithos.diskspace'): len(data)}, + name='/'.join([other_account, container, other_obj])), + call.issue_one_commission( + holder=account, + provisions={(account, 'pithos.diskspace'): -len(data)}, + name='/'.join([account, container, obj]))] + + @assert_issue_commission_calls + def test_move_to_existing_path(self): + account = self.account + container = get_random_name() + obj = get_random_name() + self.b.put_container(account, account, container) + data = self._upload_object(account, account, container, obj) + + other = get_random_name() + self._upload_object(account, account, container, other, + length=len(data) + 1) # upload more data + + self.b.move_object(account, account, container, obj, + account, container, other, + 'application/octet-stream', + domain='pithos') + + self.expected_issue_commission_calls += [call.issue_one_commissions( + holder=account, + provisions={(account, 'pithos.diskspace'): -1 - len(data)}, + name='/'.join([account, container, other]))] + + data = self._upload_object(account, account, container, obj) + other = get_random_name() + self._upload_object(account, account, container, other, + length=len(data) - 1) # upload less data + + self.b.move_object(account, account, container, obj, + account, container, other, + 'application/octet-stream', + domain='pithos') + self.expected_issue_commission_calls += [call.issue_one_commissions( + holder=account, + provisions={(account, 'pithos.diskspace'): 1 - len(data)}, + name='/'.join([account, container, other]))] + + @assert_issue_commission_calls + def test_move_to_same_path(self): + account = self.account + container = get_random_name() + obj = get_random_name() + self.b.put_container(account, account, container) + self._upload_object(account, account, container, obj) + + self.b.move_object(account, account, container, obj, + account, container, obj, + 'application/octet-stream', + domain='pithos') + self.assertObjectExists(account, container, obj) + # No issued commissions + + @assert_issue_commission_calls + def test_move_dir(self): + account = self.account + container = get_random_name() + self.b.put_container(account, account, container) + + folder = get_random_name() + self.create_folder(account, account, container, folder) + + obj1 = '/'.join([folder, get_random_name()]) + self._upload_object(account, account, container, obj1) + + obj2 = '/'.join([folder, get_random_name()]) + self._upload_object(account, account, container, obj2) + + other_folder = get_random_name() + self.b.move_object(account, account, container, folder, + account, container, other_folder, + 'application/directory', + domain='pithos', + delimiter='/') + + @assert_issue_commission_calls + def test_move_dir_to_other_container(self): + account = self.account + container = get_random_name() + self.b.put_container(account, account, container) + + folder = get_random_name() + self.create_folder(account, account, container, folder) + + container2 = get_random_name() + self.b.put_container(account, account, container2) + + obj1 = '/'.join([folder, get_random_name()]) + self._upload_object(account, account, container, obj1) + + obj2 = '/'.join([folder, get_random_name()]) + self._upload_object(account, account, container, obj2) + + self.b.move_object(account, account, container, folder, + account, container2, folder, + 'application/directory', + domain='pithos', + delimiter='/') + + @assert_issue_commission_calls + def test_move_dir_to_existing_path(self): + account = self.account + container = get_random_name() + self.b.put_container(account, account, container) + + folder = get_random_name() + self.create_folder(account, account, container, folder) + + obj1 = '/'.join([folder, get_random_name()]) + self._upload_object(account, account, container, obj1) + + obj2 = '/'.join([folder, get_random_name()]) + self._upload_object(account, account, container, obj2) + + other_folder = get_random_name() + self.create_folder(account, account, container, other_folder) + # create object under the new folder + # having the same name as an object in the initial folder + obj3 = obj1.replace(folder, other_folder, 1) + data3 = self._upload_object(account, account, container, obj3) + + obj4 = '/'.join([other_folder, get_random_name()]) + self._upload_object(account, account, container, obj4) + + self.b.move_object(account, account, container, folder, + account, container, other_folder, + 'application/directory', + domain='pithos', + delimiter='/') + self.expected_issue_commission_calls += [ + call.issue_one_commissions( + holder=account, + provisions={(account, 'pithos.diskspace'): - len(data3)}, + name='/'.join([account, container, other_folder]))] + + @assert_issue_commission_calls + def test_move_dir_to_other_project(self): + account = self.account + container = get_random_name() + self.b.put_container(account, account, container) + + folder = get_random_name() + self.create_folder(account, account, container, folder) + + obj1 = '/'.join([folder, get_random_name()]) + data1 = self._upload_object(account, account, container, obj1) + + obj2 = '/'.join([folder, get_random_name()]) + data2 = self._upload_object(account, account, container, obj2) + + other_container = get_random_name() + project = unicode(uuidlib.uuid4()) + self.b.put_container(account, account, other_container, + policy={'project': project}) + + other_folder = get_random_name() + self.create_folder(account, account, other_container, other_folder) + # create object under the new folder + # having the same name as an object in the initial folder + obj3 = obj1.replace(folder, other_folder, 1) + data3 = self._upload_object(account, account, other_container, obj3) + + obj4 = '/'.join([other_folder, get_random_name()]) + self._upload_object(account, account, other_container, obj4) + + self.b.move_object(account, account, container, folder, + account, other_container, other_folder, + 'application/directory', + domain='pithos', + delimiter='/') + self.expected_issue_commission_calls += [ + call.issue_one_commissions( + holder=account, + provisions={(project, 'pithos.diskspace'): + len(data1) - len(data3)}, + name='/'.join([account, other_container, obj3])), + call.issue_one_commissions( + holder=account, + provisions={(account, 'pithos.diskspace'): -len(data1)}, + name='/'.join([account, container, obj1])), + call.issue_one_commissions( + holder=account, + provisions={(project, 'pithos.diskspace'): len(data2)}, + name='/'.join([account, other_container, + obj2.replace(folder, other_folder, 1)])), + call.issue_one_commissions( + holder=account, + provisions={(account, 'pithos.diskspace'): -len(data2)}, + name='/'.join([account, container, obj2]))] + + @assert_issue_commission_calls + def test_move_dir_to_other_account(self): + account = self.account + container = get_random_name() + self.b.put_container(account, account, container) + + other_account = get_random_name() + self.b.put_container(other_account, other_account, container) + + folder = get_random_name() + self.create_folder(account, account, container, folder) + self.create_folder(other_account, other_account, container, folder, + permissions={'write': [account]}) + + obj1 = '/'.join([folder, get_random_name()]) + data1 = self._upload_object(account, account, container, obj1) + + obj2 = '/'.join([folder, get_random_name()]) + data2 = self._upload_object(account, account, container, obj2) + + self.b.move_object(account, account, container, folder, + other_account, container, folder, + 'application/directory', + domain='pithos', + delimiter='/') + + self.expected_issue_commission_calls += [ + call.issue_one_commissions( + holder=other_account, + provisions={(other_account, 'pithos.diskspace'): len(data1)}, + name='/'.join([other_account, container, obj1])), + call.issue_one_commissions( + holder=account, + provisions={(account, 'pithos.diskspace'): -len(data1)}, + name='/'.join([account, container, obj1])), + call.issue_one_commissions( + holder=other_account, + provisions={(other_account, 'pithos.diskspace'): len(data2)}, + name='/'.join([other_account, container, obj2])), + call.issue_one_commissions( + holder=account, + provisions={(account, 'pithos.diskspace'): -len(data2)}, + name='/'.join([account, container, obj2]))] + + @assert_issue_commission_calls + def test_delete_container_contents(self): + account = self.account + container = get_random_name() + project = unicode(uuidlib.uuid4()) + self.b.put_container(account, account, container, + policy={'project': project}) + + folder = get_random_name() + self.create_folder(account, account, container, folder) + + obj1 = '/'.join([folder, get_random_name()]) + data = self._upload_object(account, account, container, obj1) + + obj2 = '/'.join([folder, get_random_name()]) + data += self._upload_object(account, account, container, obj2) + + self.b.delete_container(account, account, container, delimiter='/') + + self.expected_issue_commission_calls += [ + call.issue_one_commissions( + holder=account, + provisions={(project, 'pithos.diskspace'): -len(data)}, + name='/'.join([account, container, '']))] + + @assert_issue_commission_calls + def test_delete_object(self): + account = self.account + container = get_random_name() + project = unicode(uuidlib.uuid4()) + self.b.put_container(account, account, container, + policy={'project': project}) + + obj = get_random_name() + data = self._upload_object(account, account, container, obj) + + self.b.delete_object(account, account, container, obj) + + self.expected_issue_commission_calls += [ + call.issue_one_commissions( + holder=account, + provisions={(project, 'pithos.diskspace'): -len(data)}, + name='/'.join([account, container, obj]))] + + @assert_issue_commission_calls + def test_delete_dir(self): + account = self.account + container = get_random_name() + project = unicode(uuidlib.uuid4()) + self.b.put_container(account, account, container, + policy={'project': project}) + + folder = get_random_name() + self.create_folder(account, account, container, folder) + + obj1 = '/'.join([folder, get_random_name()]) + data = self._upload_object(account, account, container, obj1) + + obj2 = '/'.join([folder, get_random_name()]) + data += self._upload_object(account, account, container, obj2) + + self.b.delete_object(account, account, container, folder, + delimiter='/') + + self.expected_issue_commission_calls += [ + call.issue_one_commissions( + holder=account, + provisions={(project, 'pithos.diskspace'): -len(data)}, + name='/'.join([account, container, folder, '']))] diff --git a/snf-pithos-backend/pithos/backends/test/snapshots.py b/snf-pithos-backend/pithos/backends/test/snapshots.py new file mode 100644 index 0000000000000000000000000000000000000000..a3ff5e58bdc6f9a9b772fd72491f2935021fc725 --- /dev/null +++ b/snf-pithos-backend/pithos/backends/test/snapshots.py @@ -0,0 +1,196 @@ +# Copyright (C) 2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import uuid as uuidlib + +from pithos.backends.base import (IllegalOperationError, NotAllowedError, + ItemNotExists, BrokenSnapshot, + MAP_ERROR, MAP_UNAVAILABLE, MAP_AVAILABLE) + + +class TestSnapshotsMixin(object): + def test_copy_snapshot(self): + name = 'snf-snap-1-1' + t = [self.account, self.account, 'snapshots', name] + mapfile = 'archip:%s' % name + self.b.register_object_map(*t, size=100, + type='application/octet-stream', + mapfile=mapfile) + + meta = self.b.get_object_meta(*t, include_user_defined=False) + self.assertTrue('available' in meta) + self.assertEqual(meta['available'], MAP_UNAVAILABLE) + self.assertTrue('mapfile' in meta) + self.assertEqual(meta['mapfile'], mapfile) + self.assertTrue('is_snapshot' in meta) + self.assertEqual(meta['is_snapshot'], True) + + dest_name = 'snf-snap-1-2' + t2 = [self.account, self.account, 'snapshots', dest_name] + self.assertRaises(NotAllowedError, self.b.copy_object, *(t + t2[1:]), + type='application/octet-stream', domain='snapshots') + + self.assertRaises(ItemNotExists, self.b.get_object_meta, *t2) + + meta2 = self.b.get_object_meta(*t, include_user_defined=False) + self.assertTrue('available' in meta2) + self.assertEqual(meta['available'], meta2['available']) + self.assertTrue('mapfile' in meta2) + self.assertTrue(meta['mapfile'] == meta2['mapfile']) + self.assertTrue('is_snapshot' in meta2) + self.assertEqual(meta['is_snapshot'], meta2['is_snapshot']) + self.assertTrue('uuid' in meta2) + uuid = meta2['uuid'] + + self.assertRaises(AssertionError, self.b.update_object_status, uuid, + 'invalid_state') + self.assertRaises(NameError, self.b.update_object_status, + str(uuidlib.uuid4()), -1) + + self.b.update_object_status(uuid, MAP_ERROR) + + meta3 = self.b.get_object_meta(*t, include_user_defined=False) + self.assertTrue('available' in meta3) + self.assertEqual(meta3['available'], MAP_ERROR) + + self.assertRaises(BrokenSnapshot, self.b.get_object_hashmap, *t) + + self.b.update_object_status(uuid, MAP_AVAILABLE) + + meta4 = self.b.get_object_meta(*t, include_user_defined=False) + self.assertTrue('available' in meta4) + self.assertEqual(meta4['available'], MAP_AVAILABLE) + + def test_move_snapshot(self): + name = 'snf-snap-2-1' + t = [self.account, self.account, 'snapshots', name] + mapfile = 'archip:%s' % name + self.b.register_object_map(*t, size=100, + type='application/octet-stream', + mapfile=mapfile) + + meta = self.b.get_object_meta(*t, include_user_defined=False) + self.assertTrue('available' in meta) + self.assertEqual(meta['available'], MAP_UNAVAILABLE) + self.assertTrue('mapfile' in meta) + self.assertEqual(meta['mapfile'], mapfile) + self.assertTrue('is_snapshot' in meta) + self.assertEqual(meta['is_snapshot'], True) + + dest_name = 'snf-snap-2-2' + t2 = [self.account, self.account, 'snapshots', dest_name] + self.b.move_object(*(t + t2[1:]), type='application/octet-stream', + domain='snapshots') + + meta2 = self.b.get_object_meta(*t2, include_user_defined=False) + self.assertTrue('available' in meta2) + self.assertEqual(meta['available'], meta2['available']) + self.assertTrue('mapfile' in meta2) + self.assertEqual(meta['mapfile'], meta2['mapfile']) + self.assertTrue('is_snapshot', meta2['is_snapshot']) + self.assertEqual(meta['is_snapshot'], meta2['is_snapshot']) + + def test_update_snapshot(self): + name = 'snf-snap-3-1' + mapfile = 'archip:%s' % name + t = [self.account, self.account, 'snapshots', name] + self.b.register_object_map(*t, size=100, + type='application/octet-stream', + mapfile=mapfile) + meta = self.b.get_object_meta(*t, include_user_defined=False) + self.assertTrue('available' in meta) + self.assertEqual(meta['available'], MAP_UNAVAILABLE) + self.assertTrue('mapfile' in meta) + self.assertEqual(meta['mapfile'], mapfile) + self.assertTrue('is_snapshot' in meta) + self.assertEqual(meta['is_snapshot'], True) + + domain = 'plankton' + self.b.update_object_meta(*t, domain=domain, meta={'foo': 'bar'}) + meta2 = self.b.get_object_meta(*t, domain=domain, + include_user_defined=True) + self.assertTrue('available' in meta2) + self.assertEqual(meta2['available'], MAP_UNAVAILABLE) + self.assertTrue('mapfile' in meta2) + self.assertEqual(meta2['mapfile'], mapfile) + self.assertTrue('is_snapshot' in meta2) + self.assertEqual(meta2['is_snapshot'], True) + self.assertTrue('foo' in meta2) + self.assertTrue(meta2['foo'], 'bar') + + try: + self.b.update_object_hashmap(*t, size=0, + type='application/octet-stream', + hashmap=(), checksum='', + domain='plankton') + except IllegalOperationError: + meta = self.b.get_object_meta(*t, include_user_defined=False) + self.assertTrue('available' in meta) + self.assertEqual(meta['available'], MAP_UNAVAILABLE) + self.assertTrue('mapfile' in meta) + self.assertEqual(meta['mapfile'], mapfile) + self.assertTrue('is_snapshot' in meta) + self.assertEqual(meta['is_snapshot'], True) + else: + self.fail('Update snapshot should not be allowed') + + def test_get_domain_objects(self): + name = 'snf-snap-1-1' + t = [self.account, self.account, 'snapshots', name] + mapfile = 'archip:%s' % name + uuid = self.b.register_object_map(*t, + domain='test', + size=100, + type='application/octet-stream', + mapfile=mapfile, + meta={'foo': 'bar'}) + try: + objects = self.b.get_domain_objects(domain='test', + user=self.account) + except: + self.fail('It shouldn\'t have arrived here.') + else: + self.assertEqual(len(objects), 1) + path, meta, permissios = objects[0] + self.assertEqual(path, '/'.join(t[1:])) + self.assertTrue('uuid' in meta) + self.assertEqual(meta['uuid'], uuid) + self.assertTrue('available' in meta) + self.assertEqual(meta['available'], MAP_UNAVAILABLE) + + objects = self.b.get_domain_objects(domain='test', + user='somebody_else', + check_permissions=True) + self.assertEqual(objects, []) + + objects = self.b.get_domain_objects(domain='test', user=None, + check_permissions=True) + self.assertEqual(objects, []) + + objects = self.b.get_domain_objects(domain='test', user=None, + check_permissions=False) + self.assertEqual(len(objects), 1) + path, meta, permissios = objects[0] + self.assertEqual(path, '/'.join(t[1:])) + self.assertTrue('uuid' in meta) + self.assertEqual(meta['uuid'], uuid) + self.assertTrue('available' in meta) + self.assertEqual(meta['available'], MAP_UNAVAILABLE) + + self.assertRaises(AssertionError, + self.b.get_domain_objects, + domain='test', + user='somebody_else', + check_permissions=False) diff --git a/snf-pithos-backend/pithos/backends/test/util.py b/snf-pithos-backend/pithos/backends/test/util.py new file mode 100644 index 0000000000000000000000000000000000000000..382e991e995d45d7402a8f7829f0c99942273733 --- /dev/null +++ b/snf-pithos-backend/pithos/backends/test/util.py @@ -0,0 +1,21 @@ +# Copyright (C) 2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import random +import string + +get_random_data = lambda length: ''.join(random.choice(string.letters[:26]) + for i in xrange(length)) +get_random_name = lambda: get_random_data(length=8) diff --git a/snf-pithos-backend/pithos/backends/test/uuid_methods.py b/snf-pithos-backend/pithos/backends/test/uuid_methods.py new file mode 100644 index 0000000000000000000000000000000000000000..ccc5b9f82e6a860590d07e3c55ebaf383d53a0a5 --- /dev/null +++ b/snf-pithos-backend/pithos/backends/test/uuid_methods.py @@ -0,0 +1,174 @@ +# Copyright (C) 2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from functools import partial + +from pithos.backends.base import NotAllowedError +from pithos.backends.random_word import get_random_word + +import uuid as uuidlib + + +serial = 0 + +get_random_data = lambda length: get_random_word(length)[:length] +get_random_name = partial(get_random_word, length=8) + + +class TestUUIDMixin(object): + def test_get_object_by_uuid(self): + container = get_random_name() + obj = get_random_name() + t = self.account, self.account, container, obj + self.b.put_container(*t[:-1]) + + permissions = {'read': ['somebody']} + self.upload_object(*t, permissions=permissions) + self.b.update_object_meta(*t, domain='test1', meta={'domain': 'test1'}) + meta = self.b.get_object_meta(*t, include_user_defined=False) + v1 = meta['version'] + + self.b.update_object_meta(*t, domain='test2', meta={'domain': 'test2'}) + meta = self.b.get_object_meta(*t, include_user_defined=False) + uuid = meta['uuid'] + v2 = meta['version'] + + meta, permissions_, path = self.b.get_object_by_uuid( + uuid, domain='test1', check_permissions=False) + self.assertTrue('domain' in meta) + self.assertEqual(meta['domain'], 'test1') + self.assertEqual(permissions_, permissions) + self.assertEqual(path, '/'.join(t[1:])) + + meta, permissions_, path = self.b.get_object_by_uuid( + uuid, domain='test1', version=v2, check_permissions=False) + self.assertTrue('domain' in meta) + self.assertEqual(meta['domain'], 'test1') + self.assertEqual(permissions_, permissions) + self.assertEqual(path, '/'.join(t[1:])) + + meta, permissions_, path = self.b.get_object_by_uuid( + uuid, domain='test2', version=v2, check_permissions=False) + self.assertTrue('domain' in meta) + self.assertEqual(meta['domain'], 'test2') + self.assertEqual(permissions_, permissions) + self.assertEqual(path, '/'.join(t[1:])) + + meta, permissions_, path = self.b.get_object_by_uuid( + uuid, domain='test2', version=v1, check_permissions=False) + self.assertTrue('domain' not in meta) + self.assertEqual(permissions_, permissions) + self.assertEqual(path, '/'.join(t[1:])) + + meta, permissions_, path = self.b.get_object_by_uuid( + uuid, domain='test1', user='somebody', check_permissions=True) + self.assertTrue('domain' in meta) + self.assertEqual(meta['domain'], 'test1') + self.assertEqual(permissions_, permissions) + self.assertEqual(path, '/'.join(t[1:])) + + self.assertRaises(NotAllowedError, + self.b.get_object_by_uuid, uuid, user='not_allowed', + check_permissions=True) + + self.assertRaises(AssertionError, self.b.get_object_by_uuid, uuid, + domain='test1', user='somebody', + check_permissions=False) + + def test_delete_by_uuid(self): + self.assertRaises(ValueError, self.b.delete_by_uuid, self.account, + uuid=None) + self.assertRaises(ValueError, self.b.delete_by_uuid, self.account, + uuid='None') + random_UUID = uuidlib.uuid4() + self.assertRaises(NameError, self.b.delete_by_uuid, self.account, + uuid=str(random_UUID)) + self.assertRaises(NameError, self.b.delete_by_uuid, self.account, + uuid='{%s}' % random_UUID) + self.assertRaises(NameError, self.b.delete_by_uuid, self.account, + uuid='urn:uuid:%s' % random_UUID) + + container = get_random_name() + obj = get_random_name() + t = self.account, self.account, container, obj + self.b.put_container(*t[:-1]) + + self.upload_object(*t) + meta = self.b.get_object_meta(*t, include_user_defined=False) + uuid = meta['uuid'] + self.b.delete_by_uuid(self.account, unicode(uuid)) + self.assertObjectNotExist(*t[1:]) + + self.upload_object(*t) + meta = self.b.get_object_meta(*t, include_user_defined=False) + self.assertTrue(meta['uuid'] != uuid) # same path, new uuid + uuid = meta['uuid'] + self.b.delete_by_uuid(self.account, str(uuid)) + self.assertObjectNotExist(*t[1:]) + + self.upload_object(*t) + meta = self.b.get_object_meta(*t, include_user_defined=False) + self.assertTrue(meta['uuid'] != uuid) # same path, new uuid + uuid = meta['uuid'] + self.b.delete_by_uuid(self.account, uuid='{%s}' % uuid) + self.assertObjectNotExist(*t[1:]) + + self.upload_object(*t) + meta = self.b.get_object_meta(*t, include_user_defined=False) + self.assertTrue(meta['uuid'] != uuid) # same path, new uuid + uuid = meta['uuid'] + self.b.delete_by_uuid(self.account, uuid='urn:uuid:%s' % uuid) + self.assertObjectNotExist(*t[1:]) + + # check permissions + self.upload_object(*t) + meta = self.b.get_object_meta(*t, include_user_defined=False) + uuid = meta['uuid'] + self.assertRaises(NotAllowedError, self.b.delete_by_uuid, + user='inexistent_account', uuid=uuid) + self.assertObjectExists(*t[1:]) + + other_account = get_random_name() + self.b.put_account(other_account, other_account) + # user has no access at all to the object + self.assertRaises(NotAllowedError, self.b.delete_by_uuid, + user=other_account, uuid=uuid) + self.assertObjectExists(*t[1:]) + + # user has read access to the object + self.b.update_object_permissions(*t, + permissions={'read': [other_account]}) + try: + self.b.get_object_meta(other_account, *t[1:], + include_user_defined=False) + except NotAllowedError: + self.fail('User has read access to the object!') + self.assertRaises(NotAllowedError, self.b.delete_by_uuid, + user=other_account, uuid=uuid) + self.assertObjectExists(*t[1:]) + + # user has write access to the object + self.b.update_object_permissions(*t, + permissions={'write': + [other_account]}) + try: + self.b.update_object_meta(other_account, *t[1:], + domain='test', + meta={'foo': 'bar'}) + except NotAllowedError: + self.fail('User has write access to the object!') + self.assertRaises(NotAllowedError, self.b.delete_by_uuid, + user=other_account, uuid=uuid) + self.assertObjectExists(*t[1:]) diff --git a/snf-pithos-backend/pithos/backends/util.py b/snf-pithos-backend/pithos/backends/util.py index c74be6920ae2d51124db35e788919cf07b5a75c6..bbbc4e66b4ae1475ace35121254145edbb13c8cd 100644 --- a/snf-pithos-backend/pithos/backends/util.py +++ b/snf-pithos-backend/pithos/backends/util.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from objpool import ObjectPool from new import instancemethod @@ -41,70 +23,12 @@ USAGE_LIMIT = 500 class PithosBackendPool(ObjectPool): - def __init__(self, size=None, db_module=None, db_connection=None, - block_module=None, block_path=None, block_umask=None, - block_size=None, hash_algorithm=None, - queue_module=None, queue_hosts=None, - queue_exchange=None, free_versioning=True, - astakos_auth_url=None, service_token=None, - astakosclient_poolsize=None, - block_params=None, - public_url_security=None, - public_url_alphabet=None, - account_quota_policy=None, - container_quota_policy=None, - container_versioning_policy=None, - backend_storage=None, - rados_ceph_conf=None): + def __init__(self, size=None, **kwargs): super(PithosBackendPool, self).__init__(size=size) - self.db_module = db_module - self.db_connection = db_connection - self.block_module = block_module - self.block_path = block_path - self.block_umask = block_umask - self.block_size = block_size - self.hash_algorithm = hash_algorithm - self.queue_module = queue_module - self.block_params = block_params - self.queue_hosts = queue_hosts - self.queue_exchange = queue_exchange - self.astakos_auth_url = astakos_auth_url - self.service_token = service_token - self.astakosclient_poolsize = astakosclient_poolsize - self.free_versioning = free_versioning - self.public_url_security = public_url_security - self.public_url_alphabet = public_url_alphabet - self.account_quota_policy = account_quota_policy - self.container_quota_policy = container_quota_policy - self.container_versioning_policy = container_versioning_policy - self.backend_storage = backend_storage - self.rados_ceph_conf = rados_ceph_conf + self.backend_kwargs = kwargs def _pool_create(self): - backend = connect_backend( - db_module=self.db_module, - db_connection=self.db_connection, - block_module=self.block_module, - block_path=self.block_path, - block_umask=self.block_umask, - block_size=self.block_size, - hash_algorithm=self.hash_algorithm, - queue_module=self.queue_module, - block_params=self.block_params, - queue_hosts=self.queue_hosts, - queue_exchange=self.queue_exchange, - astakos_auth_url=self.astakos_auth_url, - service_token=self.service_token, - astakosclient_poolsize=self.astakosclient_poolsize, - free_versioning=self.free_versioning, - public_url_security=self.public_url_security, - public_url_alphabet=self.public_url_alphabet, - account_quota_policy=self.account_quota_policy, - container_quota_policy=self.container_quota_policy, - container_versioning_policy=self.container_versioning_policy, - backend_storage=self.backend_storage, - rados_ceph_conf=self.rados_ceph_conf) - + backend = connect_backend(**self.backend_kwargs) backend._real_close = backend.close backend.close = instancemethod(_pooled_backend_close, backend, type(backend)) diff --git a/snf-pithos-backend/pithos/workers/__init__.py b/snf-pithos-backend/pithos/workers/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/snf-pithos-backend/pithos/workers/glue.py b/snf-pithos-backend/pithos/workers/glue.py new file mode 100644 index 0000000000000000000000000000000000000000..f6dedb58486ba497c7be2f3264c86a6b0504b0ab --- /dev/null +++ b/snf-pithos-backend/pithos/workers/glue.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 - +# +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import ConfigParser + + +class WorkerGlue(object): + + pmap = {} + worker_id = None + ioctx_pool = None + ArchipelagoConfFile = None + + @classmethod + def setmap(cls, pid, index): + WorkerGlue.pmap[pid] = index + WorkerGlue.worker_id = index + + @classmethod + def setupXsegPool(cls, ObjectPool, Segment, Xseg_ctx, + cfile='/etc/archipelago/archipelago.conf', pool_size=8): + if WorkerGlue.ioctx_pool is not None: + return + bcfg = ConfigParser.ConfigParser() + bcfg.readfp(open(cfile)) + worker_id = WorkerGlue.worker_id + WorkerGlue.ArchipelagoConfFile = cfile + try: + archipelago_segment_type = bcfg.get('XSEG', 'SEGMENT_TYPE') + except ConfigParser.NoOptionError: + archipelago_segment_type = 'posix' + try: + archipelago_segment_name = bcfg.get('XSEG', 'SEGMENT_NAME') + except ConfigParser.NoOptionError: + archipelago_segment_name = 'archipelago' + try: + archipelago_segment_alignment = bcfg.get('XSEG', + 'SEGMENT_ALIGNMENT') + except ConfigParser.NoOptionError: + archipelago_segment_alignment = 12 + archipelago_dynports = bcfg.getint('XSEG', 'SEGMENT_DYNPORTS') + archipelago_ports = bcfg.getint('XSEG', 'SEGMENT_PORTS') + archipelago_segment_size = bcfg.getint('XSEG', 'SEGMENT_SIZE') + + class XsegPool(ObjectPool): + + def __init__(self): + super(XsegPool, self).__init__(size=pool_size) + self.segment = Segment(archipelago_segment_type, + archipelago_segment_name, + archipelago_dynports, + archipelago_ports, + archipelago_segment_size, + archipelago_segment_alignment) + self.worker_id = worker_id + self.cnt = 1 + self._ioctx_set = set() + + def _pool_create(self): + if self.worker_id == 1: + ioctx = Xseg_ctx(self.segment, self.worker_id + self.cnt) + self.cnt += 1 + self._ioctx_set.add(ioctx) + return ioctx + elif self.worker_id > 1: + ioctx = Xseg_ctx(self.segment, + (self.worker_id - 1) * pool_size + 2 + + self.cnt) + self.cnt += 1 + self._ioctx_set.add(ioctx) + return ioctx + elif self.worker_id is None: + ioctx = Xseg_ctx(self.segment) + self._ioctx_set.add(ioctx) + return ioctx + + def _pool_verify(self, poolobj): + return True + + def _pool_cleanup(self, poolobj): + return False + + def _shutdown_pool(self): + for _ in xrange(len(self._ioctx_set)): + ioctx = self._ioctx_set.pop() + ioctx.shutdown() + + WorkerGlue.ioctx_pool = XsegPool() diff --git a/snf-pithos-backend/pithos/workers/monkey.py b/snf-pithos-backend/pithos/workers/monkey.py new file mode 100644 index 0000000000000000000000000000000000000000..b6a65acacd4ca2f4852709205fc4af2ef55587b5 --- /dev/null +++ b/snf-pithos-backend/pithos/workers/monkey.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 - +# +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +from archipelago import common +from archipelago.common import posixfd_signal_desc +from ctypes import cast, POINTER +import os + +try: + from gevent import select +except ImportError: + import select + + +def pithos_xseg_wait_signal_green(ctx, sd, timeout): + posixfd_sd = cast(sd, POINTER(posixfd_signal_desc)) + fd = posixfd_sd.contents.fd + select.select([fd], [], [], timeout / 1000000.0) + while True: + try: + os.read(fd, 512) + except OSError as (e, msg): + if e == 11: + break + else: + raise OSError(e, msg) + + +def patch_Request(): + common.xseg_wait_signal_green = pithos_xseg_wait_signal_green diff --git a/snf-pithos-backend/setup.py b/snf-pithos-backend/setup.py index caea16de3be9f52d06e13cf4d5bef5edb91acc09..543af95b2ff51ab7c18561562a1bab5ea30c8fb5 100644 --- a/snf-pithos-backend/setup.py +++ b/snf-pithos-backend/setup.py @@ -1,35 +1,17 @@ -# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # import distribute_setup @@ -61,7 +43,8 @@ INSTALL_REQUIRES = [ 'snf-common', 'SQLAlchemy>=0.6.3, <=0.7.8', 'alembic>=0.3.4, <0.4', - 'objpool>=0.3' + 'objpool>=0.3', + 'archipelago' ] EXTRAS_REQUIRES = { @@ -167,7 +150,7 @@ def find_package_data( setup( name='snf-pithos-backend', version=VERSION, - license='BSD', + license='GNU GPLv3', url='http://www.synnefo.org/', description=SHORT_DESCRIPTION, classifiers=CLASSIFIERS, diff --git a/snf-stats-app/MANIFEST.in b/snf-stats-app/MANIFEST.in index 6106c5d89680afa76e234fbcf1feb50c52b9a5f6..ce7f614828c2d727bb93e8692c231749d45fdd5a 100644 --- a/snf-stats-app/MANIFEST.in +++ b/snf-stats-app/MANIFEST.in @@ -1 +1 @@ -include distribute_setup.py +include distribute_setup.py README.md diff --git a/snf-stats-app/README.md b/snf-stats-app/README.md new file mode 100644 index 0000000000000000000000000000000000000000..bbe81158a6fd0fe56070410b5020e7ff86ddf1e1 --- /dev/null +++ b/snf-stats-app/README.md @@ -0,0 +1,27 @@ +snf-stats-app +============= + +Overview +-------- + +This is Synnefo's snf-stats-app component. Please see the [official Synnefo +site](http://www.synnefo.org) for more information. + + +Copyright and license +===================== + +Copyright (C) 2010-2014 GRNET S.A. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. diff --git a/snf-stats-app/setup.py b/snf-stats-app/setup.py index bfffe053cd05bbe6b6a052de9556b41aa2d2fd28..62b2276333fa785c4b287a07f5dd9a6ad6b3661d 100644 --- a/snf-stats-app/setup.py +++ b/snf-stats-app/setup.py @@ -1,35 +1,17 @@ -# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # import distribute_setup @@ -65,7 +47,7 @@ INSTALL_REQUIRES = [ setup( name='snf-stats-app', version=VERSION, - license='BSD', + license='GNU GPLv3', url='http://www.synnefo.org/', description=SHORT_DESCRIPTION, classifiers=CLASSIFIERS, diff --git a/snf-stats-app/synnefo_stats/__init__.py b/snf-stats-app/synnefo_stats/__init__.py index aae657e32052f617520783bcb19ba9e9aa8a68f0..5cda842903d0bfec117db1c3c5a43281d46eb551 100644 --- a/snf-stats-app/synnefo_stats/__init__.py +++ b/snf-stats-app/synnefo_stats/__init__.py @@ -1,35 +1,17 @@ -# Copyright 2011-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # this is a namespace package try: diff --git a/snf-stats-app/synnefo_stats/grapher.py b/snf-stats-app/synnefo_stats/grapher.py index 1b006f3f4711abd0c776f16174347371693db18b..b16c2c53064ee9a7c9f03a3466eabbd21b20d8aa 100644 --- a/snf-stats-app/synnefo_stats/grapher.py +++ b/snf-stats-app/synnefo_stats/grapher.py @@ -1,40 +1,24 @@ -# Copyright 2011-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.http import HttpResponse +from django.utils.encoding import smart_str import gd import os +import os.path from cStringIO import StringIO @@ -46,7 +30,6 @@ from hashlib import sha256 from synnefo_stats import settings -from synnefo.util.text import uenc from snf_django.lib.api import faults, api_method from logging import getLogger @@ -113,6 +96,8 @@ def draw_cpu_bar(fname, outfname=None): def draw_net_bar(fname, outfname=None): fname = os.path.join(fname, "interface", "if_octets-eth0.rrd") + if not os.path.isfile(fname): + raise faults.ItemNotFound("VM has no attached NICs") try: values = rrdtool.fetch(fname, "AVERAGE")[2][-20:] @@ -169,9 +154,9 @@ def draw_cpu_ts(fname, outfname): outfname += "-cpu.png" rrdtool.graph(outfname, "-s", "-1d", "-e", "-20s", - #"-t", "CPU usage", + # "-t", "CPU usage", "-v", "%", - #"--lazy", + # "--lazy", "DEF:cpu=%s:value:AVERAGE" % fname, "LINE1:cpu#00ff00:") @@ -183,9 +168,9 @@ def draw_cpu_ts_w(fname, outfname): outfname += "-cpu-weekly.png" rrdtool.graph(outfname, "-s", "-1w", "-e", "-20s", - #"-t", "CPU usage", + # "-t", "CPU usage", "-v", "%", - #"--lazy", + # "--lazy", "DEF:cpu=%s:value:AVERAGE" % fname, "LINE1:cpu#00ff00:") @@ -195,6 +180,8 @@ def draw_cpu_ts_w(fname, outfname): def draw_net_ts(fname, outfname): fname = os.path.join(fname, "interface", "if_octets-eth0.rrd") outfname += "-net.png" + if not os.path.isfile(fname): + raise faults.ItemNotFound("VM has no attached NICs") rrdtool.graph(outfname, "-s", "-1d", "-e", "-20s", "--units", "si", @@ -215,6 +202,8 @@ def draw_net_ts(fname, outfname): def draw_net_ts_w(fname, outfname): fname = os.path.join(fname, "interface", "if_octets-eth0.rrd") outfname += "-net-weekly.png" + if not os.path.isfile(fname): + raise faults.ItemNotFound("VM has no attached NICs") rrdtool.graph(outfname, "-s", "-1w", "-e", "-20s", "--units", "si", @@ -253,14 +242,14 @@ available_graph_types = {'cpu-bar': draw_cpu_bar, format_allowed=False, logger=log) def grapher(request, graph_type, hostname): try: - hostname = decrypt(uenc(hostname)) + hostname = decrypt(smart_str(hostname)) except (ValueError, TypeError): raise faults.BadRequest("Invalid encrypted virtual server name") - fname = uenc(os.path.join(settings.RRD_PREFIX, hostname)) + fname = smart_str(os.path.join(settings.RRD_PREFIX, hostname)) if not os.path.isdir(fname): raise faults.ItemNotFound('No such instance') - outfname = uenc(os.path.join(settings.GRAPH_PREFIX, hostname)) + outfname = smart_str(os.path.join(settings.GRAPH_PREFIX, hostname)) draw_func = available_graph_types[graph_type] response = HttpResponse(draw_func(fname, outfname), diff --git a/snf-stats-app/synnefo_stats/synnefo_settings.py b/snf-stats-app/synnefo_stats/synnefo_settings.py index 914465fd45098216a4642765d0120ff7efed0694..59bf4ddcbe95bf236a991bc91a5ce826ee92978d 100644 --- a/snf-stats-app/synnefo_stats/synnefo_settings.py +++ b/snf-stats-app/synnefo_stats/synnefo_settings.py @@ -1,35 +1,17 @@ -# Copyright 2011-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # """ diff --git a/snf-stats-app/synnefo_stats/urls.py b/snf-stats-app/synnefo_stats/urls.py index 852e43f978d9597c4839a5c41e9bd9af781e98e6..2704cca4340b6b4a178ab369a02c0032ee46fe1f 100644 --- a/snf-stats-app/synnefo_stats/urls.py +++ b/snf-stats-app/synnefo_stats/urls.py @@ -1,35 +1,17 @@ -# Copyright 2011-2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.conf.urls import patterns, include diff --git a/snf-tools/MANIFEST.in b/snf-tools/MANIFEST.in index 6106c5d89680afa76e234fbcf1feb50c52b9a5f6..ce7f614828c2d727bb93e8692c231749d45fdd5a 100644 --- a/snf-tools/MANIFEST.in +++ b/snf-tools/MANIFEST.in @@ -1 +1 @@ -include distribute_setup.py +include distribute_setup.py README.md diff --git a/snf-tools/README.md b/snf-tools/README.md new file mode 100644 index 0000000000000000000000000000000000000000..e6a05eba2e5f0c4a7c699b223fc62be8ba9c3f89 --- /dev/null +++ b/snf-tools/README.md @@ -0,0 +1,27 @@ +snf-tools +========= + +Overview +-------- + +This is Synnefo's snf-tools component. Please see the [official Synnefo +site](http://www.synnefo.org) for more information. + + +Copyright and license +===================== + +Copyright (C) 2010-2014 GRNET S.A. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. diff --git a/snf-tools/setup.py b/snf-tools/setup.py index edac06669a39b66f00b6362a1b1ac6182203c970..df7ab27ee33ef855a8cd237361b7371ba30ea49e 100644 --- a/snf-tools/setup.py +++ b/snf-tools/setup.py @@ -1,35 +1,17 @@ -# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # import distribute_setup @@ -37,8 +19,8 @@ distribute_setup.use_setuptools() import os -#from distutils.util import convert_path -#from fnmatch import fnmatchcase +# from distutils.util import convert_path +# from fnmatch import fnmatchcase from setuptools import setup, find_packages HERE = os.path.abspath(os.path.normpath(os.path.dirname(__file__))) @@ -60,12 +42,13 @@ INSTALL_REQUIRES = [ "IPy", "paramiko", "vncauthproxy", - "kamaki >= 0.12.3"] + "kamaki >= 0.13rc5", +] setup( name='snf-tools', version=VERSION, - license='BSD', + license='GNU GPLv3', url='http://www.synnefo.org/', description=SHORT_DESCRIPTION, classifiers=CLASSIFIERS, diff --git a/snf-tools/synnefo_tools/burnin/__init__.py b/snf-tools/synnefo_tools/burnin/__init__.py index 056ab87897f2e2cc026b17dd8970a2c2f453bc73..0be4eaeac26e2943f8ff8f2678a0f80c587ff94a 100644 --- a/snf-tools/synnefo_tools/burnin/__init__.py +++ b/snf-tools/synnefo_tools/burnin/__init__.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. """ Burnin: functional tests for Synnefo @@ -47,6 +29,8 @@ from synnefo_tools.burnin.images_tests import \ from synnefo_tools.burnin.pithos_tests import PithosTestSuite from synnefo_tools.burnin.server_tests import ServerTestSuite from synnefo_tools.burnin.network_tests import NetworkTestSuite +from synnefo_tools.burnin.projects_tests import QuotasTestSuite +from synnefo_tools.burnin.snapshots import SnapshotsTestSuite from synnefo_tools.burnin.stale_tests import \ StaleServersTestSuite, StaleFloatingIPsTestSuite, StaleNetworksTestSuite @@ -60,6 +44,8 @@ TESTSUITES = [ PithosTestSuite, ServerTestSuite, NetworkTestSuite, + QuotasTestSuite, + SnapshotsTestSuite ] TSUITES_NAMES = [tsuite.__name__ for tsuite in TESTSUITES] @@ -92,8 +78,7 @@ def parse_arguments(args): kwargs["description"] = \ "%prog runs a number of test scenarios on a Synnefo deployment." - # Used * or ** magic. pylint: disable-msg=W0142 - parser = optparse.OptionParser(**kwargs) + parser = optparse.OptionParser(**kwargs) # pylint: disable=star-args parser.disable_interspersed_args() parser.add_option( @@ -104,6 +89,10 @@ def parse_arguments(args): "--token", action="store", type="string", default=None, dest="token", help="The token to use for authentication to the API") + parser.add_option( + "--ignore-ssl", "-k", action="store_true", + default=None, dest="ignore_ssl", + help="Don't verify SSL certificates") parser.add_option( "--failfast", action="store_true", default=False, dest="failfast", @@ -144,7 +133,7 @@ def parse_arguments(args): "--system-user", action="store", type="string", default=None, dest="system_user", help="Owner of system images (typed option in the form of " - "\"name:user_name\" or \"id:uuuid\")") + "\"name:user_name\" or \"id:uuid\")") parser.add_option( "--show-stale", action="store_true", default=False, dest="show_stale", @@ -194,6 +183,21 @@ def parse_arguments(args): "--temp-directory", action="store", default="/tmp/", dest="temp_directory", help="Directory to use for saving temporary files") + parser.add_option( + "--obj-upload-num", action="store", + type="int", default=2, dest="obj_upload_num", + help="Set the number of objects to massively be uploaded " + "(default: 2)") + parser.add_option( + "--obj-upload-min-size", action="store", + type="int", default=10 * common.MB, dest="obj_upload_min_size", + help="Set the min size of the object to massively be uploaded " + "(default: 10MB)") + parser.add_option( + "--obj-upload-max-size", action="store", + type="int", default=20 * common.MB, dest="obj_upload_max_size", + help="Set the max size of the objects to massively be uploaded " + "(default: 20MB)") (opts, args) = parser.parse_args(args) diff --git a/snf-tools/synnefo_tools/burnin/astakos_tests.py b/snf-tools/synnefo_tools/burnin/astakos_tests.py index 02e276da0ab783f247657ae1ef46ce18be6c6e47..67036b56eb79d6192ce079ba0cb28e59a9d51c36 100644 --- a/snf-tools/synnefo_tools/burnin/astakos_tests.py +++ b/snf-tools/synnefo_tools/burnin/astakos_tests.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. """ This is the burnin class that tests the Astakos functionality @@ -39,12 +21,14 @@ This is the burnin class that tests the Astakos functionality from kamaki.clients.compute import ComputeClient from kamaki.clients import ClientError -from synnefo_tools.burnin import common +from synnefo_tools.burnin.common import BurninTests, Proper -# Too many public methods. pylint: disable-msg=R0904 -class AstakosTestSuite(common.BurninTests): +# pylint: disable=too-many-public-methods +class AstakosTestSuite(BurninTests): """Test Astakos functionality""" + details = Proper(value=None) + def test_001_unauthorized_access(self): """Test that access without a valid token fails""" false_token = "12345" @@ -69,3 +53,36 @@ class AstakosTestSuite(common.BurninTests): self.assertIn(given_name[our_uuid], given_uuid) self.assertEqual(given_uuid[given_name[our_uuid]], our_uuid) + + def test_005_authenticate(self): + """Test astakos.authenticate""" + astakos = self.clients.astakos + self.details = astakos.authenticate() + self.info('Check result integrity') + self.assertIn('access', self.details) + access = self.details['access'] + self.assertEqual(set(('user', 'token', 'serviceCatalog')), set(access)) + self.info('Top-level keys are correct') + self.assertEqual(self.clients.token, access['token']['id']) + self.info('Token is correct') + self.assertEqual( + set(['roles', 'name', 'id', 'roles_links']), + set(astakos.user_info)) + self.info('User section is correct') + + def test_010_get_service_endpoints(self): + """Test endpoints integrity""" + scat = self.details['access']['serviceCatalog'] + types = ( + 'compute', 'object-store', 'identity', 'account', + 'image', 'volume', 'network', 'astakos_weblogin', + 'admin', 'vmapi', 'astakos_auth') + self.assertEqual(set(types), set([s['type'] for s in scat])) + self.info('All expected endpoint types (and only them) found') + + astakos = self.clients.astakos + for etype in types: + endpoint = [s for s in scat + if s['type'] == etype][0]['endpoints'][0] + self.assertEqual(endpoint, astakos.get_service_endpoints(etype)) + self.info('Endpoint call results match original results') diff --git a/snf-tools/synnefo_tools/burnin/common.py b/snf-tools/synnefo_tools/burnin/common.py index f1cd9b68631bcf7cb71de499392a3bbdd7135b3a..116dae61a85da39e3cf5288ce2929e165b7b815b 100644 --- a/snf-tools/synnefo_tools/burnin/common.py +++ b/snf-tools/synnefo_tools/burnin/common.py @@ -1,61 +1,52 @@ -# Copyright 2013-2014 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. """ Common utils for burnin tests """ +import hashlib import re import shutil import unittest import datetime import tempfile import traceback +from tempfile import NamedTemporaryFile +from os import urandom +from string import ascii_letters +from StringIO import StringIO +from binascii import hexlify +from kamaki.cli import config as kamaki_config from kamaki.clients.cyclades import CycladesClient, CycladesNetworkClient -from kamaki.clients.astakos import AstakosClient, parse_endpoints +from kamaki.clients.astakos import AstakosClient from kamaki.clients.compute import ComputeClient from kamaki.clients.pithos import PithosClient from kamaki.clients.image import ImageClient +from kamaki.clients.utils import https +from kamaki.clients.blockstorage import BlockStorageClient from synnefo_tools.burnin.logger import Log # -------------------------------------------------------------------- # Global variables -logger = None # Invalid constant name. pylint: disable-msg=C0103 -success = None # Invalid constant name. pylint: disable-msg=C0103 +logger = None # pylint: disable=invalid-name +success = None # pylint: disable=invalid-name SNF_TEST_PREFIX = "snf-test-" CONNECTION_RETRY_LIMIT = 2 SYSTEM_USERS = ["images@okeanos.grnet.gr", "images@demo.synnefo.org"] @@ -63,6 +54,17 @@ KB = 2**10 MB = 2**20 GB = 2**30 +QADD = 1 +QREMOVE = -1 + +QDISK = "cyclades.disk" +QVM = "cyclades.vm" +QPITHOS = "pithos.diskspace" +QRAM = "cyclades.ram" +QIP = "cyclades.floating_ip" +QCPU = "cyclades.cpu" +QNET = "cyclades.network.private" + # -------------------------------------------------------------------- # BurninTestResult class @@ -77,9 +79,11 @@ class BurninTestResult(unittest.TestResult): def startTest(self, test): # noqa """Called when the test case test is about to be run""" super(BurninTestResult, self).startTest(test) - logger.log(test.__class__.__name__, test.shortDescription()) + logger.log( + test.__class__.__name__, + test.shortDescription() or 'Test %s' % test.__class__.__name__) - # Method could be a function. pylint: disable-msg=R0201 + # pylint: disable=no-self-use def _test_failed(self, test, err): """Test failed""" # Get class name @@ -105,11 +109,25 @@ class BurninTestResult(unittest.TestResult): super(BurninTestResult, self).addFailure(test, err) self._test_failed(test, err) + # pylint: disable=fixme + def addSkip(self, test, reason): # noqa + """Called when the test case test is skipped + + If reason starts with "__SkipClass__: " then + we should stop the execution of all the TestSuite. + + TODO: There should be a better way to do this + + """ + super(BurninTestResult, self).addSkip(test, reason) + if reason.startswith("__SkipClass__: "): + self.stop() + # -------------------------------------------------------------------- # Helper Classes -# Too few public methods. pylint: disable-msg=R0903 -# Too many instance attributes. pylint: disable-msg=R0902 +# pylint: disable=too-few-public-methods +# pylint: disable=too-many-instance-attributes class Clients(object): """Our kamaki clients""" auth_url = None @@ -132,40 +150,67 @@ class Clients(object): image = None image_url = None - def initialize_clients(self): + def _kamaki_ssl(self, ignore_ssl=None): + """Patch kamaki to use the correct CA certificates + + Read kamaki's config file and decide if we are going to use + CA certificates and patch kamaki clients accordingly. + + """ + config = kamaki_config.Config() + if ignore_ssl is None: + ignore_ssl = config.get("global", "ignore_ssl").lower() == "on" + ca_file = config.get("global", "ca_certs") + + if ignore_ssl: + # Skip SSL verification + https.patch_ignore_ssl() + else: + # Use ca_certs path found in kamakirc + https.patch_with_certs(ca_file) + + def initialize_clients(self, ignore_ssl=False): """Initialize all the Kamaki Clients""" + + # Path kamaki for SSL verification + self._kamaki_ssl(ignore_ssl=ignore_ssl) + + # Initialize kamaki Clients self.astakos = AstakosClient(self.auth_url, self.token) self.astakos.CONNECTION_RETRY_LIMIT = self.retry - endpoints = self.astakos.authenticate() - - self.compute_url = _get_endpoint_url(endpoints, "compute") + self.compute_url = self.astakos.get_endpoint_url( + ComputeClient.service_type) self.compute = ComputeClient(self.compute_url, self.token) self.compute.CONNECTION_RETRY_LIMIT = self.retry - self.cyclades = CycladesClient(self.compute_url, self.token) + self.cyclades_url = self.astakos.get_endpoint_url( + CycladesClient.service_type) + self.cyclades = CycladesClient(self.cyclades_url, self.token) self.cyclades.CONNECTION_RETRY_LIMIT = self.retry - self.network_url = _get_endpoint_url(endpoints, "network") + self.block_storage_url = self.astakos.get_endpoint_url( + BlockStorageClient.service_type) + self.block_storage = BlockStorageClient(self.block_storage_url, + self.token) + self.block_storage.CONNECTION_RETRY_LIMIT = self.retry + + self.network_url = self.astakos.get_endpoint_url( + CycladesNetworkClient.service_type) self.network = CycladesNetworkClient(self.network_url, self.token) self.network.CONNECTION_RETRY_LIMIT = self.retry - self.pithos_url = _get_endpoint_url(endpoints, "object-store") + self.pithos_url = self.astakos.get_endpoint_url( + PithosClient.service_type) self.pithos = PithosClient(self.pithos_url, self.token) self.pithos.CONNECTION_RETRY_LIMIT = self.retry - self.image_url = _get_endpoint_url(endpoints, "image") + self.image_url = self.astakos.get_endpoint_url( + ImageClient.service_type) self.image = ImageClient(self.image_url, self.token) self.image.CONNECTION_RETRY_LIMIT = self.retry -def _get_endpoint_url(endpoints, endpoint_type): - """Get the publicURL for the specified endpoint""" - - service_catalog = parse_endpoints(endpoints, ep_type=endpoint_type) - return service_catalog[0]['endpoints'][0]['publicURL'] - - class Proper(object): """A descriptor used by tests implementing the TestCase class @@ -184,13 +229,66 @@ class Proper(object): self.val = value +def file_read_iterator(fp, size=1024): + while True: + data = fp.read(size) + if not data: + break + yield data + + +class HashMap(list): + + def __init__(self, blocksize, blockhash): + super(HashMap, self).__init__() + self.blocksize = blocksize + self.blockhash = blockhash + + def _hash_raw(self, v): + h = hashlib.new(self.blockhash) + h.update(v) + return h.digest() + + def _hash_block(self, v): + return self._hash_raw(v.rstrip('\x00')) + + def hash(self): + if len(self) == 0: + return self._hash_raw('') + if len(self) == 1: + return self.__getitem__(0) + + h = list(self) + s = 2 + while s < len(h): + s = s * 2 + h += [('\x00' * len(h[0]))] * (s - len(h)) + while len(h) > 1: + h = [self._hash_raw(h[x] + h[x + 1]) for x in range(0, len(h), 2)] + return h[0] + + def load(self, data): + self.size = 0 + fp = StringIO(data) + for block in file_read_iterator(fp, self.blocksize): + self.append(self._hash_block(block)) + self.size += len(block) + + +def merkle(data, blocksize, blockhash): + hashes = HashMap(blocksize, blockhash) + hashes.load(data) + return hexlify(hashes.hash()) + + # -------------------------------------------------------------------- # BurninTests class -# Too many public methods (45/20). pylint: disable-msg=R0904 +# pylint: disable=too-many-public-methods class BurninTests(unittest.TestCase): """Common class that all burnin tests should implement""" clients = Clients() run_id = None + ignore_ssl = False use_ipv6 = None action_timeout = None action_warning = None @@ -201,8 +299,10 @@ class BurninTests(unittest.TestCase): delete_stale = False temp_directory = None failfast = None + temp_containers = [] quotas = Proper(value=None) + uuid = Proper(value=None) @classmethod def setUpClass(cls): # noqa @@ -216,7 +316,7 @@ class BurninTests(unittest.TestCase): def test_000_clients_setup(self): """Initializing astakos/cyclades/pithos clients""" # Update class attributes - self.clients.initialize_clients() + self.clients.initialize_clients(ignore_ssl=self.ignore_ssl) self.info("Astakos auth url is %s", self.clients.auth_url) self.info("Cyclades url is %s", self.clients.compute_url) self.info("Network url is %s", self.clients.network_url) @@ -224,24 +324,27 @@ class BurninTests(unittest.TestCase): self.info("Image url is %s", self.clients.image_url) self.quotas = self._get_quotas() - self.info(" Disk usage is %s bytes", - self.quotas['system']['cyclades.disk']['usage']) - self.info(" VM usage is %s", - self.quotas['system']['cyclades.vm']['usage']) - self.info(" DiskSpace usage is %s bytes", - self.quotas['system']['pithos.diskspace']['usage']) - self.info(" Ram usage is %s bytes", - self.quotas['system']['cyclades.ram']['usage']) - self.info(" Floating IPs usage is %s", - self.quotas['system']['cyclades.floating_ip']['usage']) - self.info(" CPU usage is %s", - self.quotas['system']['cyclades.cpu']['usage']) - self.info(" Network usage is %s", - self.quotas['system']['cyclades.network.private']['usage']) + for puuid, quotas in self.quotas.items(): + project_name = self._get_project_name(puuid) + self.info(" Project %s:", project_name) + self.info(" Disk usage is %s bytes", + quotas['cyclades.disk']['usage']) + self.info(" VM usage is %s", + quotas['cyclades.vm']['usage']) + self.info(" DiskSpace usage is %s bytes", + quotas['pithos.diskspace']['usage']) + self.info(" Ram usage is %s bytes", + quotas['cyclades.ram']['usage']) + self.info(" Floating IPs usage is %s", + quotas['cyclades.floating_ip']['usage']) + self.info(" CPU usage is %s", + quotas['cyclades.cpu']['usage']) + self.info(" Network usage is %s", + quotas['cyclades.network.private']['usage']) def _run_tests(self, tcases): """Run some generated testcases""" - global success # Using global. pylint: disable-msg=C0103,W0603,W0602 + global success # pylint: disable=invalid-name, global-statement for tcase in tcases: self.info("Running testsuite %s", tcase.__name__) @@ -270,15 +373,17 @@ class BurninTests(unittest.TestCase): def error(self, msg, *args): """Pass the section value to logger""" logger.error(self.suite_name, msg, *args) + self.fail(msg % args) # ---------------------------------- # Helper functions that every testsuite may need def _get_uuid(self): """Get our uuid""" - authenticate = self.clients.astakos.authenticate() - uuid = authenticate['access']['user']['id'] - self.info("User's uuid is %s", uuid) - return uuid + if self.uuid is None: + authenticate = self.clients.astakos.authenticate() + self.uuid = authenticate['access']['user']['id'] + self.info("User's uuid is %s", self.uuid) + return self.uuid def _get_username(self): """Get our User Name""" @@ -301,6 +406,69 @@ class BurninTests(unittest.TestCase): except OSError: pass + def _create_large_file(self, size): + """Create a large file at fs""" + named_file = NamedTemporaryFile() + seg = size / 8 + self.debug('Create file %s ', named_file.name) + for sbytes in [b * seg for b in range(size / seg)]: + named_file.seek(sbytes) + named_file.write(urandom(seg)) + named_file.flush() + named_file.seek(0) + return named_file + + def _create_file(self, size): + """Create a file and compute its merkle hash""" + + tmp_file = NamedTemporaryFile() + self.debug('\tCreate file %s ' % tmp_file.name) + meta = self.clients.pithos.get_container_info() + block_size = int(meta['x-container-block-size']) + block_hash_algorithm = meta['x-container-block-hash'] + num_of_blocks = size / block_size + hashmap = HashMap(block_size, block_hash_algorithm) + s = 0 + for i in range(num_of_blocks): + seg = urandom(block_size) + tmp_file.write(seg) + hashmap.load(seg) + s += len(seg) + else: + rest = size - s + if rest: + seg = urandom(rest) + tmp_file.write(seg) + hashmap.load(seg) + s += len(seg) + tmp_file.seek(0) + tmp_file.hash = hexlify(hashmap.hash()) + return tmp_file + + def _create_boring_file(self, num_of_blocks): + """Create a file with some blocks being the same""" + + def chargen(): + """10 + 2 * 26 + 26 = 88""" + while True: + for char in xrange(10): + yield '%s' % char + for char in ascii_letters: + yield char + for char in '~!@#$%^&*()_+`-=:";|<>?,./': + yield char + + tmp_file = NamedTemporaryFile() + self.debug('\tCreate file %s ' % tmp_file.name) + block_size = 4 * 1024 * 1024 + chars = chargen() + while num_of_blocks: + fslice = 3 if num_of_blocks > 3 else num_of_blocks + tmp_file.write(fslice * block_size * chars.next()) + num_of_blocks -= fslice + tmp_file.seek(0) + return tmp_file + def _get_uuid_of_system_user(self): """Get the uuid of the system user @@ -319,7 +487,6 @@ class BurninTests(unittest.TestCase): return su_value else: self.error("Unrecognized system-user type %s", su_type) - self.fail("Unrecognized system-user type") except ValueError: msg = "Invalid system-user format: %s. Must be [id|name]:.+" self.warning(msg, self.system_user) @@ -343,6 +510,12 @@ class BurninTests(unittest.TestCase): self.info("Test skipped: %s" % msg) self.skipTest(msg) + def _skip_suite_if(self, condition, msg): + """Skip the whole testsuite""" + if condition: + self.info("TestSuite skipped: %s" % msg) + self.skipTest("__SkipClass__: %s" % msg) + # ---------------------------------- # Flavors def _get_list_of_flavors(self, detail=False): @@ -398,7 +571,6 @@ class BurninTests(unittest.TestCase): [f for f in flavors if str(f['id']) == flv_value] else: self.error("Unrecognized flavor type %s", flv_type) - self.fail("Unrecognized flavor type") # Get only flavors that are allowed to create a machine filtered_flvs = [f for f in filtered_flvs @@ -434,7 +606,8 @@ class BurninTests(unittest.TestCase): su_uuid = self._get_uuid_of_system_user() my_uuid = self._get_uuid() ret_images = [i for i in images - if i['owner'] == su_uuid or i['owner'] == my_uuid] + if (i['owner'] == su_uuid or i['owner'] == my_uuid) + and not i['is_snapshot']] return ret_images @@ -473,7 +646,6 @@ class BurninTests(unittest.TestCase): i['id'].lower() == img_value.lower()] else: self.error("Unrecognized image type %s", img_type) - self.fail("Unrecognized image type") # Append and continue ret_images.extend(filtered_imgs) @@ -514,62 +686,79 @@ class BurninTests(unittest.TestCase): assert container, "No pithos container was given" self.info("Creating pithos container %s", container) - self.clients.pithos.container = container - self.clients.pithos.container_put() + self.clients.pithos.create_container(container) + self.temp_containers.append(container) # ---------------------------------- # Quotas def _get_quotas(self): """Get quotas""" self.info("Getting quotas") - return self.clients.astakos.get_quotas() + return dict(self.clients.astakos.get_quotas()) + + # pylint: disable=invalid-name + # pylint: disable=too-many-arguments + def _check_quotas(self, changes): + """Check that quotas' changes are consistent + + @param changes: A dict of the changes that have been made in quotas + + """ + def dicts_are_equal(d1, d2): + """Helper function to check dict equality""" + self.assertEqual(set(d1), set(d2)) + for key, val in d1.items(): + if isinstance(val, (list, tuple)): + self.assertEqual(set(val), set(d2[key])) + elif isinstance(val, dict): + dicts_are_equal(val, d2[key]) + else: + self.assertEqual(val, d2[key]) - # Invalid argument name. pylint: disable-msg=C0103 - # Too many arguments. pylint: disable-msg=R0913 - def _check_quotas(self, disk=None, vm=None, diskspace=None, - ram=None, ip=None, cpu=None, network=None): - """Check that quotas' changes are consistent""" - assert any(v is None for v in - [disk, vm, diskspace, ram, ip, cpu, network]), \ - "_check_quotas require arguments" + if not changes: + return self.info("Check that quotas' changes are consistent") old_quotas = self.quotas new_quotas = self._get_quotas() self.quotas = new_quotas - # Check Disk usage - self._check_quotas_aux( - old_quotas, new_quotas, 'cyclades.disk', disk) - # Check VM usage - self._check_quotas_aux( - old_quotas, new_quotas, 'cyclades.vm', vm) - # Check DiskSpace usage - self._check_quotas_aux( - old_quotas, new_quotas, 'pithos.diskspace', diskspace) - # Check Ram usage - self._check_quotas_aux( - old_quotas, new_quotas, 'cyclades.ram', ram) - # Check Floating IPs usage - self._check_quotas_aux( - old_quotas, new_quotas, 'cyclades.floating_ip', ip) - # Check CPU usage - self._check_quotas_aux( - old_quotas, new_quotas, 'cyclades.cpu', cpu) - # Check Network usage - self._check_quotas_aux( - old_quotas, new_quotas, 'cyclades.network.private', network) - - def _check_quotas_aux(self, old_quotas, new_quotas, resource, value): - """Auxiliary function for _check_quotas""" - old_value = old_quotas['system'][resource]['usage'] - new_value = new_quotas['system'][resource]['usage'] - if value is not None: - assert isinstance(value, int), \ - "%s value has to be integer" % resource - old_value += value - self.assertEqual(old_value, new_value, - "%s quotas don't match" % resource) + self.assertListEqual(sorted(old_quotas.keys()), + sorted(new_quotas.keys())) + + # Take old_quotas and apply changes + for prj, values in changes.items(): + self.assertIn(prj, old_quotas.keys()) + for q_name, q_mult, q_value, q_unit in values: + if q_unit is None: + q_unit = 1 + q_value = q_mult*int(q_value)*q_unit + assert isinstance(q_value, int), \ + "Project %s: %s value has to be integer" % (prj, q_name) + old_quotas[prj][q_name]['usage'] += q_value + old_quotas[prj][q_name]['project_usage'] += q_value + + dicts_are_equal(old_quotas, new_quotas) + + # ---------------------------------- + # Projects + def _get_project_name(self, puuid): + """Get the name of a project""" + uuid = self._get_uuid() + if puuid == uuid: + return "base" + else: + project_info = self.clients.astakos.get_project(puuid) + return project_info['name'] + + def _get_merkle_hash(self, data): + self.clients.pithos._assert_account() + meta = self.clients.pithos.get_container_info() + block_size = int(meta['x-container-block-size']) + block_hash_algorithm = meta['x-container-block-hash'] + hashes = HashMap(block_size, block_hash_algorithm) + hashes.load(data) + return hexlify(hashes.hash()) # -------------------------------------------------------------------- @@ -581,7 +770,7 @@ def initialize(opts, testsuites, stale_testsuites): """ # Initialize logger - global logger # Using global statement. pylint: disable-msg=C0103,W0603 + global logger # pylint: disable=invalid-name, global-statement curr_time = datetime.datetime.now() logger = Log(opts.log_folder, verbose=opts.verbose, use_colors=opts.use_colors, in_parallel=False, @@ -592,6 +781,7 @@ def initialize(opts, testsuites, stale_testsuites): Clients.token = opts.token # Pass the rest options to BurninTests + BurninTests.ignore_ssl = opts.ignore_ssl BurninTests.use_ipv6 = opts.use_ipv6 BurninTests.action_timeout = opts.action_timeout BurninTests.action_warning = opts.action_warning @@ -604,6 +794,9 @@ def initialize(opts, testsuites, stale_testsuites): BurninTests.failfast = opts.failfast BurninTests.run_id = SNF_TEST_PREFIX + \ datetime.datetime.strftime(curr_time, "%Y%m%d%H%M%S") + BurninTests.obj_upload_num = opts.obj_upload_num + BurninTests.obj_upload_min_size = opts.obj_upload_min_size + BurninTests.obj_upload_max_size = opts.obj_upload_max_size # Choose tests to run if opts.show_stale: @@ -623,7 +816,8 @@ def initialize(opts, testsuites, stale_testsuites): # Run Burnin def run_burnin(testsuites, failfast=False): """Run burnin testsuites""" - # Using global. pylint: disable-msg=C0103,W0603,W0602 + # pylint: disable=invalid-name,global-statement + # pylint: disable=global-variable-not-assigned global logger, success success = True @@ -638,7 +832,9 @@ def run_burnin(testsuites, failfast=False): def run_tests(tcases, failfast=False): """Run some testcases""" - global success # Using global. pylint: disable-msg=C0103,W0603,W0602 + # pylint: disable=invalid-name,global-statement + # pylint: disable=global-variable-not-assigned + global success for tcase in tcases: was_success = run_test(tcase) diff --git a/snf-tools/synnefo_tools/burnin/cyclades_common.py b/snf-tools/synnefo_tools/burnin/cyclades_common.py index 79870ad06baa17c24596fb63ef74db50221bd8e2..0c06a682239dc1c512474007feb97369a77b0ab8 100644 --- a/snf-tools/synnefo_tools/burnin/cyclades_common.py +++ b/snf-tools/synnefo_tools/burnin/cyclades_common.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. """ Utility functions for Cyclades Tests @@ -49,12 +31,38 @@ import subprocess from kamaki.clients import ClientError -from synnefo_tools.burnin.common import BurninTests, MB, GB +from synnefo_tools.burnin.common import BurninTests, MB, GB, QADD, QREMOVE, \ + QDISK, QVM, QRAM, QIP, QCPU, QNET -# Too many public methods. pylint: disable-msg=R0904 +# pylint: disable=too-many-public-methods class CycladesTests(BurninTests): """Extends the BurninTests class for Cyclades""" + def _parse_images(self): + """Find images given to command line""" + if self.images is None: + self.info("No --images given. Will use the default %s", + "^Debian Base$") + filters = ["name:^Debian Base$"] + else: + filters = self.images + avail_images = self._find_images(filters) + self.info("Found %s images to choose from", len(avail_images)) + return avail_images + + def _parse_flavors(self): + """Find flavors given to command line""" + flavors = self._get_list_of_flavors(detail=True) + + if self.flavors is None: + self.info("No --flavors given. Will use all of them") + avail_flavors = flavors + else: + avail_flavors = self._find_flavors(self.flavors, flavors=flavors) + + self.info("Found %s flavors to choose from", len(avail_flavors)) + return avail_flavors + def _try_until_timeout_expires(self, opmsg, check_fun): """Try to perform an action until timeout expires""" assert callable(check_fun), "Not a function" @@ -90,6 +98,23 @@ class CycladesTests(BurninTests): opmsg, int(time.time()) - start_time) self.fail("time out") + def _try_once(self, opmsg, check_fun, should_fail=False): + """Try to perform an action once""" + assert callable(check_fun), "Not a function" + ret_value = None + failed = False + try: + ret_value = check_fun() + except Retry: + failed = True + + if failed and not should_fail: + self.error("Operation `%s' failed", opmsg) + elif not failed and should_fail: + self.error("Operation `%s' should have failed", opmsg) + else: + return ret_value + def _get_list_of_servers(self, detail=False): """Get (detailed) list of servers""" if detail: @@ -113,23 +138,27 @@ class CycladesTests(BurninTests): server['name'], server['id']) return self.clients.cyclades.get_server_details(server['id']) - def _create_server(self, image, flavor, personality=None, network=False): + # pylint: disable=too-many-arguments + def _create_server(self, image, flavor, personality=None, + network=False, project_id=None): """Create a new server""" if network: - fip = self._create_floating_ip() + fip = self._create_floating_ip(project_id=project_id) port = self._create_port(fip['floating_network_id'], floating_ip=fip) networks = [{'port': port['id']}] else: networks = None - servername = "%s for %s" % (self.run_id, image['name']) + name = image.get('name', image.get('display_name', '')) + servername = "%s for %s" % (self.run_id, name) self.info("Creating a server with name %s", servername) - self.info("Using image %s with id %s", image['name'], image['id']) + self.info("Using image %s with id %s", name, image['id']) self.info("Using flavor %s with id %s", flavor['name'], flavor['id']) server = self.clients.cyclades.create_server( servername, flavor['id'], image['id'], - personality=personality, networks=networks) + personality=personality, networks=networks, + project_id=project_id) self.info("Server id: %s", server['id']) self.info("Server password: %s", server['adminPass']) @@ -138,12 +167,18 @@ class CycladesTests(BurninTests): self.assertEqual(server['flavor']['id'], flavor['id']) self.assertEqual(server['image']['id'], image['id']) self.assertEqual(server['status'], "BUILD") + if project_id is None: + project_id = self._get_uuid() + self.assertEqual(server['tenant_id'], project_id) # Verify quotas - self._check_quotas(disk=+int(flavor['disk'])*GB, - vm=+1, - ram=+int(flavor['ram'])*MB, - cpu=+int(flavor['vcpus'])) + changes = \ + {project_id: + [(QDISK, QADD, flavor['disk'], GB), + (QVM, QADD, 1, None), + (QRAM, QADD, flavor['ram'], MB), + (QCPU, QADD, flavor['vcpus'], None)]} + self._check_quotas(changes) return server @@ -180,26 +215,25 @@ class CycladesTests(BurninTests): self.assertNotIn(srv['id'], new_servers) # Verify quotas - flavors = \ - [self.clients.compute.get_flavor_details(srv['flavor']['id']) - for srv in servers] - self._verify_quotas_deleted(flavors) + self._verify_quotas_deleted(servers) - def _verify_quotas_deleted(self, flavors): + def _verify_quotas_deleted(self, servers): """Verify quotas for a number of deleted servers""" - used_disk = 0 - used_vm = 0 - used_ram = 0 - used_cpu = 0 - for flavor in flavors: - used_disk += int(flavor['disk']) * GB - used_vm += 1 - used_ram += int(flavor['ram']) * MB - used_cpu += int(flavor['vcpus']) - self._check_quotas(disk=-used_disk, - vm=-used_vm, - ram=-used_ram, - cpu=-used_cpu) + changes = dict() + for server in servers: + project = server['tenant_id'] + if project not in changes: + changes[project] = [] + flavor = \ + self.clients.compute.get_flavor_details(server['flavor']['id']) + new_changes = [ + (QDISK, QREMOVE, flavor['disk'], GB), + (QVM, QREMOVE, 1, None), + (QRAM, QREMOVE, flavor['ram'], MB), + (QCPU, QREMOVE, flavor['vcpus'], None)] + changes[project].extend(new_changes) + + self._check_quotas(changes) def _get_connection_username(self, server): """Determine the username to use to connect to the server""" @@ -244,6 +278,42 @@ class CycladesTests(BurninTests): opmsg = opmsg % (server['name'], server['id'], new_status) self._try_until_timeout_expires(opmsg, check_fun) + def _insist_on_snapshot_transition(self, snapshot, + curr_statuses, new_status): + """Insist on snapshot transiting from curr_statuses to new_status""" + def check_fun(): + """Check snapstho status""" + snap = \ + self.clients.block_storage.get_snapshot_details(snapshot['id']) + if snap['status'] in curr_statuses: + raise Retry() + elif snap['status'] == new_status: + return + else: + msg = "Snapshot \"%s\" with id %s went to unexpected status %s" + self.error(msg, snapshot['display_name'], + snapshot['id'], snap['status']) + opmsg = "Waiting for snapshot \"%s\" with id %s to become %s" + self.info(opmsg, snapshot['display_name'], snapshot['id'], new_status) + opmsg = opmsg % (snapshot['display_name'], snapshot['id'], new_status) + self._try_until_timeout_expires(opmsg, check_fun) + + def _insist_on_snapshot_deletion(self, snapshot_id): + """Insist on snapshot deletion""" + def check_fun(): + """Check snapshot details""" + try: + self.clients.block_storage.get_snapshot_details(snapshot_id) + except ClientError as err: + if err.status != 404: + raise + else: + raise Retry() + opmsg = "Waiting for snapshot %s to be deleted" + self.info(opmsg, snapshot_id) + opmsg = opmsg % snapshot_id + self._try_until_timeout_expires(opmsg, check_fun) + def _insist_on_network_transition(self, network, curr_statuses, new_status): """Insist on network transiting from curr_statuses to new_status""" @@ -320,7 +390,7 @@ class CycladesTests(BurninTests): "Can not get IPs from server attachments") for addr in addrs: - self.assertEquals(IPy.IP(addr).version(), version) + self.assertEqual(IPy.IP(addr).version(), version) if network is None: msg = "Server's public IPv%s is %s" @@ -332,7 +402,7 @@ class CycladesTests(BurninTests): self.info(msg, version, network['id'], addr) return addrs - def _insist_on_ping(self, ip_addr, version=4): + def _insist_on_ping(self, ip_addr, version=4, should_fail=False): """Test server responds to a single IPv4 of IPv6 ping""" def check_fun(): """Ping to server""" @@ -349,14 +419,17 @@ class CycladesTests(BurninTests): opmsg = "Sent IPv%s ping requests to %s" self.info(opmsg, version, ip_addr) opmsg = opmsg % (version, ip_addr) - self._try_until_timeout_expires(opmsg, check_fun) + if should_fail: + self._try_once(opmsg, check_fun, should_fail=True) + else: + self._try_until_timeout_expires(opmsg, check_fun) def _image_is(self, image, osfamily): """Return true if the image is of `osfamily'""" d_image = self.clients.cyclades.get_image_details(image['id']) return d_image['metadata']['osfamily'].lower().find(osfamily) >= 0 - # Method could be a function. pylint: disable-msg=R0201 + # pylint: disable=no-self-use def _ssh_execute(self, hostip, username, password, command): """Execute a command via ssh""" ssh = paramiko.SSHClient() @@ -364,10 +437,8 @@ class CycladesTests(BurninTests): try: ssh.connect(hostip, username=username, password=password) except paramiko.SSHException as err: - if err.args[0] == "Error reading SSH protocol banner": - raise Retry() - else: - raise + self.warning("%s", err.message) + raise Retry() _, stdout, _ = ssh.exec_command(command) status = stdout.channel.recv_exit_status() output = stdout.readlines() @@ -393,39 +464,54 @@ class CycladesTests(BurninTests): self.info("Server's hostname is %s", hostname) return hostname - # Too many arguments. pylint: disable-msg=R0913 + # pylint: disable=too-many-arguments def _check_file_through_ssh(self, hostip, username, password, remotepath, content): """Fetch file from server and compare contents""" - self.info("Fetching file %s from remote server", remotepath) - transport = paramiko.Transport((hostip, 22)) - transport.connect(username=username, password=password) - with tempfile.NamedTemporaryFile() as ftmp: - sftp = paramiko.SFTPClient.from_transport(transport) - sftp.get(remotepath, ftmp.name) - sftp.close() - transport.close() - self.info("Comparing file contents") - remote_content = base64.b64encode(ftmp.read()) - self.assertEqual(content, remote_content) + def check_fun(): + """Fetch file""" + try: + transport = paramiko.Transport((hostip, 22)) + transport.connect(username=username, password=password) + with tempfile.NamedTemporaryFile() as ftmp: + sftp = paramiko.SFTPClient.from_transport(transport) + sftp.get(remotepath, ftmp.name) + sftp.close() + transport.close() + self.info("Comparing file contents") + remote_content = base64.b64encode(ftmp.read()) + self.assertEqual(content, remote_content) + except paramiko.SSHException as err: + self.warning("%s", err.message) + raise Retry() + opmsg = "Fetching file %s from remote server" % remotepath + self.info(opmsg) + self._try_until_timeout_expires(opmsg, check_fun) # ---------------------------------- # Networks - def _create_network(self, cidr="10.0.1.0/28", dhcp=True): + def _create_network(self, cidr="10.0.1.0/28", dhcp=True, + project_id=None): """Create a new private network""" name = self.run_id network = self.clients.network.create_network( - "MAC_FILTERED", name=name, shared=False) + "MAC_FILTERED", name=name, shared=False, + project_id=project_id) self.info("Network with id %s created", network['id']) subnet = self.clients.network.create_subnet( network['id'], cidr=cidr, enable_dhcp=dhcp) self.info("Subnet with id %s created", subnet['id']) # Verify quotas - self._check_quotas(network=+1) + if project_id is None: + project_id = self._get_uuid() + changes = \ + {project_id: [(QNET, QADD, 1, None)]} + self._check_quotas(changes) - #Test if the right name is assigned + # Test if the right name is assigned self.assertEqual(network['name'], name) + self.assertEqual(network['tenant_id'], project_id) return network @@ -450,7 +536,9 @@ class CycladesTests(BurninTests): self.assertNotIn(net['id'], new_networks) # Verify quotas - self._check_quotas(network=-len(networks)) + changes = \ + {self._get_uuid(): [(QNET, QREMOVE, len(networks), None)]} + self._check_quotas(changes) def _get_public_networks(self, networks=None): """Get the public networks""" @@ -466,14 +554,15 @@ class CycladesTests(BurninTests): "Could not find a public network to use") return public_networks - def _create_floating_ip(self): + def _create_floating_ip(self, project_id=None): """Create a new floating ip""" pub_nets = self._get_public_networks() for pub_net in pub_nets: self.info("Creating a new floating ip for network with id %s", pub_net['id']) try: - fip = self.clients.network.create_floatingip(pub_net['id']) + fip = self.clients.network.create_floatingip( + pub_net['id'], project_id=project_id) except ClientError as err: self.warning("%s: %s", err.message, err.details) continue @@ -481,8 +570,13 @@ class CycladesTests(BurninTests): fips = self.clients.network.list_floatingips() fips = [f['id'] for f in fips] self.assertIn(fip['id'], fips) + # Verify quotas - self._check_quotas(ip=+1) + if project_id is None: + project_id = self._get_uuid() + changes = \ + {project_id: [(QIP, QADD, 1, None)]} + self._check_quotas(changes) # Check that IP is IPv4 self.assertEquals(IPy.IP(fip['floating_ip_address']).version(), 4) @@ -603,8 +697,51 @@ class CycladesTests(BurninTests): list_ips = [f['id'] for f in self.clients.network.list_floatingips()] for fip in fips: self.assertNotIn(fip['id'], list_ips) + # Verify quotas - self._check_quotas(ip=-len(fips)) + changes = dict() + for fip in fips: + project = fip['tenant_id'] + if project not in changes: + changes[project] = [] + changes[project].append((QIP, QREMOVE, 1, None)) + self._check_quotas(changes) + + def _find_project(self, flavors, projects=None): + """Return a pair of flavor, project that we can use""" + if projects is None: + projects = self.quotas.keys() + + # XXX: Well there seems to be no easy way to find how many resources + # we have left in a project (we have to substract usage from limit, + # check both per_user and project quotas, blah, blah). For now + # just return the first flavor with the first project and lets hope + # that it fits. + return (flavors[0], projects[0]) + + # # Get only the quotas for the given 'projects' + # quotas = dict() + # for prj, qts in self.quotas.items(): + # if prj in projects: + # quotas[prj] = qts + # + # results = [] + # for flv in flavors: + # for prj, qts in quotas.items(): + # self.debug("Testing flavor %s, project %s", flv['name'], prj) + # condition = \ + # (flv['ram'] <= qts['cyclades.ram']['usage'] and + # flv['vcpus'] <= qts['cyclades.cpu']['usage'] and + # flv['disk'] <= qts['cyclades.disk']['usage'] and + # qts['cyclades.vm']['usage'] >= 1) + # if condition: + # results.append((flv, prj)) + # + # if not results: + # msg = "Couldn't find a suitable flavor to use for current qutoas" + # self.error(msg) + # + # return random.choice(results) class Retry(Exception): diff --git a/snf-tools/synnefo_tools/burnin/images_tests.py b/snf-tools/synnefo_tools/burnin/images_tests.py index 8e33cbade3e1189f03527ea0e97ed36337023405..1eb7b24cc4b5b4c5d70018933227a65bdafa3277 100644 --- a/snf-tools/synnefo_tools/burnin/images_tests.py +++ b/snf-tools/synnefo_tools/burnin/images_tests.py @@ -1,36 +1,18 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. """ This is the burnin class that tests the Flavors/Images functionality @@ -42,10 +24,11 @@ import shutil from kamaki.clients import ClientError -from synnefo_tools.burnin.common import BurninTests, Proper +from synnefo_tools.burnin.common import BurninTests, Proper, \ + QPITHOS, QADD, QREMOVE -# Too many public methods. pylint: disable-msg=R0904 +# pylint: disable=too-many-public-methods class FlavorsTestSuite(BurninTests): """Test flavor lists for consistency""" simple_flavors = Proper(value=None) @@ -89,7 +72,7 @@ class FlavorsTestSuite(BurninTests): # -------------------------------------------------------------------- -# Too many public methods. pylint: disable-msg=R0904 +# pylint: disable=too-many-public-methods class ImagesTestSuite(BurninTests): """Test image lists for consistency""" simple_images = Proper(value=None) @@ -130,7 +113,8 @@ class ImagesTestSuite(BurninTests): """Test every system image has specific metadata defined""" keys = frozenset(["osfamily", "root_partition"]) for i in self.system_images: - self.assertTrue(keys.issubset(i['properties'].keys())) + self.assertTrue(keys.issubset(i['properties'].keys()), + "Failed in image with id '%s'" % i['id']) def test_007_download_image(self): """Download image from Pithos""" @@ -166,12 +150,15 @@ class ImagesTestSuite(BurninTests): """Upload the image to Pithos""" self._set_pithos_account(self._get_uuid()) self._create_pithos_container("burnin-images") + self._set_pithos_container("burnin-images") file_size = os.path.getsize(self.temp_image_file) with open(self.temp_image_file, "r+b") as fin: self.clients.pithos.upload_object(self.temp_image_name, fin) # Verify quotas - self._check_quotas(diskspace=file_size) + changes = \ + {self._get_uuid(): [(QPITHOS, QADD, file_size, None)]} + self._check_quotas(changes) def test_009_register_image(self): """Register image to Plankton""" @@ -191,14 +178,81 @@ class ImagesTestSuite(BurninTests): self.assertEqual(len(images), 1) self.info("Image registered with id %s", images[0]['id']) + self.info("Registering with unicode name") + uni_str = u'\u03b5\u03b9\u03ba\u03cc\u03bd\u03b1' + uni_name = u'%s, or %s in Greek' % (self.temp_image_name, uni_str) + img = self.clients.image.register( + uni_name, location, params, properties) + + self.info('Checking if image with unicode name exists') + found_img = self.clients.image.get_meta(img['id']) + self.assertEqual(found_img['name'], uni_name) + self.info("Image registered with id %s", found_img['id']) + + self.info("Checking if image is listed " + "under the specific container in pithos") + self._set_pithos_account(self._get_uuid()) + pithos = self.clients.pithos + pithos.container = 'burnin-images' + self.assertTrue(self.temp_image_name in ( + o['name'] for o in pithos.list_objects())) + + self.info("Checking copying image to " + "another pithos container.") + pithos.container = other_container = 'burnin-images-backup' + pithos.create_container() + pithos.copy_object( + src_container='burnin-images', + src_object=self.temp_image_name, + dst_container=other_container, + dst_object='%s_copy' % self.temp_image_name) + + # Verify quotas + file_size = os.path.getsize(self.temp_image_file) + changes = \ + {self._get_uuid(): [(QPITHOS, QADD, file_size, None)]} + self._check_quotas(changes) + + self.info("Checking copied image " + "is listed among the images.") + images = self._get_list_of_images(detail=True) + locations = [i['location'] for i in images] + location2 = "pithos://" + self._get_uuid() + \ + "/burnin-images-backup/" + '%s_copy' % self.temp_image_name + self.assertTrue(location2 in locations) + + self.info("Set image metadata in the pithos domain") + pithos.set_object_meta('%s_copy' % self.temp_image_name, + {'foo': 'bar'}) + + self.info("Checking copied image " + "is still listed among the images.") + images = self._get_list_of_images(detail=True) + locations = [i['location'] for i in images] + location2 = "pithos://" + self._get_uuid() + \ + "/burnin-images-backup/" + '%s_copy' % self.temp_image_name + self.assertTrue(location2 in locations) + + # delete copied object + self.clients.pithos.del_object('%s_copy' % self.temp_image_name) + + # Verify quotas + file_size = os.path.getsize(self.temp_image_file) + changes = \ + {self._get_uuid(): [(QPITHOS, QREMOVE, file_size, None)]} + self._check_quotas(changes) + def test_010_cleanup_image(self): """Remove uploaded image from Pithos""" # Remove uploaded image self.info("Deleting uploaded image %s", self.temp_image_name) + self._set_pithos_container("burnin-images") self.clients.pithos.del_object(self.temp_image_name) # Verify quotas file_size = os.path.getsize(self.temp_image_file) - self._check_quotas(diskspace=-file_size) + changes = \ + {self._get_uuid(): [(QPITHOS, QREMOVE, file_size, None)]} + self._check_quotas(changes) self.temp_image_name = None # Remove temp directory self.info("Deleting temp directory %s", self.temp_dir) @@ -208,9 +262,11 @@ class ImagesTestSuite(BurninTests): @classmethod def tearDownClass(cls): # noqa """Clean up""" - if cls.temp_image_name is not None: + for container in ["burnin-images", "burnin-images-backup"]: + cls.clients.pithos.container = container try: - cls.clients.pithos.del_object(cls.temp_image_name) + cls.clients.pithos.del_container(delimiter='/') + cls.clients.pithos.purge_container(container) except ClientError: pass diff --git a/snf-tools/synnefo_tools/burnin/logger.py b/snf-tools/synnefo_tools/burnin/logger.py index d90318d111d79e2b1bc57a14779fb02f69791b3a..66fcf937410172f2e626d9d5bd67c9f79b4f3935 100644 --- a/snf-tools/synnefo_tools/burnin/logger.py +++ b/snf-tools/synnefo_tools/burnin/logger.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. """ This is the logging class for burnin @@ -152,7 +134,7 @@ def _locate_input(contents, section): # We will add our message in this location for (index, obj) in enumerate(sect_locs): if section in contents[obj]: - return sect_locs[index+1] - 3 + return sect_locs[index + 1] - 3 # We didn't find our section?? sys.stderr.write("Section %s could not be found in logging file\n" @@ -220,7 +202,7 @@ class Log(object): """ # ---------------------------------- - # Too many arguments. pylint: disable-msg=R0913 + # pylint: disable=too-many-arguments def __init__(self, output_dir, verbose=1, use_colors=True, in_parallel=False, log_level=0, curr_time=None): """Initialize our loggers diff --git a/snf-tools/synnefo_tools/burnin/network_tests.py b/snf-tools/synnefo_tools/burnin/network_tests.py index 69387cbd405ab3582a73cf6796510504913efaef..183ab4c3b666d953d6dabedb7c7f86d1d32a1f16 100644 --- a/snf-tools/synnefo_tools/burnin/network_tests.py +++ b/snf-tools/synnefo_tools/burnin/network_tests.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. """ This is the burnin class that tests the Networks' functionality @@ -42,7 +24,7 @@ from synnefo_tools.burnin.common import Proper from synnefo_tools.burnin.cyclades_common import CycladesTests -# Too many public methods. pylint: disable-msg=R0904 +# pylint: disable=too-many-public-methods class NetworkTestSuite(CycladesTests): """Test Networking in Cyclades""" avail_images = Proper(value=None) @@ -53,27 +35,11 @@ class NetworkTestSuite(CycladesTests): def test_001_images_to_use(self): """Find images to be used to create our machines""" - if self.images is None: - self.info("No --images given. Will use the default %s", - "^Debian Base$") - filters = ["name:^Debian Base$"] - else: - filters = self.images - - self.avail_images = self._find_images(filters) - self.info("Found %s images to choose from", len(self.avail_images)) + self.avail_images = self._parse_images() def test_002_flavors_to_use(self): """Find flavors to be used to create our machines""" - flavors = self._get_list_of_flavors(detail=True) - - if self.flavors is None: - self.info("No --flavors given. Will use all of them") - self.avail_flavors = flavors - else: - self.avail_flavors = self._find_flavors( - self.flavors, flavors=flavors) - self.info("Found %s flavors to choose from", len(self.avail_flavors)) + self.avail_flavors = self._parse_flavors() def test_003_submit_create_server_a(self): """Submit create server request for server A""" diff --git a/snf-tools/synnefo_tools/burnin/pithos_tests.py b/snf-tools/synnefo_tools/burnin/pithos_tests.py index 84b427ca251e1f0eb678904198af7e8b53b8db12..772fc165a4f63c8060ebb80b8c38572df1e248d3 100644 --- a/snf-tools/synnefo_tools/burnin/pithos_tests.py +++ b/snf-tools/synnefo_tools/burnin/pithos_tests.py @@ -1,88 +1,846 @@ -# Copyright 2013 GRNET S.A. All rights reserved. -# -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: -# -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# pylint: disable=too-many-lines + +# Copyright (C) 2010-2014 GRNET S.A. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. +# 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 General Public License for more details. # -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + """ This is the burnin class that tests the Pithos functionality """ +import itertools import os import random import tempfile +from datetime import datetime +from tempfile import NamedTemporaryFile -from synnefo_tools.burnin.common import BurninTests, Proper +from synnefo_tools.burnin.common import BurninTests, Proper, \ + QPITHOS, QADD, QREMOVE, MB +from kamaki.clients import ClientError -# Too many public methods. pylint: disable-msg=R0904 +def sample_block(fid, block): + """Read a block from fid""" + block_size = 4 * 1024 * 1024 + fid.seek(block * block_size) + chars = [fid.read(1)] + fid.seek(block_size / 2, 1) + chars.append(fid.read(1)) + fid.seek((block + 1) * block_size - 1) + chars.append(fid.read(1)) + return chars + + +# pylint: disable=too-many-public-methods class PithosTestSuite(BurninTests): + """Test Pithos functionality""" containers = Proper(value=None) created_container = Proper(value=None) + now_unformated = Proper(value=datetime.utcnow()) + obj_metakey = Proper(value=None) + large_file = Proper(value=None) + temp_local_files = Proper(value=[]) + uvalue = u'\u03c3\u03cd\u03bd\u03bd\u03b5\u03c6\u03bf' - def test_001_list_containers(self): - """Test container list actually returns containers""" + def test_005_account_head(self): + """Test account HEAD""" self._set_pithos_account(self._get_uuid()) - self.containers = self._get_list_of_containers() - self.assertGreater(len(self.containers), 0) - - def test_002_unique_containers(self): - """Test if containers have unique names""" - names = [n['name'] for n in self.containers] - names = sorted(names) - self.assertEqual(sorted(list(set(names))), names) - - def test_003_create_container(self): - """Test creating a new container""" - names = [n['name'] for n in self.containers] - while True: - rand_num = random.randint(1000, 9999) - rand_name = "%s%s" % (self.run_id, rand_num) - self.info("Trying container name %s", rand_name) - if rand_name not in names: + pithos = self.clients.pithos + resp = pithos.account_head() + self.assertEqual(resp.status_code, 204) + self.info('Returns 204') + + resp = pithos.account_head(until='1000000000') + self.assertEqual(resp.status_code, 204) + datestring = unicode(resp.headers['x-account-until-timestamp']) + self.assertEqual(u'Sun, 09 Sep 2001 01:46:40 GMT', datestring) + self.assertTrue(any([ + h.startswith('x-account-policy-quota') for h in resp.headers])) + self.info('Until and account policy quota exist') + + for date_format in pithos.DATE_FORMATS: + now_formated = self.now_unformated.strftime(date_format) + resp1 = pithos.account_head( + if_modified_since=now_formated, success=(204, 304, 412)) + resp2 = pithos.account_head( + if_unmodified_since=now_formated, success=(204, 304, 412)) + self.assertNotEqual(resp1.status_code, resp2.status_code) + self.info('If_(un)modified_since is OK') + + # pylint: disable=too-many-locals + def test_010_account_get(self): + """Test account GET""" + self.info('Preparation') + pithos = self.clients.pithos + for i in range(1, 3): + cont_name = "cont%s_%s%s" % ( + i, self.run_id or 0, random.randint(1000, 9999)) + self._create_pithos_container(cont_name) + pithos.container, obj = cont_name, 'shared_file' + pithos.create_object(obj) + pithos.set_object_sharing(obj, read_permission='*') + self.info('Created object /%s/%s' % (cont_name, obj)) + + # Try to re-create the same container + pithos.create_container(cont_name) + + resp = pithos.list_containers() + full_len = len(resp) + self.assertTrue(full_len >= 2) + self.info('Normal use is OK') + + cnames = [c['name'] for c in resp] + self.assertEqual(sorted(list(set(cnames))), sorted(cnames)) + self.info('Containers have unique names') + + resp = pithos.account_get(limit=1) + self.assertEqual(len(resp.json), 1) + self.info('Limit works') + + resp = pithos.account_get(marker='cont') + cont1 = resp.json[0] + self.info('Marker works') + + resp = pithos.account_get(limit=2, marker='cont') + conames = [container['name'] for container in resp.json if ( + container['name'].lower().startswith('cont'))] + self.assertTrue(cont1['name'] in conames) + self.info('Marker-limit combination works') + + resp = pithos.account_get(show_only_shared=True) + self.assertTrue(cont_name in [c['name'] for c in resp.json]) + self.info('Show-only-shared works') + + resp = pithos.account_get(until=1342609206.0) + self.assertTrue(len(resp.json) <= full_len) + self.info('Until works') + + for date_format in pithos.DATE_FORMATS: + now_formated = self.now_unformated.strftime(date_format) + resp1 = pithos.account_get( + if_modified_since=now_formated, success=(200, 304, 412)) + resp2 = pithos.account_get( + if_unmodified_since=now_formated, success=(200, 304, 412)) + self.assertNotEqual(resp1.status_code, resp2.status_code) + self.info('If_(un)modified_since is OK') + + def test_015_account_post(self): + """Test account POST""" + pithos = self.clients.pithos + resp = pithos.account_post() + self.assertEqual(resp.status_code, 202) + self.info('Status code is OK') + + rand_num = '%s%s' % (self.run_id or 0, random.randint(1000, 9999)) + grp_name = 'grp%s' % rand_num + self.assertRaises( + ClientError, pithos.set_account_group, grp_name, [pithos.account]) + self.info('Invalid group name is handled correctly') + + rand_num = rand_num.replace('-', 'x') + grp_name = 'grp%s' % rand_num + + uuid1, uuid2 = pithos.account, 'invalid-user-uuid-%s' % rand_num + self.assertRaises( + ClientError, pithos.set_account_group, grp_name, [uuid1, uuid2]) + self.info('Invalid uuid is handled correctly') + + pithos.set_account_group(grp_name, [uuid1]) + resp = pithos.get_account_group() + self.assertEqual(resp['x-account-group-' + grp_name], '%s' % uuid1) + self.info('Account group is OK (ascii group name)') + + grp_name_u = '%s%s' % (grp_name, self.uvalue) + pithos.set_account_group(grp_name_u, [uuid1]) + resp = pithos.get_account_group() + self.assertEqual(resp['x-account-group-' + grp_name_u], '%s' % uuid1) + self.info('Account group is OK (unicode group name)') + + pithos.del_account_group(grp_name) + resp = pithos.get_account_group() + self.assertTrue('x-account-group-' + grp_name not in resp) + self.info('Removed account group (ascii)') + + pithos.del_account_group(grp_name_u) + resp = pithos.get_account_group() + self.assertTrue('x-account-group-' + grp_name_u not in resp) + self.info('Removed account group (unicode)') + + mprefix = 'meta%s' % rand_num + pithos.set_account_meta({ + mprefix + '1': 'v1', mprefix + '2': 'v2'}) + resp = pithos.get_account_meta() + self.assertEqual(resp['x-account-meta-' + mprefix + '1'], 'v1') + self.assertEqual(resp['x-account-meta-' + mprefix + '2'], 'v2') + self.info('Account meta is OK (ascii meta key)') + + mprefix_u, vu = '%s%s' % (mprefix, self.uvalue), 'v%s' % self.uvalue + pithos.set_account_meta({mprefix_u: vu}) + resp = pithos.get_account_meta() + self.assertEqual(resp['x-account-meta-' + mprefix_u], vu) + self.info('Account meta is OK (unicode meta key)') + + pithos.del_account_meta(mprefix + '1') + resp = pithos.get_account_meta() + self.assertTrue('x-account-meta-' + mprefix + '1' not in resp) + self.assertTrue('x-account-meta-' + mprefix + '2' in resp) + self.info('Selective removal of account meta is OK (ascii)') + + pithos.del_account_meta(mprefix_u) + resp = pithos.get_account_meta() + self.assertTrue('x-account-meta-' + mprefix_u not in resp) + self.info('Account meta removal is OK (unicode)') + + pithos.del_account_meta(mprefix + '2') + self.info('Metadata cleaned up') + + def test_020_container_head(self): + """Test container HEAD""" + pithos = self.clients.pithos + resp = pithos.container_head() + self.assertEqual(resp.status_code, 204) + self.info('Status code is OK') + + resp = pithos.container_head(until=1000000, success=(204, 404)) + self.assertEqual(resp.status_code, 404) + self.info('Until works') + + for date_format in pithos.DATE_FORMATS: + now_formated = self.now_unformated.strftime(date_format) + resp1 = pithos.container_head( + if_modified_since=now_formated, success=(204, 304, 412)) + resp2 = pithos.container_head( + if_unmodified_since=now_formated, success=(204, 304, 412)) + self.assertNotEqual(resp1.status_code, resp2.status_code) + + k = 'metakey%s' % random.randint(1000, 9999) + pithos.set_container_meta({k: 'our value'}) + resp = pithos.get_container_meta() + k = 'x-container-meta-%s' % k + self.assertIn(k, resp) + self.assertEqual('our value', resp[k]) + self.info('Container meta exists') + + self.obj_metakey = 'metakey%s' % random.randint(1000, 9999) + obj = 'object_with_meta' + pithos.create_object(obj) + pithos.set_object_meta(obj, {self.obj_metakey: 'our value'}) + resp = pithos.get_container_object_meta() + self.assertIn('x-container-object-meta', resp) + self.assertIn( + self.obj_metakey, resp['x-container-object-meta'].lower()) + self.info('Container object meta exists') + + def test_025_container_get(self): + """Test container GET""" + pithos = self.clients.pithos + + resp = pithos.container_get() + self.assertEqual(resp.status_code, 200) + self.info('Status code is OK') + + full_len = len(resp.json) + self.assertGreater(full_len, 0) + self.info('There are enough (%s) containers' % full_len) + + obj1 = 'test%s' % random.randint(1000, 9999) + pithos.create_object(obj1) + obj2 = 'test%s' % random.randint(1000, 9999) + pithos.create_object(obj2) + obj3 = 'another%s.test' % random.randint(1000, 9999) + pithos.create_object(obj3) + + resp = pithos.container_get(prefix='test') + self.assertTrue(len(resp.json) > 1) + test_objects = [o for o in resp.json if o['name'].startswith('test')] + self.assertEqual(len(resp.json), len(test_objects)) + self.info('Prefix is OK') + + resp = pithos.container_get(limit=1) + self.assertEqual(len(resp.json), 1) + self.info('Limit is OK') + + resp = pithos.container_get(marker=obj3[:-5]) + self.assertTrue(len(resp.json) > 1) + aoobjects = [obj for obj in resp.json if obj['name'] > obj3[:-5]] + self.assertEqual(len(resp.json), len(aoobjects)) + self.info('Marker is OK') + + resp = pithos.container_get(prefix=obj3, delimiter='.') + self.assertTrue(full_len > len(resp.json)) + self.info('Delimiter is OK') + + resp = pithos.container_get(path='/') + full_len += 3 + self.assertEqual(full_len, len(resp.json)) + self.info('Path is OK') + + resp = pithos.container_get(format='xml') + self.assertEqual(resp.text.split()[4], + 'name="' + pithos.container + '">') + self.info('Format is OK') + + resp = pithos.container_get(meta=[self.obj_metakey, ]) + self.assertTrue(len(resp.json) > 0) + self.info('Meta is OK') + + resp = pithos.container_get(show_only_shared=True) + self.assertTrue(len(resp.json) < full_len) + self.info('Show-only-shared is OK') + + try: + resp = pithos.container_get(until=1000000000) + datestring = unicode(resp.headers['x-account-until-timestamp']) + self.assertEqual(u'Sun, 09 Sep 2001 01:46:40 GMT', datestring) + self.info('Until is OK') + except ClientError: + pass + + def test_030_container_put(self): + """Test container PUT""" + pithos = self.clients.pithos + pithos.container = 'cont%s%s' % ( + self.run_id or 0, random.randint(1000, 9999)) + self.temp_containers.append(pithos.container) + + resp = pithos.create_container() + self.assertTrue(isinstance(resp, dict)) + + resp = pithos.get_container_limit(pithos.container) + cquota = resp.values()[0] + newquota = 2 * int(cquota) + self.info('Limit is OK') + pithos.del_container() + + resp = pithos.create_container(sizelimit=newquota) + self.assertTrue(isinstance(resp, dict)) + + resp = pithos.get_container_limit(pithos.container) + xquota = int(resp.values()[0]) + self.assertEqual(newquota, xquota) + self.info('Can set container limit') + pithos.del_container() + + resp = pithos.create_container(versioning='auto') + self.assertTrue(isinstance(resp, dict)) + + resp = pithos.get_container_versioning(pithos.container) + nvers = resp.values()[0] + self.assertEqual('auto', nvers) + self.info('Versioning=auto is OK') + pithos.del_container() + + resp = pithos.container_put(versioning='none') + self.assertEqual(resp.status_code, 201) + + resp = pithos.get_container_versioning(pithos.container) + nvers = resp.values()[0] + self.assertEqual('none', nvers) + self.info('Versioning=none is OK') + pithos.del_container() + + mu, vu = 'm%s' % self.uvalue, 'v%s' % self.uvalue + resp = pithos.create_container(metadata={'m1': 'v1', mu: vu}) + self.assertTrue(isinstance(resp, dict)) + + resp = pithos.get_container_meta(pithos.container) + self.assertTrue('x-container-meta-m1' in resp) + self.assertEqual(resp['x-container-meta-m1'], 'v1') + self.assertTrue('x-container-meta-' + mu in resp) + self.assertEqual(resp['x-container-meta-' + mu], vu) + + resp = pithos.container_put(metadata={'m1': '', 'm2': 'v2a'}) + self.assertEqual(resp.status_code, 202) + + resp = pithos.get_container_meta(pithos.container) + self.assertTrue('x-container-meta-m1' not in resp) + self.assertTrue('x-container-meta-m2' in resp) + self.assertEqual(resp['x-container-meta-m2'], 'v2a') + self.info('Container meta is OK (ascii and unicode)') + + pithos.del_container_meta(pithos.container) + + # pylint: disable=too-many-statements + def test_035_container_post(self): + """Test container POST""" + pithos = self.clients.pithos + + resp = pithos.container_post() + self.assertEqual(resp.status_code, 202) + self.info('Status is OK') + + mu, vu = 'm%s' % self.uvalue, 'v%s' % self.uvalue + pithos.set_container_meta({'m1': 'v1', mu: vu}) + resp = pithos.get_container_meta(pithos.container) + self.assertTrue('x-container-meta-m1' in resp) + self.assertEqual(resp['x-container-meta-m1'], 'v1') + self.assertTrue('x-container-meta-' + mu in resp) + self.assertEqual(resp['x-container-meta-' + mu], vu) + self.info('Set metadata works (ascii and unicode)') + + resp = pithos.del_container_meta('m1') + resp = pithos.set_container_meta({mu: 'v2a'}) + resp = pithos.get_container_meta(pithos.container) + self.assertTrue('x-container-meta-m1' not in resp) + self.assertTrue('x-container-meta-' + mu in resp) + self.assertEqual(resp['x-container-meta-' + mu], 'v2a') + self.info('Modify metadata works (ascii and unicode)') + + resp = pithos.get_container_limit(pithos.container) + cquota = resp.values()[0] + newquota = 2 * int(cquota) + resp = pithos.set_container_limit(newquota) + resp = pithos.get_container_limit(pithos.container) + xquota = int(resp.values()[0]) + self.assertEqual(newquota, xquota) + self.info('Set quota works') + + pithos.set_container_versioning('auto') + resp = pithos.get_container_versioning(pithos.container) + nvers = resp.values()[0] + self.assertEqual('auto', nvers) + pithos.set_container_versioning('none') + resp = pithos.get_container_versioning(pithos.container) + nvers = resp.values()[0] + self.assertEqual('none', nvers) + self.info('Set versioning works') + + named_file = self._create_large_file(1024 * 1024 * 100) + self.large_file = named_file + self.info('Created file %s of 100 MB' % named_file.name) + + # Add file to 'temp_local_files' for cleanup + self.temp_local_files.append(named_file.name) + + pithos.create_directory('dir') + self.info('Upload the file ...') + resp = pithos.upload_object('/dir/sample.file', named_file) + for term in ('content-length', 'content-type', 'x-object-version'): + self.assertTrue(term in resp) + resp = pithos.get_object_info('/dir/sample.file') + self.assertTrue(int(resp['content-length']) > 100000000) + self.info('Made remote directory /dir and object /dir/sample.file') + + # TODO: What is tranfer_encoding? What should I check about it? + + size = os.fstat(self.large_file.fileno()).st_size + self._check_quotas({self._get_uuid(): [(QPITHOS, QADD, size, None)]}) + + obj = 'object_with_meta' + pithos.container = self.temp_containers[-2] + resp = pithos.object_post( + obj, update='False', metadata={'newmeta': 'newval'}) + + resp = pithos.get_object_info(obj) + self.assertTrue('x-object-meta-newmeta' in resp) + self.assertFalse('x-object-meta-%s' % self.obj_metakey not in resp) + self.info('Metadata with update=False works') + + def test_040_container_delete(self): + """Test container DELETE""" + pithos = self.clients.pithos + + resp = pithos.container_delete(success=409) + self.assertEqual(resp.status_code, 409) + self.assertRaises(ClientError, pithos.container_delete) + self.info('Successfully failed to delete non-empty container') + + resp = pithos.container_delete(until='1000000000') + self.assertEqual(resp.status_code, 204) + self.info('Successfully failed to delete old-timestamped container') + + obj_names = [o['name'] for o in pithos.container_get().json] + pithos.del_container(delimiter='/') + resp = pithos.container_get() + self.assertEqual(len(resp.json), 0) + self.info('Successfully emptied container') + + for obj in obj_names: + resp = pithos.get_object_versionlist(obj) + self.assertTrue(len(resp) > 0) + self.info('Versions are still there') + + pithos.purge_container() + for obj in obj_names: + self.assertRaises(ClientError, pithos.get_object_versionlist, obj) + self.info('Successfully purged container') + + self.temp_containers.remove(pithos.container) + pithos.container = self.temp_containers[-1] + + def test_045_object_head(self): + """Test object HEAD""" + pithos = self.clients.pithos + + obj = 'dir/sample.file' + resp = pithos.object_head(obj) + self.assertEqual(resp.status_code, 200) + self.info('Status code is OK') + etag = resp.headers['etag'] + real_version = resp.headers['x-object-version'] + + self.assertRaises(ClientError, pithos.object_head, obj, version=-10) + resp = pithos.object_head(obj, version=real_version) + self.assertEqual(resp.headers['x-object-version'], real_version) + self.info('Version works') + + resp = pithos.object_head(obj, if_etag_match=etag) + self.assertEqual(resp.status_code, 200) + self.info('if-etag-match is OK') + + resp = pithos.object_head( + obj, if_etag_not_match=etag, success=(200, 412, 304)) + self.assertNotEqual(resp.status_code, 200) + self.info('if-etag-not-match works') + + resp = pithos.object_head( + obj, version=real_version, if_etag_match=etag, success=200) + self.assertEqual(resp.status_code, 200) + self.info('Version with if-etag-match works') + + for date_format in pithos.DATE_FORMATS: + now_formated = self.now_unformated.strftime(date_format) + resp1 = pithos.object_head( + obj, if_modified_since=now_formated, success=(200, 304, 412)) + resp2 = pithos.object_head( + obj, if_unmodified_since=now_formated, success=(200, 304, 412)) + self.assertNotEqual(resp1.status_code, resp2.status_code) + self.info('if-(un)modified-since works') + + # pylint: disable=too-many-locals + def test_050_object_get(self): + """Test object GET""" + pithos = self.clients.pithos + obj = 'dir/sample.file' + + resp = pithos.object_get(obj) + self.assertEqual(resp.status_code, 200) + self.info('Status code is OK') + + osize = int(resp.headers['content-length']) + etag = resp.headers['etag'] + + resp = pithos.object_get(obj, hashmap=True) + self.assertEqual( + set(('hashes', 'block_size', 'block_hash', 'bytes')), + set(resp.json)) + self.info('Hashmap works') + hash0 = resp.json['hashes'][0] + + resp = pithos.object_get(obj, format='xml', hashmap=True) + self.assertTrue(resp.text.split('hash>')[1].startswith(hash0)) + self.info('Hashmap with XML format works') + + rangestr = 'bytes=%s-%s' % (osize / 3, osize / 2) + resp = pithos.object_get(obj, data_range=rangestr, success=(200, 206)) + partsize = int(resp.headers['content-length']) + self.assertTrue(0 < partsize and partsize <= 1 + osize / 3) + self.info('Range x-y works') + orig = resp.text + + rangestr = 'bytes=%s' % (osize / 3) + resp = pithos.object_get( + obj, data_range=rangestr, if_range=True, success=(200, 206)) + partsize = int(resp.headers['content-length']) + self.assertTrue(partsize, 1 + (osize / 3)) + diff = set(resp.text).symmetric_difference(set(orig[:partsize])) + self.assertEqual(len(diff), 0) + self.info('Range x works') + + rangestr = 'bytes=-%s' % (osize / 3) + resp = pithos.object_get( + obj, data_range=rangestr, if_range=True, success=(200, 206)) + partsize = int(resp.headers['content-length']) + self.assertTrue(partsize, osize / 3) + diff = set(resp.text).symmetric_difference(set(orig[-partsize:])) + self.assertEqual(len(diff), 0) + self.info('Range -x works') + + resp = pithos.object_get(obj, if_etag_match=etag) + self.assertEqual(resp.status_code, 200) + self.info('if-etag-match works') + + resp = pithos.object_get(obj, if_etag_not_match=etag + 'LALALA') + self.assertEqual(resp.status_code, 200) + self.info('if-etag-not-match works') + + for date_format in pithos.DATE_FORMATS: + now_formated = self.now_unformated.strftime(date_format) + resp1 = pithos.object_get( + obj, if_modified_since=now_formated, success=(200, 304, 412)) + resp2 = pithos.object_get( + obj, if_unmodified_since=now_formated, success=(200, 304, 412)) + self.assertNotEqual(resp1.status_code, resp2.status_code) + self.info('if(un)modified-since works') + + obj, dnl_f = 'dir/sample.file', NamedTemporaryFile() + self.info('Download %s as %s ...' % (obj, dnl_f.name)) + pithos.download_object(obj, dnl_f) + self.info('Download is completed') + + f_size = len(orig) + for pos in (0, f_size / 2, f_size - 128): + dnl_f.seek(pos) + self.large_file.seek(pos) + self.assertEqual(self.large_file.read(64), dnl_f.read(64)) + self.info('Sampling shows that files match') + + # Upload a boring file + self.info('Create a boring file of 42 blocks...') + bor_f = self._create_boring_file(42) + # Add file to 'temp_local_files' for cleanup + self.temp_local_files.append(bor_f.name) + trg_fname = 'dir/uploaded.file' + self.info('Now, upload the boring file as %s...' % trg_fname) + pithos.upload_object(trg_fname, bor_f) + self.info('Boring file %s is uploaded as %s' % (bor_f.name, trg_fname)) + + size = os.fstat(bor_f.fileno()).st_size + self._check_quotas({self._get_uuid(): [(QPITHOS, QADD, size, None)]}) + + dnl_f = NamedTemporaryFile() + # Add file to 'temp_local_files' for cleanup + self.temp_local_files.append(dnl_f.name) + self.info('Download boring file as %s' % dnl_f.name) + pithos.download_object(trg_fname, dnl_f) + self.info('File is downloaded') + + for i in range(42): + self.assertEqual(sample_block(bor_f, i), sample_block(dnl_f, i)) + + def test_053_object_put(self): + """Test object PUT""" + pithos = self.clients.pithos + obj = 'sample.file' + + pithos.create_object(obj + '.FAKE') + resp = pithos.get_object_info(obj + '.FAKE') + self.assertEqual( + set(resp['content-type']), set('application/octer-stream')) + self.info('Simple call creates a new object correctly') + ku, vu = 'key%s' % self.uvalue, 'v%s' % self.uvalue + + resp = pithos.object_put( + obj, + data='a', + content_type='application/octer-stream', + permissions=dict( + read=['accX:groupA', 'u1', 'u2'], + write=['u2', 'u3']), + metadata={'key1': 'val1', ku: vu}, + content_encoding='UTF-8', + content_disposition='attachment; filename="fname.ext"') + self.assertEqual(resp.status_code, 201) + self.info('Status code is OK (includes ascii and unicode metas)') + etag = resp.headers['etag'] + + resp = pithos.get_object_info(obj) + self.assertTrue('content-disposition' in resp) + self.assertEqual( + resp['content-disposition'], 'attachment; filename="fname.ext"') + self.info('Content-disposition is OK') + + sharing = resp['x-object-sharing'].split('; ') + self.assertTrue(sharing[0].startswith('read=')) + read = set(sharing[0][5:].split(',')) + self.assertEqual(set(('u1', 'accx:groupa')), read) + self.assertTrue(sharing[1].startswith('write=')) + write = set(sharing[1][6:].split(',')) + self.assertEqual(set(('u2', 'u3')), write) + self.info('Permissions are OK') + + resp = pithos.get_object_meta(obj) + self.assertEqual(resp['x-object-meta-key1'], 'val1') + self.assertEqual(resp['x-object-meta-' + ku], vu) + self.info('Meta are OK (ascii and unicode)') + + pithos.object_put( + obj, + if_etag_match=etag, + data='b', + content_type='application/octet-stream', + public=True) + self.info('If-etag-match is OK') + + self._check_quotas({self._get_uuid(): [(QPITHOS, QADD, 1, None)]}) + + resp = pithos.object_get(obj) + self.assertTrue('x-object-public' in resp.headers) + self.info('Publishing works') + + etag = resp.headers['etag'] + self.assertEqual(resp.text, 'b') + self.info('Remote object content is correct') + + resp = pithos.object_put( + obj, + if_etag_not_match=etag, + data='c', + content_type='application/octet-stream', + success=(201, 412)) + self.assertEqual(resp.status_code, 412) + self.info('If-etag-not-match is OK') + + resp = pithos.get_object_info('dir') + self.assertEqual(resp['content-type'], 'application/directory') + self.info('Directory has been created correctly') + + resp = pithos.object_put( + '%s_v2' % obj, + format=None, + copy_from='/%s/%s' % (pithos.container, obj), + content_encoding='application/octet-stream', + source_account=pithos.account, + content_length=0, + success=201) + self.assertEqual(resp.status_code, 201) + resp1 = pithos.get_object_info(obj) + resp2 = pithos.get_object_info('%s_v2' % obj) + self.assertEqual(resp1['x-object-hash'], resp2['x-object-hash']) + self.info('Object has being copied in same container, OK') + + self._check_quotas({self._get_uuid(): [(QPITHOS, QADD, 1, None)]}) + + pithos.copy_object( + src_container=pithos.container, + src_object=obj, + dst_container=self.temp_containers[-2], + dst_object='%s_new' % obj) + pithos.container = self.temp_containers[-2] + resp1 = pithos.get_object_info('%s_new' % obj) + pithos.container = self.temp_containers[-1] + resp2 = pithos.get_object_info(obj) + self.assertEqual(resp1['x-object-hash'], resp2['x-object-hash']) + self.info('Object has being copied in another container, OK') + + self._check_quotas({self._get_uuid(): [(QPITHOS, QADD, 1, None)]}) + + fromstr = '/%s/%s_new' % (self.temp_containers[-2], obj) + resp = pithos.object_put( + obj, + format=None, + copy_from=fromstr, + content_encoding='application/octet-stream', + source_account=pithos.account, + content_length=0, + success=201) + self.assertEqual(resp.status_code, 201) + self.info('Cross container put accepts content_encoding') + + resp = pithos.get_object_info(obj) + self.assertEqual(resp['etag'], etag) + self.info('Etag is OK') + + resp = pithos.object_put( + '%s_v3' % obj, + format=None, + move_from=fromstr, + content_encoding='application/octet-stream', + source_account='nonExistendAddress@NeverLand.com', + content_length=0, + success=(403, )) + self.info('Fake source account is handled correctly') + + resp1 = pithos.get_object_info(obj) + pithos.container = self.temp_containers[-2] + + target_size_before = 0 + for o in pithos.list_objects(): + if o['name'] == obj + '_new': + target_size_before += o['bytes'] break - self.info("Container name %s already exists", rand_name) - # Create container - self._create_pithos_container(rand_name) - # Verify that container is created - containers = self._get_list_of_containers() - self.info("Verify that container %s is created", rand_name) - names = [n['name'] for n in containers] - self.assertIn(rand_name, names) - # Keep the name of the container so we can remove it - # at cleanup phase, if something goes wrong. - self.created_container = rand_name - def test_004_upload_file(self): + pithos.move_object( + src_container=self.temp_containers[-1], + src_object=obj, + dst_container=pithos.container, + dst_object=obj + '_new') + resp0 = pithos.get_object_info(obj + '_new') + + self.assertEqual(resp1['x-object-hash'], resp0['x-object-hash']) + self.info('Cross container move is OK') + + self._check_quotas( + {self._get_uuid(): [(QPITHOS, QREMOVE, target_size_before, None)]}) + pithos.container = self.temp_containers[-1] + pithos.create_container(versioning='auto') + pithos.upload_from_string(obj, 'first version') + source_hashmap = pithos.get_object_hashmap(obj)['hashes'] + pithos.upload_from_string(obj, 'second version') + pithos.upload_from_string(obj, 'third version') + versions = pithos.get_object_versionlist(obj) + self.assertEqual(len(versions), 3) + vers0 = versions[0][0] + + size = len('third version') + self._check_quotas({self._get_uuid(): [(QPITHOS, QADD, size, None)]}) + + pithos.container = self.temp_containers[-2] + pithos.object_put( + obj, + format=None, + move_from='/%s/%s' % (self.temp_containers[-1], obj), + source_version=vers0, + content_encoding='application/octet-stream', + content_length=0, success=201) + target_hashmap = pithos.get_object_hashmap(obj)['hashes'] + self.info('Source-version is probably not OK (Check bug #4963)') + source_hashmap, target_hashmap = source_hashmap, target_hashmap + # Comment out until it's fixed + # self.assertEqual(source_hashmap, target_hashmap) + # self.info('Source-version is OK') + + mobj = 'manifest.test' + txt = '' + for i in range(10): + txt += '%s' % i + pithos.object_put( + '%s/%s' % (mobj, i), + data='%s' % i, + content_length=1, + success=201, + content_type='application/octet-stream', + content_encoding='application/octet-stream') + pithos.object_put( + mobj, + content_length=0, + content_type='application/octet-stream', + manifest='%s/%s' % (pithos.container, mobj)) + resp = pithos.object_get(mobj) + self.assertEqual(resp.text, txt) + self.info('Manifest file creation works') + self._check_quotas({self._get_uuid(): [(QPITHOS, QADD, 10, None)]}) + + oldf = pithos.get_object_info('sample.file') + named_f = self._create_large_file(1024 * 10) + # Add file to 'temp_local_files' for cleanup + self.temp_local_files.append(named_f.name) + pithos.upload_object('sample.file', named_f) + resp = pithos.get_object_info('sample.file') + self.assertEqual(int(resp['content-length']), 10240) + self.info('Overwrite is OK') + + size = os.fstat(named_f.fileno()).st_size - int(oldf['content-length']) + self._check_quotas({self._get_uuid(): [(QPITHOS, QADD, size, None)]}) + + # TODO: MISSING: test transfer-encoding? + + def test_054_upload_file(self): """Test uploading a txt file to Pithos""" # Create a tmp file with tempfile.TemporaryFile(dir=self.temp_directory) as fout: @@ -92,9 +850,11 @@ class PithosTestSuite(BurninTests): # The container is the one choosen during the `create_container' self.clients.pithos.upload_object("test.txt", fout) # Verify quotas - self._check_quotas(diskspace=+os.fstat(fout.fileno()).st_size) + size = os.fstat(fout.fileno()).st_size + self._check_quotas( + {self._get_uuid(): [(QPITHOS, QADD, size, None)]}) - def test_005_download_file(self): + def test_055_download_file(self): """Test downloading the file from Pithos""" # Create a tmp directory to save the file with tempfile.TemporaryFile(dir=self.temp_directory) as fout: @@ -106,19 +866,530 @@ class PithosTestSuite(BurninTests): self.info("Comparing contents with the uploaded file") self.assertEqual(contents, "This is a temp file") - def test_006_remove(self): + def test_056_upload_files(self): + """Test uploading a number of txt files to Pithos""" + self.info('Simple call uploads %d new objects' % self.obj_upload_num) + pithos = self.clients.pithos + + size_change = 0 + min_size = self.obj_upload_min_size + max_size = self.obj_upload_max_size + + # Create a new container where we should upload the files + # This will be deleted in tear-down + self._create_pithos_container("burnin_big_files") + self._set_pithos_container("burnin_big_files") + + hashes = {} + open_files = [] + uuid = self._get_uuid() + usage = self.quotas[uuid]['pithos.diskspace']['usage'] + limit = pithos.get_container_limit() + for i, size in enumerate(random.sample(range(min_size, max_size), + self.obj_upload_num)): + assert usage + size_change + size <= limit, \ + 'Not enough quotas to upload files.' + named_file = self._create_file(size) + # Delete temp file at tear-down + self.temp_local_files.append(named_file.name) + self.info('Created file %s of %s MB' + % (named_file.name, float(size) / MB)) + name = named_file.name.split('/')[-1] + hashes[name] = named_file.hash + open_files.append(dict(obj=name, f=named_file)) + size_change += size + pithos.async_run(pithos.upload_object, open_files) + self._check_quotas({self._get_uuid(): + [(QPITHOS, QADD, size_change, None)]}) + + r = pithos.container_get() + self.info("Comparing hashes with the uploaded files") + for name, hash_ in hashes.iteritems(): + try: + o = itertools.ifilter(lambda o: o['name'] == name, + r.json).next() + assert o['x_object_hash'] == hash_, \ + 'Inconsistent hash for object: %s' % name + except StopIteration: + raise AssertionError('Object %s not found in the server' % + name) + self.info('Bulk upload is OK') + + def test_060_object_copy(self): + """Test object COPY""" + pithos = self.clients.pithos + obj, trg = 'source.file2copy', 'copied.file' + data = '{"key1":"val1", "key2":"val2"}' + + self._set_pithos_container(self.temp_containers[-3]) + + resp = pithos.object_put( + obj, + content_type='application/octet-stream', + data=data, + metadata=dict(mkey1='mval1', mkey2='mval2'), + permissions=dict( + read=['accX:groupA', 'u1', 'u2'], + write=['u2', 'u3']), + content_disposition='attachment; filename="fname.ext"') + self.info('Prepared a file /%s/%s' % (pithos.container, obj)) + + self._check_quotas( + {self._get_uuid(): [(QPITHOS, QADD, len(data), None)]}) + + resp = pithos.object_copy( + obj, + destination='/%s/%s' % (pithos.container, trg), + ignore_content_type=False, content_type='application/json', + metadata={'mkey2': 'mval2a', }, + permissions={'write': ['u5', 'accX:groupB']}) + self.assertEqual(resp.status_code, 201) + self.info('Status code is OK') + + self._check_quotas( + {self._get_uuid(): [(QPITHOS, QADD, len(data), None)]}) + + resp = pithos.get_object_info(trg) + self.assertTrue('content-disposition' in resp) + self.info('Content-disposition is OK') + + self.assertEqual(resp['x-object-meta-mkey1'], 'mval1') + self.assertEqual(resp['x-object-meta-mkey2'], 'mval2a') + self.info('Metadata are OK') + + resp = pithos.get_object_sharing(trg) + self.assertFalse('read' in resp or 'u2' in resp['write']) + self.assertTrue('accx:groupb' in resp['write']) + self.info('Sharing is OK') + + resp = pithos.object_copy( + obj, + destination='/%s/%s' % (pithos.container, obj), + content_encoding='utf8', + content_type='application/json', + destination_account='nonExistendAddress@NeverLand.com', + success=(201, 404)) + self.assertEqual(resp.status_code, 404) + self.info('Non existing UUID correctly causes a 404') + + resp = pithos.object_copy( + obj, + destination='/%s/%s' % (self.temp_containers[-2], obj), + content_encoding='utf8', + content_type='application/json') + self.assertEqual(resp.status_code, 201) + self.assertEqual( + resp.headers['content-type'], + 'application/json; charset=UTF-8') + + self._check_quotas( + {self._get_uuid(): [(QPITHOS, QADD, len(data), None)]}) + + # Check ignore_content_type and content_type + pithos.container = self.temp_containers[-2] + resp = pithos.object_get(obj) + etag = resp.headers['etag'] + ctype = resp.headers['content-type'] + self.assertEqual(ctype, 'application/json') + self.info('Cross container copy w. content-type/encoding is OK') + + resp = pithos.object_copy( + obj, + destination='/%s/%s0' % (pithos.container, obj), + ignore_content_type=True, + content_type='text/x-python') + self.assertEqual(resp.status_code, 201) + self.assertNotEqual(resp.headers['content-type'], 'application/json') + resp = pithos.object_get(obj + '0') + self.assertNotEqual(resp.headers['content-type'], 'text/x-python') + + self._check_quotas( + {self._get_uuid(): [(QPITHOS, QADD, len(data), None)]}) + + resp = pithos.object_copy( + obj, + destination='/%s/%s1' % (pithos.container, obj), + if_etag_match=etag) + self.assertEqual(resp.status_code, 201) + self.info('if-etag-match is OK') + + self._check_quotas( + {self._get_uuid(): [(QPITHOS, QADD, len(data), None)]}) + + resp = pithos.object_copy( + obj, + destination='/%s/%s2' % (pithos.container, obj), + if_etag_not_match='lalala') + self.assertEqual(resp.status_code, 201) + self.info('if-etag-not-match is OK') + + self._check_quotas( + {self._get_uuid(): [(QPITHOS, QADD, len(data), None)]}) + + resp = pithos.object_copy( + '%s2' % obj, + destination='/%s/%s3' % (pithos.container, obj), + format='xml', + public=True) + self.assertEqual(resp.status_code, 201) + self.assertTrue('xml' in resp.headers['content-type']) + + self._check_quotas( + {self._get_uuid(): [(QPITHOS, QADD, len(data), None)]}) + + resp = pithos.get_object_info(obj + '3') + self.assertTrue('x-object-public' in resp) + self.info('Publish, format and source-version are OK') + + def test_065_object_move(self): + """Test object MOVE""" + pithos = self.clients.pithos + obj = 'source.file2move' + data = '{"key1": "val1", "key2": "val2"}' + + resp = pithos.object_put( + obj, + content_type='application/octet-stream', + data=data, + metadata=dict(mkey1='mval1', mkey2='mval2'), + permissions=dict( + read=['accX:groupA', 'u1', 'u2'], + write=['u2', 'u3'])) + self.info('Prepared a file /%s/%s' % (pithos.container, obj)) + + self._check_quotas( + {self._get_uuid(): [(QPITHOS, QADD, len(data), None)]}) + + resp = pithos.object_move( + obj, + destination='/%s/%s0' % (pithos.container, obj), + ignore_content_type=False, content_type='application/json', + metadata={'mkey2': 'mval2a'}, + permissions={'write': ['u5', 'accX:groupB']}) + self.assertEqual(resp.status_code, 201) + self.info('Status code is OK') + + resp = pithos.get_object_meta(obj + '0') + self.assertEqual(resp['x-object-meta-mkey1'], 'mval1') + self.assertEqual(resp['x-object-meta-mkey2'], 'mval2a') + self.info('Metadata are OK') + + resp = pithos.get_object_sharing(obj + '0') + self.assertFalse('read' in resp) + self.assertTrue('u5' in resp['write']) + self.assertTrue('accx:groupb' in resp['write']) + self.info('Sharing is OK') + + self.assertRaises(ClientError, pithos.get_object_info, obj) + self.info('Old object is not there, which is OK') + + resp = pithos.object_move( + obj + '0', + destination='/%s/%s' % (pithos.container, obj), + content_encoding='utf8', + content_type='application/json', + destination_account='nonExistendAddress@NeverLand.com', + success=(201, 404)) + self.assertEqual(resp.status_code, 404) + self.info('Non existing UUID correctly causes a 404') + + resp = pithos.object_move( + obj + '0', + destination='/%s/%s' % (self.temp_containers[-3], obj), + content_encoding='utf8', + content_type='application/json', + content_disposition='attachment; filename="fname.ext"') + self.assertEqual(resp.status_code, 201) + self.assertEqual( + resp.headers['content-type'], + 'application/json; charset=UTF-8') + + pithos.container = self.temp_containers[-3] + resp = pithos.object_get(obj) + etag = resp.headers['etag'] + ctype = resp.headers['content-type'] + self.assertEqual(ctype, 'application/json') + self.assertTrue('fname.ext' in resp.headers['content-disposition']) + self.info('Cross container copy w. content-type/encoding is OK') + + resp = pithos.object_move( + obj, + destination='/%s/%s0' % (pithos.container, obj), + ignore_content_type=True, + content_type='text/x-python') + self.assertEqual(resp.status_code, 201) + self.assertNotEqual(resp.headers['content-type'], 'application/json') + resp = pithos.object_get(obj + '0') + self.assertNotEqual(resp.headers['content-type'], 'text/x-python') + + self._check_quotas({self._get_uuid(): [(QPITHOS, QADD, 0, None)]}) + + resp = pithos.object_move( + obj + '0', + destination='/%s/%s' % (pithos.container, obj), + if_etag_match=etag) + self.assertEqual(resp.status_code, 201) + self.info('if-etag-match is OK') + + resp = pithos.object_move( + obj, + destination='/%s/%s0' % (pithos.container, obj), + if_etag_not_match='lalala') + self.assertEqual(resp.status_code, 201) + self.info('if-etag-not-match is OK') + + resp = pithos.object_move( + obj + '0', + destination='/%s/%s' % (pithos.container, obj), + format='xml', + public=True) + self.assertEqual(resp.status_code, 201) + self.assertTrue('xml' in resp.headers['content-type']) + + self._check_quotas({self._get_uuid(): [(QPITHOS, QADD, 0, None)]}) + + resp = pithos.get_object_info(obj) + self.assertTrue('x-object-public' in resp) + self.info('Publish, format and source-version are OK') + + f_name, f_size, old_size = None, None, None + for o in pithos.list_objects(): + if o['name'] == obj: + old_size = o['bytes'] + break + pithos.container = self.temp_containers[-2] + for o in pithos.list_objects(): + if o['bytes']: + f_name, f_size = o['name'], o['bytes'] + break + resp = pithos.object_move( + f_name, + destination='/%s/%s' % (self.temp_containers[-3], obj)) + pithos.container = self.temp_containers[-3] + for o in pithos.list_objects(): + if o['name'] == obj: + self.assertEqual(f_size, o['bytes']) + break + self._check_quotas( + {self._get_uuid(): [(QPITHOS, QREMOVE, old_size, None)]}) + self.info('Cross container MOVE is OK') + + def test_070_object_post(self): + """Test object POST""" + pithos = self.clients.pithos + obj = 'sample2post.file' + newf = NamedTemporaryFile() + # Add file to 'temp_local_files' for cleanup + self.temp_local_files.append(newf.name) + newf.writelines([ + 'ello!\n', + 'This is a test line\n', + 'inside a test file\n']) + newf.flush() + + resp = pithos.object_put( + obj, + content_type='text/x-python', + data='H', + metadata=dict(mkey1='mval1', mkey2='mval2'), + permissions=dict( + read=['accX:groupA', 'u1', 'u2'], + write=['u2', 'u3'])) + self.info( + 'Prepared a local file %s & a remote object %s', newf.name, obj) + + self._check_quotas({self._get_uuid(): [(QPITHOS, QADD, 1, None)]}) + + newf.seek(0) + pithos.append_object(obj, newf) + resp = pithos.object_get(obj) + self.assertEqual(resp.text[:5], 'Hello') + self.info('Append is OK') + + size = os.fstat(newf.fileno()).st_size + self._check_quotas({self._get_uuid(): [(QPITHOS, QADD, size, None)]}) + + newf.seek(0) + resp = pithos.overwrite_object(obj, 0, 10, newf) + resp = pithos.object_get(obj) + self.assertTrue(resp.text.startswith('ello!')) + self.assertEqual(resp.headers['content-type'], 'text/x-python') + self.info('Overwrite (involves content-legth/range/type) is OK') + + self._check_quotas({self._get_uuid(): [(QPITHOS, QADD, 0, None)]}) + + resp = pithos.truncate_object(obj, 5) + resp = pithos.object_get(obj) + self.assertEqual(resp.text, 'ello!') + self.assertEqual(resp.headers['content-type'], 'text/x-python') + self.info( + 'Truncate (involves content-range, object-bytes and source-object)' + ' is OK') + + self._check_quotas( + {self._get_uuid(): [(QPITHOS, QREMOVE, size - 4, None)]}) + + mu, vu = 'mk%s' % self.uvalue, 'mv%s' % self.uvalue + pithos.set_object_meta(obj, {'mkey2': 'mval2a', mu: vu}) + + resp = pithos.get_object_meta(obj) + self.assertEqual(resp['x-object-meta-mkey1'], 'mval1') + self.assertEqual(resp['x-object-meta-mkey2'], 'mval2a') + self.assertEqual(resp['x-object-meta-' + mu], vu) + pithos.del_object_meta(obj, 'mkey1') + resp = pithos.get_object_meta(obj) + self.assertFalse('x-object-meta-mkey1' in resp) + self.info('Metadata are OK (ascii and unicode)') + + pithos.set_object_sharing( + obj, read_permission=['u4', 'u5'], write_permission=['u4']) + resp = pithos.get_object_sharing(obj) + self.assertTrue('read' in resp) + self.assertTrue('u5' in resp['read']) + self.assertTrue('write' in resp) + self.assertTrue('u4' in resp['write']) + pithos.del_object_sharing(obj) + resp = pithos.get_object_sharing(obj) + self.assertTrue(len(resp) == 0) + self.info('Sharing is OK') + + pithos.publish_object(obj) + resp = pithos.get_object_info(obj) + self.assertTrue('x-object-public' in resp) + pithos.unpublish_object(obj) + resp = pithos.get_object_info(obj) + self.assertFalse('x-object-public' in resp) + self.info('Publishing is OK') + + etag = resp['etag'] + resp = pithos.object_post( + obj, + update=True, + public=True, + if_etag_not_match=etag, + success=(412, 202, 204)) + self.assertEqual(resp.status_code, 412) + self.info('if-etag-not-match is OK') + + resp = pithos.object_post( + obj, + update=True, + public=True, + if_etag_match=etag, + content_type='application/octet-srteam', + content_encoding='application/json') + + resp = pithos.get_object_info(obj) + hello_version = resp['x-object-version'] + self.assertTrue('x-object-public' in resp) + self.assertEqual(resp['content-type'], 'text/x-python') + self.info('If-etag-match is OK') + + pithos.container = self.temp_containers[-2] + pithos.create_object(obj) + resp = pithos.object_post( + obj, + update=True, + content_type='application/octet-srteam', + content_length=5, + content_range='bytes 1-5/*', + source_object='/%s/%s' % (self.temp_containers[-3], obj), + source_account='thisAccountWillNeverExist@adminland.com', + source_version=hello_version, + data='12345', + success=(416, 202, 204)) + self.assertEqual(resp.status_code, 416) + self.info('Successfully failed with invalid user UUID') + + pithos.upload_from_string(obj, '12345') + resp = pithos.object_post( + obj, + update=True, + content_type='application/octet-srteam', + content_length=3, + content_range='bytes 1-3/*', + source_object='/%s/%s' % (self.temp_containers[-3], obj), + source_account=pithos.account, + source_version=hello_version, + data='123', + content_disposition='attachment; filename="fname.ext"') + + resp = pithos.object_get(obj) + self.assertEqual(resp.text, '1ell5') + self.info('Cross container POST with source-version/account are OK') + + self._check_quotas({self._get_uuid(): [(QPITHOS, QADD, 5, None)]}) + + self.assertTrue('content-disposition' in resp.headers) + self.assertTrue('fname.ext' in resp.headers['content-disposition']) + self.info('Content-disposition POST is OK') + + mobj = 'manifest.test' + txt = '' + for i in range(10): + txt += '%s' % i + resp = pithos.object_put( + '%s/%s' % (mobj, i), + data='%s' % i, + content_length=1, + success=201, + content_encoding='application/octet-stream', + content_type='application/octet-stream') + + pithos.create_object_by_manifestation( + mobj, content_type='application/octet-stream') + + resp = pithos.object_post( + mobj, manifest='%s/%s' % (pithos.container, mobj)) + + resp = pithos.object_get(mobj) + self.assertEqual(resp.text, txt) + self.info('Manifestation is OK') + + self._check_quotas({self._get_uuid(): [(QPITHOS, QADD, 10, None)]}) + + # TODO: We need to check transfer_encoding + + def test_075_object_delete(self): + """Test object DELETE""" + pithos = self.clients.pithos + obj = 'sample2post.file' + + resp = pithos.object_delete(obj, until=1000000) + resp = pithos.object_get(obj, success=(200, 404)) + self.assertEqual(resp.status_code, 200) + self.info('Successfully failed to delete with false "until"') + size = int(resp.headers['content-length']) + + resp = pithos.object_delete(obj) + self.assertEqual(resp.status_code, 204) + self.info('Status code is OK') + + self._check_quotas( + {self._get_uuid(): [(QPITHOS, QREMOVE, size, None)]}) + + resp = pithos.object_get(obj, success=(200, 404)) + self.assertEqual(resp.status_code, 404) + self.info('Successfully failed to delete a deleted file') + + def test_080_remove(self): """Test removing files and containers from Pithos""" + self.created_container = self.clients.pithos.container + fname = 'sample.file_v2' self.info("Removing the file %s from container %s", - "test.txt", self.created_container) + fname, self.created_container) # The container is the one choosen during the `create_container' - content_length = \ - self.clients.pithos.get_object_info("test.txt")['content-length'] - self.clients.pithos.del_object("test.txt") + obj_info = self.clients.pithos.get_object_info(fname) + content_length = obj_info['content-length'] + self.clients.pithos.del_object(fname) # Verify quotas - self._check_quotas(diskspace=-int(content_length)) + self._check_quotas( + {self._get_uuid(): [(QPITHOS, QREMOVE, content_length, None)]}) self.info("Removing the container %s", self.created_container) + self.clients.pithos.container_delete( + self.created_container, delimiter='/') self.clients.pithos.purge_container() # List containers @@ -134,6 +1405,17 @@ class PithosTestSuite(BurninTests): @classmethod def tearDownClass(cls): # noqa """Clean up""" - if cls.created_container is not None: - cls.clients.pithos.del_container(delimiter='/') - cls.clients.pithos.purge_container() + pithos = cls.clients.pithos + for tcont in getattr(cls, 'temp_containers', []): + pithos.container = tcont + try: + pithos.del_container(delimiter='/') + pithos.purge_container(tcont) + except ClientError: + pass + # Delete temporary files + for tfile in getattr(cls, 'temp_local_files', []): + try: + os.remove(tfile) + except OSError: + pass diff --git a/snf-tools/synnefo_tools/burnin/projects_tests.py b/snf-tools/synnefo_tools/burnin/projects_tests.py new file mode 100644 index 0000000000000000000000000000000000000000..e3183342aa22db2fa9bb873264b4986741a7df5e --- /dev/null +++ b/snf-tools/synnefo_tools/burnin/projects_tests.py @@ -0,0 +1,89 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +""" +This is the burnin class that tests the Projects functionality + +""" + +import random + +from synnefo_tools.burnin.common import Proper +from synnefo_tools.burnin.cyclades_common import CycladesTests, \ + QADD, QREMOVE, MB, GB, QDISK, QVM, QRAM, QCPU + + +# pylint: disable=too-many-public-methods +class QuotasTestSuite(CycladesTests): + """Test Quotas functionality""" + server = Proper(value=None) + + def test_001_check_skip(self): + """Check if we are members in more than one projects""" + self._skip_suite_if(len(self.quotas.keys()) < 2, + "This user is not a member of 2 or more projects") + + def test_002_create(self): + """Create a machine to a different project than base""" + image = random.choice(self._parse_images()) + flavors = self._parse_flavors() + + # We want to create our machine in a project other than 'base' + projects = self.quotas.keys() + projects.remove(self._get_uuid()) + (flavor, project) = self._find_project(flavors, projects) + + # Create machine + self.server = self._create_server(image, flavor, network=True, + project_id=project) + + # Wait for server to become active + self._insist_on_server_transition( + self.server, ["BUILD"], "ACTIVE") + + def test_003_assign(self): + """Re-Assign the machine to a different project""" + # We will use the base project for now + new_project = self._get_uuid() + project_name = self._get_project_name(new_project) + self.info("Assign %s to project %s", self.server['name'], project_name) + + # Reassign server + old_project = self.server['tenant_id'] + self.clients.cyclades.reassign_server(self.server['id'], new_project) + + # Check tenant_id + self.server = self._get_server_details(self.server, quiet=True) + self.assertEqual(self.server['tenant_id'], new_project) + + # Check new quotas + flavor = self.clients.compute.get_flavor_details( + self.server['flavor']['id']) + changes = \ + {old_project: + [(QDISK, QREMOVE, flavor['disk'], GB), + (QVM, QREMOVE, 1, None), + (QRAM, QREMOVE, flavor['ram'], MB), + (QCPU, QREMOVE, flavor['vcpus'], None)], + new_project: + [(QDISK, QADD, flavor['disk'], GB), + (QVM, QADD, 1, None), + (QRAM, QADD, flavor['ram'], MB), + (QCPU, QADD, flavor['vcpus'], None)]} + self._check_quotas(changes) + + def test_004_cleanup(self): + """Remove test server""" + self._delete_servers([self.server]) diff --git a/snf-tools/synnefo_tools/burnin/server_tests.py b/snf-tools/synnefo_tools/burnin/server_tests.py index 295be39498a6f01e7c4886c4634e47ef52afa72c..a01870a79d40d0dae27e375c7e62d79ea2bbc375 100644 --- a/snf-tools/synnefo_tools/burnin/server_tests.py +++ b/snf-tools/synnefo_tools/burnin/server_tests.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. """ This is the burnin class that tests the Servers' functionality @@ -44,12 +26,11 @@ import socket from vncauthproxy.d3des import generate_response as d3des_generate_response -from synnefo_tools.burnin.common import BurninTests, Proper +from synnefo_tools.burnin.common import Proper from synnefo_tools.burnin.cyclades_common import CycladesTests -# Too many public methods. pylint: disable-msg=R0904 -# Too many instance attributes. pylint: disable-msg=R0902 +# pylint: disable=too-many-public-methods,too-many-instance-attributes # This class gets replicated into actual TestCases dynamically class GeneratedServerTestSuite(CycladesTests): """Test Spawning Serverfunctionality""" @@ -182,8 +163,15 @@ class GeneratedServerTestSuite(CycladesTests): self._skip_if(not self.use_ipv6, "--no-ipv6 flag enabled") self._insist_on_ping(self.ipv6[0], version=6) - def test_011_attach_second_network(self): - """Attach a second public IP to our server""" + def test_011a_detach_from_network(self): + """Detach server from public network""" + self._disconnect_from_network(self.server) + + # Test that server is unreachable + self._insist_on_ping(self.ipv4[0], should_fail=True) + + def test_011b_attach_network(self): + """Re-Attach a public IP to our server""" floating_ip = self._create_floating_ip() self._create_port(floating_ip['floating_network_id'], device_id=self.server['id'], @@ -193,7 +181,7 @@ class GeneratedServerTestSuite(CycladesTests): server = self.clients.cyclades.get_server_details(self.server['id']) self.server = server self.ipv4 = self._get_ips(server, version=4) - self.assertEqual(len(self.ipv4), 2) + self.assertEqual(len(self.ipv4), 1) # Test new IPv4 self.test_009_server_ping_ipv4() @@ -222,13 +210,11 @@ class GeneratedServerTestSuite(CycladesTests): """Test SSH to server public IPv4 works, verify hostname""" self._skip_if(not self._image_is(self.use_image, "linux"), "only valid for Linux servers") - hostname1 = self._insist_get_hostname_over_ssh( + hostname = self._insist_get_hostname_over_ssh( self.ipv4[0], self.username, self.password) - hostname2 = self._insist_get_hostname_over_ssh( - self.ipv4[1], self.username, self.password) + # The hostname must be of the form 'prefix-id' - self.assertTrue(hostname1.endswith("-%d" % self.server['id'])) - self.assertEqual(hostname1, hostname2) + self.assertTrue(hostname.endswith("-%d" % self.server['id'])) def test_018_ssh_to_server_ipv6(self): """Test SSH to server public IPv6 works, verify hostname""" @@ -248,7 +234,7 @@ class GeneratedServerTestSuite(CycladesTests): socket.AF_INET, self.ipv4[0], 3389) # No actual RDP processing done. We assume the RDP server is there # if the connection to the RDP port is successful. - # pylint: disable-msg=W0511 + # pylint: disable=fixme # FIXME: Use rdesktop, analyze exit code? see manpage sock.close() @@ -261,7 +247,7 @@ class GeneratedServerTestSuite(CycladesTests): socket.AF_INET, self.ipv6[0], 3389) # No actual RDP processing done. We assume the RDP server is there # if the connection to the RDP port is successful. - # pylint: disable-msg=W0511 + # pylint: disable=fixme # FIXME: Use rdesktop, analyze exit code? see manpage sock.close() @@ -291,7 +277,7 @@ class GeneratedServerTestSuite(CycladesTests): # will run the same tests using different images and or flavors. # The creation and running of our GeneratedServerTestSuite class will # happen as a testsuite itself (everything here is a test!). -class ServerTestSuite(BurninTests): +class ServerTestSuite(CycladesTests): """Generate and run the GeneratedServerTestSuite We will generate as many testsuites as the number of images given. @@ -304,28 +290,11 @@ class ServerTestSuite(BurninTests): def test_001_images_to_use(self): """Find images to be used by GeneratedServerTestSuite""" - if self.images is None: - self.info("No --images given. Will use the default %s", - "^Debian Base$") - filters = ["name:^Debian Base$"] - else: - filters = self.images - - self.avail_images = self._find_images(filters) - self.info("Found %s images. Let's create an equal number of tests", - len(self.avail_images)) + self.avail_images = self._parse_images() def test_002_flavors_to_use(self): """Find flavors to be used by GeneratedServerTestSuite""" - flavors = self._get_list_of_flavors(detail=True) - - if self.flavors is None: - self.info("No --flavors given. Will use all of them") - self.avail_flavors = flavors - else: - self.avail_flavors = self._find_flavors( - self.flavors, flavors=flavors) - self.info("Found %s flavors to choose from", len(self.avail_flavors)) + self.avail_flavors = self._parse_flavors() def test_003_create_testsuites(self): """Generate the GeneratedServerTestSuite tests""" diff --git a/snf-tools/synnefo_tools/burnin/snapshots.py b/snf-tools/synnefo_tools/burnin/snapshots.py new file mode 100644 index 0000000000000000000000000000000000000000..5941dab13541c3914c53a63ad9acda519731ce96 --- /dev/null +++ b/snf-tools/synnefo_tools/burnin/snapshots.py @@ -0,0 +1,234 @@ +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +""" +This is the burnin class that tests the Snapshots functionality +""" + +import stat +import base64 +import random + +from synnefo_tools.burnin.common import Proper, QPITHOS, QADD, QREMOVE, GB +from synnefo_tools.burnin.cyclades_common import CycladesTests + +from kamaki.clients import ClientError + + +# This class gets replicated into actual TestCases dynamically +class SnapshotsTestSuite(CycladesTests): + """Test Snapshot functionality""" + personality = Proper(value=None) + account = Proper(value=None) + servers = Proper(value=[]) + snapshot = Proper(value=None) + tmp_container = Proper(value='burnin-snapshot-temp') + use_flavor = Proper(value=None) + personality = Proper(value=None) + + def test_001_submit_create_snapshot(self): + """Create a server and take a snapshot""" + self.account = self._get_uuid() + use_image = random.choice(self._parse_images()) + archipelago_flavors = \ + [f for f in self._parse_flavors() if + f['SNF:disk_template'].startswith('ext_archipelago')] + self.assertGreater(len(archipelago_flavors), 0, + "No 'archipelago' disk template found") + self.use_flavor = random.choice(archipelago_flavors) + if self._image_is(use_image, "linux"): + # Enforce personality test + self.info("Creating personality content to be used") + self.personality = [{ + 'path': "/root/test_inj_file", + 'owner': "root", + 'group': "root", + 'mode': stat.S_IRUSR | stat.S_IWUSR, + 'contents': base64.b64encode("This is a personality file") + }] + + self.info("Using image %s with id %s", + use_image['name'], use_image['id']) + self.info("Using flavor %s with id %s", + self.use_flavor['name'], self.use_flavor['id']) + self.servers.append(self._create_server(use_image, + self.use_flavor, + personality=self.personality, + network=True)) + server = self.servers[0] + self._insist_on_server_transition(server, ["BUILD"], "ACTIVE") + + volume_id = server['volumes'][0] + snapshot_name = 'snf-burnin-snapshot_%s' % volume_id + self.info("Creating snapshot with name '%s', for volume %s", + snapshot_name, volume_id) + self.snapshot = self.clients.block_storage.create_snapshot( + volume_id, display_name=snapshot_name) + self.info("Snapshot with id '%s' created", self.snapshot['id']) + + volume_size = self.snapshot['size'] * GB + self._check_quotas({self.account: + [(QPITHOS, QADD, volume_size, None)]}) + + self.info('Check that snapshot is listed among snapshots') + self.assertTrue(self.snapshot['id'] in [i['id'] for i in + self.clients.block_storage.list_snapshots()]) + + self.info('Get snapshot details') + self.clients.block_storage.get_snapshot_details(self.snapshot['id']) + + self.info('Check the snapshot is listed under ' + 'pithos snapshots container') + pithos = self.clients.pithos + self._set_pithos_account(self.account) + self.pithos_account = pithos.account + pithos.container = 'snapshots' + self.assertTrue(self.snapshot['display_name'] in + [o['name'] for o in pithos.list_objects()]) + + self._insist_on_snapshot_transition( + self.snapshot, ["UNAVAILABLE", "CREATING"], "AVAILABLE") + + def test_002_copy_snapshot(self): + """Copy snapshot to secondary container""" + self._create_pithos_container(self.tmp_container) + pithos = self.clients.pithos + + pithos.copy_object( + src_container=pithos.container, + src_object=self.snapshot['display_name'], + dst_container=self.tmp_container, + dst_object='%s_new' % self.snapshot['display_name']) + pithos.container = 'snapshots' + resp1 = pithos.get_object_info(self.snapshot['display_name']) + + pithos.container = self.tmp_container + resp2 = pithos.get_object_info( + '%s_new' % self.snapshot['display_name']) + + self.assertEqual(resp1['x-object-hash'], resp2['x-object-hash']) + self.info('Snapshot has being copied in another container, OK') + + self.info('Check both snapshots are listed among snapshots') + uploaded_snapshots = [i['id'] for i in + self.clients.block_storage.list_snapshots()] + self.assertTrue(resp1['x-object-uuid'] in uploaded_snapshots) + self.assertTrue(resp2['x-object-uuid'] in uploaded_snapshots) + + volume_size = self.snapshot['size'] * GB + self._check_quotas({self.account: + [(QPITHOS, QADD, volume_size, None)]}) + + def test_003_move_snapshot(self): + """Move snapshot to secondary container""" + pithos = self.clients.pithos + pithos.container = self.tmp_container + resp1 = pithos.get_object_info( + '%s_new' % self.snapshot['display_name']) + + pithos.move_object( + src_container=self.tmp_container, + src_object='%s_new' % self.snapshot['display_name'], + dst_container=self.tmp_container, + dst_object='%s_renamed' % self.snapshot['display_name']) + self.assertRaises( + ClientError, pithos.get_object_info, + '%s_new' % self.snapshot['display_name']) + + resp2 = pithos.get_object_info( + '%s_renamed' % self.snapshot['display_name']) + self.info('Snapshot has being renamed, OK') + + self.info('Check both snapshots are listed among snapshots') + uploaded_snapshots = [i['id'] for i in + self.clients.block_storage.list_snapshots()] + self.assertTrue(self.snapshot['id'] in uploaded_snapshots) + self.assertEqual(resp1['x-object-uuid'], resp2['x-object-uuid']) + self.assertTrue(resp2['x-object-uuid'] in uploaded_snapshots) + + # self._check_quotas({self.account: [(QPITHOS, QADD, 0, None)]}) + self._check_quotas({self.account: []}) + + def test_004_update_snapshot(self): + """Update snapshot metadata""" + pithos = self.clients.pithos + pithos.container = 'snapshots' + self.info('Update snapshot \'pithos\' domain metadata') + pithos.set_object_meta(self.snapshot['display_name'], {'foo': 'bar'}) + resp = pithos.get_object_meta(self.snapshot['display_name']) + self.assertEqual(resp['x-object-meta-foo'], 'bar') + + self.info('Check snapshot is still listed among snapshots') + uploaded_snapshots = [i['id'] for i in + self.clients.block_storage.list_snapshots()] + self.assertTrue(self.snapshot['id'] in uploaded_snapshots) + + def test_005_spawn_vm_from_snapshot(self): + """Spawn a VM from the newly created snapshot""" + self.servers.append(self._create_server( + self.snapshot, self.use_flavor, + network=True)) + server = self.servers[-1] + self._insist_on_server_transition(server, ["BUILD"], "ACTIVE") + + server = self.clients.cyclades.get_server_details( + server['id']) + ipv4 = self._get_ips(server, version=4) + self.assertTrue(len(ipv4) >= 1) + self._insist_on_ping(ipv4[0], version=4) + + # use initial server's password + for inj_file in (self.personality or ()): + self._check_file_through_ssh( + ipv4[0], inj_file['owner'], self.servers[0]['adminPass'], + inj_file['path'], inj_file['contents']) + + def test_006_delete_snapshot(self): + """Delete snapshot""" + self.info('Delete snapshot') + self.clients.block_storage.delete_snapshot(self.snapshot['id']) + self._insist_on_snapshot_deletion(self.snapshot['id']) + + volume_size = self.snapshot['size'] * GB + self._check_quotas({self.account: + [(QPITHOS, QREMOVE, volume_size, None)]}) + self.snapshot = None + + def test_cleanup(self): + """Cleanup created servers""" + for s in self.servers: + self._disconnect_from_network(s) + self._delete_servers(self.servers) + + @classmethod + def tearDownClass(cls): # noqa + """Clean up""" + # Delete snapshot + snapshots = [s for s in cls.clients.block_storage.list_snapshots() + if s['display_name'].startswith("snf-burnin-")] + for snapshot in snapshots: + try: + cls.clients.block_storage.delete_snapshot(snapshot['id']) + except ClientError: + pass + + # Delete temp containers + cls.clients.pithos.account = cls.account + cls.clients.pithos.container = cls.tmp_container + try: + cls.clients.pithos.del_container(delimiter='/') + cls.clients.pithos.purge_container(cls.tmp_container) + except ClientError: + pass diff --git a/snf-tools/synnefo_tools/burnin/stale_tests.py b/snf-tools/synnefo_tools/burnin/stale_tests.py index 4413a8d9b99324b128f5079a416d88d3f4d9fb47..1af703955b92f885315c8273bc69bcc56dedd78c 100644 --- a/snf-tools/synnefo_tools/burnin/stale_tests.py +++ b/snf-tools/synnefo_tools/burnin/stale_tests.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. """ This is the burnin class that handles stale resources (Servers/Networks) @@ -40,7 +22,7 @@ from synnefo_tools.burnin.common import Proper, SNF_TEST_PREFIX from synnefo_tools.burnin.cyclades_common import CycladesTests -# Too many public methods. pylint: disable-msg=R0904 +# pylint: disable=too-many-public-methods class StaleServersTestSuite(CycladesTests): """Handle stale Servers""" stale_servers = Proper(value=None) @@ -72,7 +54,7 @@ class StaleServersTestSuite(CycladesTests): self._delete_servers(self.stale_servers, error=True) -# Too many public methods. pylint: disable-msg=R0904 +# pylint: disable=too-many-public-methods class StaleFloatingIPsTestSuite(CycladesTests): """Handle stale Floating IPs""" stale_ips = Proper(value=None) @@ -107,7 +89,7 @@ class StaleFloatingIPsTestSuite(CycladesTests): self._delete_floating_ips(self.stale_ips) -# Too many public methods. pylint: disable-msg=R0904 +# pylint: disable=too-many-public-methods class StaleNetworksTestSuite(CycladesTests): """Handle stale Networks""" stale_networks = Proper(value=None) diff --git a/snf-webproject/MANIFEST.in b/snf-webproject/MANIFEST.in index 401e13e83982141c6a2100b3890326c627a7524f..f40c996eb48009a002f71d03483b4181dfb6406b 100644 --- a/snf-webproject/MANIFEST.in +++ b/snf-webproject/MANIFEST.in @@ -1,7 +1,5 @@ recursive-include synnefo *.json *.html *.json *.xml *.txt -recursive-include synnefo/admin/static * -recursive-include synnefo/ui/static * -recursive-include docs/ *.rst -recursive-include extras/ * +recursive-include docs *.rst +recursive-include extras * -include distribute_setup.py +include distribute_setup.py README.md diff --git a/snf-webproject/README.md b/snf-webproject/README.md new file mode 100644 index 0000000000000000000000000000000000000000..a680899b32bc19ba69c2e7b8803244f8594a68b7 --- /dev/null +++ b/snf-webproject/README.md @@ -0,0 +1,27 @@ +snf-webproject +============== + +Overview +-------- + +This is Synnefo's snf-webproject component. Please see the [official Synnefo +site](http://www.synnefo.org) for more information. + + +Copyright and license +===================== + +Copyright (C) 2010-2014 GRNET S.A. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. diff --git a/snf-webproject/conf/gunicorn-hooks/gunicorn-archipelago.py b/snf-webproject/conf/gunicorn-hooks/gunicorn-archipelago.py new file mode 100644 index 0000000000000000000000000000000000000000..15cf374660147b5b634360a65a6be7fd89cc789c --- /dev/null +++ b/snf-webproject/conf/gunicorn-hooks/gunicorn-archipelago.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 - +# +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +from pithos.workers import glue +from multiprocessing import Lock +import mmap +import pickle +import os + +SYNNEFO_UMASK=0o007 + +def find_hole(workers, fworkers): + old_key = [] + old_age = [] + for key in fworkers: + if key not in workers.keys(): + old_age.append(fworkers[key]) + old_key.append(key) + break + if len(old_age) and len(old_key): + for key in old_key: + del fworkers[key] + return old_age + return old_age + + +def follow_workers(pid, wid, server): + hole = None + fd = server.state_fd + fd.seek(0) + f = pickle.load(fd) + hole = find_hole(server.WORKERS, f) + if len(hole) > 0: + k = {pid: int(hole[0])} + else: + k = {pid: wid} + f.update(k) + fd.seek(0) + pickle.dump(f, fd) + return hole + + +def allocate_wid(pid, wid, server): + hole = None + hole = follow_workers(pid, wid, server) + return hole + + +def when_ready(server): + server.lock = Lock() + server.state_fd = mmap.mmap(-1, 4096) + pickle.dump({}, server.state_fd) + + +def update_workers(pid, wid, server): + fd = server.state_fd + fd.seek(0) + f = pickle.load(fd) + for k, v in f.items(): + if wid == v: + del f[k] + break + k = {pid: wid} + f.update(k) + fd.seek(0) + pickle.dump(f, fd) + + +def post_fork(server, worker): + # set umask for the gunicorn worker + os.umask(SYNNEFO_UMASK) + server.lock.acquire() + if server.worker_age <= server.num_workers: + update_workers(worker.pid, server.worker_age, server) + glue.WorkerGlue.setmap(worker.pid, server.worker_age) + else: + wid = allocate_wid(worker.pid, server.worker_age, server) + glue.WorkerGlue.setmap(worker.pid, wid[0]) + server.lock.release() + + +def worker_exit(server, worker): + if glue.WorkerGlue.ioctx_pool: + glue.WorkerGlue.ioctx_pool._shutdown_pool() + + +def on_exit(server): + server.state_fd.close() + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/snf-webproject/extras/synnefo.example b/snf-webproject/extras/synnefo.example index d346f5c7dd0c64c79abf30a3ec903b5fd7093090..d869dc3736d83c6b59892ddc2bc07e2898e16ebc 100644 --- a/snf-webproject/extras/synnefo.example +++ b/snf-webproject/extras/synnefo.example @@ -4,8 +4,8 @@ CONFIG = { 'DJANGO_SETTINGS_MODULE': 'synnefo.settings', }, 'working_dir': '/etc/synnefo', - 'user': 'www-data', - 'group': 'www-data', + 'user': 'synnefo', + 'group': 'synnefo', 'args': ( '--bind=127.0.0.1:8080', '--worker-class=gevent', diff --git a/snf-webproject/setup.py b/snf-webproject/setup.py index 32149086302694e552b065c2275283fd10ea77ac..0b8ac66974c229610607f427cb1e20b6d529c76e 100644 --- a/snf-webproject/setup.py +++ b/snf-webproject/setup.py @@ -1,35 +1,17 @@ -# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # import distribute_setup @@ -162,7 +144,7 @@ def find_package_data( setup( name='snf-webproject', version=VERSION, - license='BSD', + license='GNU GPLv3', url='http://www.synnefo.org/', description=SHORT_DESCRIPTION, classifiers=CLASSIFIERS, diff --git a/snf-webproject/synnefo/__init__.py b/snf-webproject/synnefo/__init__.py index e68793f97a2b2b2232a6ac7007e881e9bf203b66..4ee938c013a307567139bc35cd25d0ff33546fba 100644 --- a/snf-webproject/synnefo/__init__.py +++ b/snf-webproject/synnefo/__init__.py @@ -1,35 +1,17 @@ -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # this is a namespace package try: diff --git a/snf-webproject/synnefo/versions/__init__.py b/snf-webproject/synnefo/versions/__init__.py index e68793f97a2b2b2232a6ac7007e881e9bf203b66..4ee938c013a307567139bc35cd25d0ff33546fba 100644 --- a/snf-webproject/synnefo/versions/__init__.py +++ b/snf-webproject/synnefo/versions/__init__.py @@ -1,35 +1,17 @@ -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # this is a namespace package try: diff --git a/snf-webproject/synnefo/webproject/context_processors.py b/snf-webproject/synnefo/webproject/context_processors.py index 9082a14f30124fcdafe5a7a6cd5322cca55a9e2e..1c8a75e207d1423eacc8fc3506bb1a69cce99a89 100644 --- a/snf-webproject/synnefo/webproject/context_processors.py +++ b/snf-webproject/synnefo/webproject/context_processors.py @@ -1,6 +1,11 @@ +import json + from django.utils.safestring import mark_safe from django.conf import settings +from synnefo.util import version +from synnefo_branding import settings as branding_settings + def cloudbar(request): """ @@ -30,8 +35,12 @@ def cloudbar(request): """ + BRANDING_CSS = getattr(branding_settings, 'FONTS_CSS_URLS', []) + CB_ACTIVE = getattr(settings, 'CLOUDBAR_ACTIVE', True) CB_LOCATION = getattr(settings, 'CLOUDBAR_LOCATION') + CB_VERSION = version.get_component_version("webproject") + CB_COOKIE_NAME = getattr(settings, 'CLOUDBAR_COOKIE_NAME', 'okeanos_account') CB_SERVICES_URL = getattr(settings, 'CLOUDBAR_SERVICES_URL') @@ -43,14 +52,17 @@ def cloudbar(request): CB_CODE = """ <script type="text/javascript"> + var CLOUDBAR_VERSION = '%(version)s'; var CLOUDBAR_LOCATION = "%(location)s"; var CLOUDBAR_COOKIE_NAME = "%(cookie_name)s"; var GET_SERVICES_URL = "%(services_url)s"; var GET_MENU_URL = "%(menu_url)s"; var CLOUDBAR_HEIGHT = '%(height)s'; + var CLOUDBAR_EXTRA_CSS = %(branding_css)s; + $(document).ready(function(){ - $.getScript(CLOUDBAR_LOCATION + 'cloudbar.js'); + $.getScript(CLOUDBAR_LOCATION + 'cloudbar.js?' + CLOUDBAR_VERSION); }); </script> @@ -67,7 +79,9 @@ def cloudbar(request): 'services_url': CB_SERVICES_URL, 'menu_url': CB_MENU_URL, 'height': str(CB_HEIGHT), - 'bg_color': CB_BGCOLOR} + 'bg_color': CB_BGCOLOR, + 'version': CB_VERSION, + 'branding_css': json.dumps(BRANDING_CSS)} CB_CODE = mark_safe(CB_CODE) diff --git a/snf-webproject/synnefo/webproject/exception_filter.py b/snf-webproject/synnefo/webproject/exception_filter.py index 4df186ebe3fc1d5073bed0e892ee0ddd8ee0df06..6bd952fec1433dbb1b6faf9918c3508299ee3dc5 100644 --- a/snf-webproject/synnefo/webproject/exception_filter.py +++ b/snf-webproject/synnefo/webproject/exception_filter.py @@ -1,35 +1,17 @@ -# Copyright 2013 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.conf import settings from django.views.debug import SafeExceptionReporterFilter diff --git a/snf-webproject/synnefo/webproject/i18n.py b/snf-webproject/synnefo/webproject/i18n.py index 61a937c82c9a329fe00c13d054bd9f78f22b1ebb..5de2a666e30228a1368a9da1a910db8b574242d0 100644 --- a/snf-webproject/synnefo/webproject/i18n.py +++ b/snf-webproject/synnefo/webproject/i18n.py @@ -1,35 +1,17 @@ -# Copyright 2011 GRNET S.A. All rights reserved. -# -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: -# -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. -# -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# Copyright (C) 2010-2014 GRNET S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. # from django import http from django.conf import settings diff --git a/snf-webproject/synnefo/webproject/manage.py b/snf-webproject/synnefo/webproject/manage.py index a9e2a3d4fe3ef1381fb8fad22823ba87b5c7173b..26fff30a328cbaf345ebe5c6e47dac2b32a67fa2 100755 --- a/snf-webproject/synnefo/webproject/manage.py +++ b/snf-webproject/synnefo/webproject/manage.py @@ -1,35 +1,17 @@ -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. """ Extented django management module @@ -243,8 +225,9 @@ class SynnefoManagementUtility(ManagementUtility): # Encode stdout. This check is required because of the way python # checks if something is tty: # https://bugzilla.redhat.com/show_bug.cgi?id=841152 - if not subcommand in ['test'] and not 'shell' in subcommand: - sys.stdout = EncodedStdOut(sys.stdout) + if subcommand not in ['test'] and 'shell' not in subcommand: + sys.stdout = EncodedStream(sys.stdout) + sys.stderr = EncodedStream(sys.stderr) if subcommand == 'help': if len(args) > 2: @@ -262,7 +245,21 @@ class SynnefoManagementUtility(ManagementUtility): parser.print_lax_help() sys.stdout.write(self.main_help_text() + '\n') else: - self.fetch_command(subcommand).run_from_argv(self.argv) + sub_command = self.fetch_command(subcommand) + # NOTE: This is an ugly workaround to bypass the problem with + # the required permissions for the named pipes that Pithos backend + # is creating in order to communicate with XSEG. + if subcommand == 'test' or\ + subcommand.startswith('image-') or\ + subcommand.startswith('snapshot-') or\ + subcommand.startswith('file-'): + # Set common umask for known commands + os.umask(0o007) + # Allow command to define a custom umask + cmd_umask = getattr(sub_command, 'umask', None) + if cmd_umask is not None: + os.umask(cmd_umask) + sub_command.run_from_argv(self.argv) def main_help_text(self): """ @@ -309,22 +306,22 @@ def configure_logging(): log.warning("SNF_MANAGE_LOGGING_SETUP setting missing.") -class EncodedStdOut(object): - def __init__(self, stdout): +class EncodedStream(object): + def __init__(self, stream): try: - std_encoding = stdout.encoding + std_encoding = stream.encoding except AttributeError: std_encoding = None self.encoding = std_encoding or locale.getpreferredencoding() - self.original_stdout = stdout + self.original_stream = stream def write(self, string): if isinstance(string, unicode): - string = string.encode(self.encoding) - self.original_stdout.write(string) + string = string.encode(self.encoding, errors="replace") + self.original_stream.write(string) def __getattr__(self, name): - return getattr(self.original_stdout, name) + return getattr(self.original_stream, name) def main(): diff --git a/snf-webproject/synnefo/webproject/management/commands/link_static.py b/snf-webproject/synnefo/webproject/management/commands/link_static.py index ee6e7717bfd23a5b4c221f416054b957c6d617f6..af127a372251f1ad2ca854340d0c43805cf01d44 100644 --- a/snf-webproject/synnefo/webproject/management/commands/link_static.py +++ b/snf-webproject/synnefo/webproject/management/commands/link_static.py @@ -1,55 +1,45 @@ -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. """ Collect static files required by synnefo to a specific location """ -import os, shutil +import os from django.utils.importlib import import_module from optparse import make_option - -from django.core.management.base import BaseCommand, CommandError from django.conf import settings +from snf_django.management.commands import SynnefoCommand + STATIC_FILES = getattr(settings, "STATIC_FILES", {}) -class Command(BaseCommand): + +class Command(SynnefoCommand): help = 'Symlink static files to directory specified' - option_list = BaseCommand.option_list + ( - make_option('--static-root', + option_list = SynnefoCommand.option_list + ( + make_option( + '--static-root', action='store', dest='static_root', default=settings.MEDIA_ROOT, - help='Path to place symlinks (default: `%s`)' % settings.MEDIA_ROOT), - make_option('--dry-run', + help='Path to place symlinks (default: `%s`)' + % settings.MEDIA_ROOT), + make_option( + '--dry-run', action='store_true', dest='dry', default=False, @@ -58,29 +48,32 @@ class Command(BaseCommand): def collect_files(self, target): symlinks = [] - dirs_to_create = set() for module, ns in STATIC_FILES.iteritems(): module = import_module(module) - static_root = os.path.join(os.path.dirname(module.__file__), 'static') + static_root = os.path.join(os.path.dirname(module.__file__), + 'static') # no nested dir exists for the app if ns == '': for f in os.listdir(static_root): - symlinks.append((os.path.join(static_root, f), os.path.join(target, ns, f))) + symlinks.append((os.path.join(static_root, f), + os.path.join(target, ns, f))) # symlink whole app directory else: - symlinks.append((os.path.join(static_root), os.path.join(target, ns))) + symlinks.append((os.path.join(static_root), + os.path.join(target, ns))) return symlinks def handle(self, *args, **options): - print "The following synlinks will get created" + self.stderr.write("The following symlinks will be created\n") symlinks = self.collect_files(options['static_root']) for linkfrom, linkto in symlinks: - print "Symlink '%s' to '%s' will get created." % (linkfrom, linkto) + self.stderr.write("Symlink '%s' to '%s' will be created.\n" + % (linkfrom, linkto)) if not options['dry']: confirm = raw_input(""" @@ -89,10 +82,10 @@ Type 'yes' to continue, or 'no' to cancel: """) if confirm == "yes": for linkfrom, linkto in symlinks: - print "Creating link from %s to %s" % (linkfrom, linkto) + self.stderr.write("Creating link from %s to %s\n" + % (linkfrom, linkto)) if os.path.exists(linkto): - print "Skippig %s" % linkto + self.stderr.write("Skippig %s\n" % linkto) continue os.symlink(linkfrom, linkto) - diff --git a/snf-webproject/synnefo/webproject/management/commands/show_urls.py b/snf-webproject/synnefo/webproject/management/commands/show_urls.py index 8b21e010ec26c4d097ca65ecb37d8ecb9abee03b..8a861176f77653524c434d24ce4a84d6a537ad7a 100644 --- a/snf-webproject/synnefo/webproject/management/commands/show_urls.py +++ b/snf-webproject/synnefo/webproject/management/commands/show_urls.py @@ -2,10 +2,11 @@ # http://code.google.com/p/django-command-extensions/ from django.conf import settings -from django.core.management.base import BaseCommand from django.core.management import color from django.utils import termcolors +from snf_django.management.commands import SynnefoCommand + def color_style(): style = color.color_style() @@ -18,12 +19,15 @@ def color_style(): try: # 2008-05-30 admindocs found in newforms-admin brand - from django.contrib.admindocs.views import extract_views_from_urlpatterns, simplify_regex + from django.contrib.admindocs.views import \ + extract_views_from_urlpatterns, simplify_regex except ImportError: # fall back to trunk, pre-NFA merge - from django.contrib.admin.views.doc import extract_views_from_urlpatterns, simplify_regex + from django.contrib.admin.views.doc import \ + extract_views_from_urlpatterns, simplify_regex + -class Command(BaseCommand): +class Command(SynnefoCommand): help = "Displays all of the url matching routes for the project." requires_model_validation = True @@ -35,7 +39,8 @@ class Command(BaseCommand): style = color_style() if settings.ADMIN_FOR: - settings_modules = [__import__(m, {}, {}, ['']) for m in settings.ADMIN_FOR] + settings_modules = [__import__(m, {}, {}, ['']) + for m in settings.ADMIN_FOR] else: settings_modules = [settings] @@ -46,14 +51,21 @@ class Command(BaseCommand): except Exception, e: if options.get('traceback', None): import traceback - traceback.print_exc() - print style.ERROR("Error occurred while trying to load %s: %s" % (settings_mod.ROOT_URLCONF, str(e))) + self.stderr.write(traceback.format_exc() + "\n") + self.stderr.write( + style.ERROR("Error occurred while trying to load %s: %s\n" + % (settings_mod.ROOT_URLCONF, str(e)))) continue - view_functions = extract_views_from_urlpatterns(urlconf.urlpatterns) + view_functions = \ + extract_views_from_urlpatterns(urlconf.urlpatterns) for (func, regex) in view_functions: - func_name = hasattr(func, '__name__') and func.__name__ or repr(func) - views.append("%(url)s\t%(module)s.%(name)s" % {'name': style.MODULE_NAME(func_name), - 'module': style.MODULE(getattr(func, '__module__', '<no module>')), - 'url': style.URL(simplify_regex(regex))}) + func_name = hasattr(func, '__name__') and \ + func.__name__ or repr(func) + views.append("%(url)s\t%(module)s.%(name)s" + % {'name': style.MODULE_NAME(func_name), + 'module': style.MODULE( + getattr(func, '__module__', + '<no module>')), + 'url': style.URL(simplify_regex(regex))}) return "\n".join([v for v in views]) diff --git a/snf-webproject/synnefo/webproject/middleware/log.py b/snf-webproject/synnefo/webproject/middleware/log.py index 2680f21d6f65940ae3ffb1d119d9de9eefd5be6c..19b06ed7754407391f9b627546d9c3e5c44cc78f 100644 --- a/snf-webproject/synnefo/webproject/middleware/log.py +++ b/snf-webproject/synnefo/webproject/middleware/log.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from django.conf import settings from django.core.exceptions import MiddlewareNotUsed diff --git a/snf-webproject/synnefo/webproject/middleware/secure.py b/snf-webproject/synnefo/webproject/middleware/secure.py index 5d68e3cc71e85428d9489b2c1bbeea818528c913..e69be005689989223aad2664916cbb03a1304451 100644 --- a/snf-webproject/synnefo/webproject/middleware/secure.py +++ b/snf-webproject/synnefo/webproject/middleware/secure.py @@ -1,35 +1,17 @@ -# Copyright 2011-2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. class SecureMiddleware(object): diff --git a/snf-webproject/synnefo/webproject/settings/__init__.py b/snf-webproject/synnefo/webproject/settings/__init__.py index aa52d1c16b93f231aaf5fea1b5025a4e831e0ba0..e46e482de00a7be81ff441be0a80fa3a39166ceb 100644 --- a/snf-webproject/synnefo/webproject/settings/__init__.py +++ b/snf-webproject/synnefo/webproject/settings/__init__.py @@ -1,35 +1,17 @@ -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. from synnefo.webproject.settings.default import * diff --git a/snf-webproject/synnefo/webproject/templates/403.html b/snf-webproject/synnefo/webproject/templates/403.html index 4e315aa1b26195950c25ecdd239e7fc964e57b19..6530deff369ea78552fb8f343169062fcc3a0cb4 100644 --- a/snf-webproject/synnefo/webproject/templates/403.html +++ b/snf-webproject/synnefo/webproject/templates/403.html @@ -6,10 +6,13 @@ <title>Error 403</title> <meta name="viewport" content="width=device-width, initial-scale=1.0,minimum-scale=1.0, maximum-scale=1.0"> - <link href='https://fonts.googleapis.com/css?family=Open+Sans:400,600,700&subset=latin,greek-ext,greek' rel='stylesheet' type='text/css'> + {% for url in BRANDING_FONTS_CSS_URLS %} + <link href="{{ url }}" rel="stylesheet" type="text/css" > + {% endfor %} <script src="{{ MEDIA_URL }}webproject/js/jquery-1.7.1.min.js"></script> {% if CLOUDBAR_ACTIVE %} + <script>window.CLOUDBAR_INCLUDE_FONTS = false;</script> {{ CLOUDBAR_CODE }} {% endif %} <style type="text/css"> @@ -59,4 +62,4 @@ body { font-family: 'Open Sans', sans-ser </div> </body> </html> -<html><head></head><body></body></html> \ No newline at end of file +<html><head></head><body></body></html> diff --git a/snf-webproject/synnefo/webproject/templates/404.html b/snf-webproject/synnefo/webproject/templates/404.html index a83ed0b0cf3f3cd5b5ab20763518ecdf654be0d9..fb4f908ec6648e7c4ca30148176ff6866827dad6 100644 --- a/snf-webproject/synnefo/webproject/templates/404.html +++ b/snf-webproject/synnefo/webproject/templates/404.html @@ -6,10 +6,13 @@ <title>Page not found</title> <meta name="viewport" content="width=device-width, initial-scale=1.0,minimum-scale=1.0, maximum-scale=1.0"> - <link href='https://fonts.googleapis.com/css?family=Open+Sans:400,600,700&subset=latin,greek-ext,greek' rel='stylesheet' type='text/css'> + {% for url in BRANDING_FONTS_CSS_URLS %} + <link href="{{ url }}" rel="stylesheet" type="text/css" > + {% endfor %} <script src="{{ MEDIA_URL }}webproject/js/jquery-1.7.1.min.js"></script> {% if CLOUDBAR_ACTIVE %} + <script>window.CLOUDBAR_INCLUDE_FONTS = false;</script> {{ CLOUDBAR_CODE }} {% endif %} <style type="text/css"> diff --git a/snf-webproject/synnefo/webproject/templates/500.html b/snf-webproject/synnefo/webproject/templates/500.html index 1d927283b7cc5daa7e1b2aba9131dd8c49d36ba0..3a104f75ced5aeb12e340d429ab581c566addef9 100644 --- a/snf-webproject/synnefo/webproject/templates/500.html +++ b/snf-webproject/synnefo/webproject/templates/500.html @@ -6,10 +6,13 @@ <title>Page not found</title> <meta name="viewport" content="width=device-width, initial-scale=1.0,minimum-scale=1.0, maximum-scale=1.0"> - <link href='https://fonts.googleapis.com/css?family=Open+Sans:400,600,700&subset=latin,greek-ext,greek' rel='stylesheet' type='text/css'> + {% for url in BRANDING_FONTS_CSS_URLS %} + <link href="{{ url }}" rel="stylesheet" type="text/css" > + {% endfor %} <script src="{{ MEDIA_URL }}webproject/js/jquery-1.7.1.min.js"></script> {% if CLOUDBAR_ACTIVE %} + <script>window.CLOUDBAR_INCLUDE_FONTS = false;</script> {{ CLOUDBAR_CODE }} {% endif %} <style type="text/css"> diff --git a/snf-webproject/synnefo/webproject/urls.py b/snf-webproject/synnefo/webproject/urls.py index 4eb4b5a022605b4e4d3971c9c66d3cfb3219ba20..66e4c6508a1392be7c1e7c3dee59f27835a94438 100644 --- a/snf-webproject/synnefo/webproject/urls.py +++ b/snf-webproject/synnefo/webproject/urls.py @@ -1,35 +1,17 @@ -# Copyright 2011 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and -# documentation are those of the authors and should not be -# interpreted as representing official policies, either expressed -# or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import os diff --git a/update_version.py b/update_version.py index 060350c79376bc3243ec9701aa6a7147aa07f18c..ee05351852e63ac874d191aad3010defb5a2ad82 100644 --- a/update_version.py +++ b/update_version.py @@ -1,35 +1,17 @@ -#Copyright (C) 2010, 2011, 2012 GRNET S.A. All rights reserved. +# Copyright (C) 2010-2014 GRNET S.A. # -#Redistribution and use in source and binary forms, with or -#without modification, are permitted provided that the following -#conditions are met: +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# 1. Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. +# 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 General Public License for more details. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials -# provided with the distribution. -# -#THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS -#OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -#WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -#PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR -#CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -#SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -#LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -#USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -#AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -#LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -#ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -#POSSIBILITY OF SUCH DAMAGE. -# -#The views and conclusions contained in the software and -#documentation are those of the authors and should not be -#interpreted as representing official policies, either expressed -#or implied, of GRNET S.A. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import sys try: diff --git a/version b/version index d3739d4a218f7c0656511c0dae00063bec9a830d..d43b7336f1aac57ca70d8b8455001a15ef5f61d4 100644 --- a/version +++ b/version @@ -1,2 +1,2 @@ # This is a comment! -0.15.2 +0.16rc4